Skip to content

Commit

Permalink
feat: bootstrap arguments for permissions boundary (#22792)
Browse files Browse the repository at this point in the history
#22744

Users can now specify in the CDK CLI a [(permissions boundary) policy](https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_boundaries.html) to be applied on the Execution Role and all subsequent IAM users and roles of their app.

If you want to try out the feature, a good starting point is having the`--example-permissions-boundary`(or `--epb`) parameter for the `cdk botstrap`:
```
cdk boostrap --epb
```
This achieves a couple of things: a new policy will be created (if not already present) in the account being bootstrapped (`cdk-${qualifier}-permissions-boundary`) and it will be referenced in the bootstrap template. In order for the bootstrap to be successful, the credentials use must include `iam:getPolicy` and `iam:createPolicy` permissions.
This works pairs with #22913, as permissions boundary needs propagation.
You can inspect the policy via the console, retrieve it via aws cli or sdk and you can copy the structure to use on your own from `packages/aws-cdk/lib/api/bootstrap/bootstrap-template.yaml`: Resources.CdkBoostrapPermissionsBoundaryPolicy

At this point you can edit the policy, add restrictions and see what scope would match your requirements.

For non-dev work, the suggestion is to use `--custom-permissions-boundary` (or `--cpb`):
```
cdk bootstrap --cpb "custom-policy-name"
```
The policy must be created and accessible for the credentials used to perform the bootstrap.

----

### All Submissions:

* [x] Have you followed the guidelines in our [Contributing guide?](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md)

### Adding new Unconventional Dependencies:

* [ ] This PR adds new unconventional dependencies following the process described [here](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md/#adding-new-unconventional-dependencies)

### New Features

* [ ] Have you added the new feature to an [integration test](https://github.com/aws/aws-cdk/blob/main/INTEGRATION_TESTS.md)?
	* [ ] Did you use `yarn integ` to deploy the infrastructure and generate the snapshot (i.e. `yarn integ` without `--dry-run`)?

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
Naumel authored Nov 23, 2022
1 parent 0bfce89 commit 6224b6d
Show file tree
Hide file tree
Showing 10 changed files with 340 additions and 14 deletions.
12 changes: 10 additions & 2 deletions packages/aws-cdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -568,8 +568,9 @@ $ cdk bootstrap --app='node bin/main.js' foo bar
By default, bootstrap stack will be protected from stack termination. This can be disabled using
`--termination-protection` argument.

If you have specific needs, policies, or requirements not met by the default template, you can customize it
to fit your own situation, by exporting the default one to a file and either deploying it yourself
If you have specific prerequisites not met by the example template, you can
[customize it](https://docs.aws.amazon.com/cdk/v2/guide/bootstrapping.html#bootstrapping-customizing)
to fit your requirements, by exporting the provided one to a file and either deploying it yourself
using CloudFormation directly, or by telling the CLI to use a custom template. That looks as follows:

```console
Expand All @@ -582,6 +583,13 @@ $ cdk bootstrap --show-template > bootstrap-template.yaml
$ cdk bootstrap --template bootstrap-template.yaml
```

Out of the box customization options are also available as arguments. To use a permissions boundary:

- `--example-permissions-boundary` indicates the example permissions boundary, supplied by CDK
- `--custom-permissions-boundary` specifies, by name a predefined, customer maintained, boundary

A few notes to add at this point. The CDK supplied permissions boundary policy should be regarded as an example. Edit the content and reference the example policy if you're testing out the feature, turn it into a new policy for actual deployments (if one does not already exist). The concern here is drift as, most likely, a permissions boundary is maintained and has dedicated conventions, naming included.

### `cdk doctor`

Inspect the current command-line environment and configurations, and collect information that can be useful for
Expand Down
9 changes: 7 additions & 2 deletions packages/aws-cdk/lib/api/aws-auth/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { Account } from './sdk-provider';
// We need to map regions to domain suffixes, and the SDK already has a function to do this.
// It's not part of the public API, but it's also unlikely to go away.
//
// Reuse that function, and add a safety check so we don't accidentally break if they ever
// Reuse that function, and add a safety check, so we don't accidentally break if they ever
// refactor that away.

/* eslint-disable @typescript-eslint/no-require-imports */
Expand Down Expand Up @@ -53,6 +53,7 @@ export interface ISDK {
lambda(): AWS.Lambda;
cloudFormation(): AWS.CloudFormation;
ec2(): AWS.EC2;
iam(): AWS.IAM;
ssm(): AWS.SSM;
s3(): AWS.S3;
route53(): AWS.Route53;
Expand Down Expand Up @@ -163,6 +164,10 @@ export class SDK implements ISDK {
return this.wrapServiceErrorHandling(new AWS.EC2(this.config));
}

public iam(): AWS.IAM {
return this.wrapServiceErrorHandling(new AWS.IAM(this.config));
}

public ssm(): AWS.SSM {
return this.wrapServiceErrorHandling(new AWS.SSM(this.config));
}
Expand Down Expand Up @@ -415,4 +420,4 @@ function allChainedExceptionMessages(e: Error | undefined) {
*/
export function isUnrecoverableAwsError(e: Error) {
return (e as any).code === 'ExpiredToken';
}
}
144 changes: 137 additions & 7 deletions packages/aws-cdk/lib/api/bootstrap/bootstrap-environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import * as cxapi from '@aws-cdk/cx-api';
import { warning } from '../../logging';
import { loadStructuredFile, serializeStructure } from '../../serialize';
import { rootDir } from '../../util/directories';
import { SdkProvider } from '../aws-auth';
import { ISDK, Mode, SdkProvider } from '../aws-auth';
import { DeployStackResult } from '../deploy-stack';
import { BootstrapEnvironmentOptions, BootstrappingParameters } from './bootstrap-props';
import { BootstrapStack, bootstrapVersionFromTemplate } from './deploy-bootstrap';
Expand Down Expand Up @@ -79,6 +79,7 @@ export class Bootstrapper {
const bootstrapTemplate = await this.loadTemplate();

const current = await BootstrapStack.lookup(sdkProvider, environment, options.toolkitStackName);
const partition = await current.partition();

if (params.createCustomerMasterKey !== undefined && params.kmsKeyId) {
throw new Error('You cannot pass \'--bootstrap-kms-key-id\' and \'--bootstrap-customer-key\' together. Specify one or the other');
Expand All @@ -102,7 +103,7 @@ export class Bootstrapper {
if (trustedAccounts.length === 0 && cloudFormationExecutionPolicies.length === 0) {
// For self-trust it's okay to default to AdministratorAccess, and it improves the usability of bootstrapping a lot.
//
// We don't actually make the implicity policy a physical parameter. The template will infer it instead,
// We don't actually make the implicitly policy a physical parameter. The template will infer it instead,
// we simply do the UI advertising that behavior here.
//
// If we DID make it an explicit parameter, we wouldn't be able to tell the difference between whether
Expand All @@ -113,7 +114,7 @@ export class Bootstrapper {
//
// Would leave AdministratorAccess policies with a trust relationship, without the user explicitly
// approving the trust policy.
const implicitPolicy = `arn:${await current.partition()}:iam::aws:policy/AdministratorAccess`;
const implicitPolicy = `arn:${partition}:iam::aws:policy/AdministratorAccess`;
warning(`Using default execution policy of '${implicitPolicy}'. Pass '--cloudformation-execution-policies' to customize.`);
} else if (cloudFormationExecutionPolicies.length === 0) {
throw new Error('Please pass \'--cloudformation-execution-policies\' when using \'--trust\' to specify deployment permissions. Try a managed policy of the form \'arn:aws:iam::aws:policy/<PolicyName>\'.');
Expand All @@ -130,9 +131,25 @@ export class Bootstrapper {
// * '-' if this is the first time we're deploying this stack (or upgrading from old to new bootstrap)
const currentKmsKeyId = current.parameters.FileAssetsBucketKmsKeyId;
const kmsKeyId = params.kmsKeyId ??
(params.createCustomerMasterKey === true ? CREATE_NEW_KEY :
params.createCustomerMasterKey === false || currentKmsKeyId === undefined ? USE_AWS_MANAGED_KEY :
undefined);
(params.createCustomerMasterKey === true ? CREATE_NEW_KEY :
params.createCustomerMasterKey === false || currentKmsKeyId === undefined ? USE_AWS_MANAGED_KEY : undefined);

/* A permissions boundary can be provided via:
* - the flag indicating the example one should be used
* - the name indicating the custom permissions boundary to be used
* Re-bootstrapping will NOT be blocked by either tightening or relaxing the permissions' boundary.
*/
const currentPermissionsBoundary = current.parameters.InputPermissionsBoundary;
const inputPolicyName = params.examplePermissionsBoundary ? CDK_BOOTSTRAP_PERMISSIONS_BOUNDARY : params.customPermissionsBoundary;
let policyName;
if (inputPolicyName) {
// If the example policy is not already in place, it must be created.
const sdk = (await sdkProvider.forEnvironment(environment, Mode.ForWriting)).sdk;
policyName = await this.getPolicyName(environment, sdk, inputPolicyName, partition, params);
}
if (currentPermissionsBoundary !== policyName) {
warning(`Switching from ${currentPermissionsBoundary} to ${policyName} as permissions boundary`);
}

return current.update(
bootstrapTemplate,
Expand All @@ -145,12 +162,121 @@ export class Bootstrapper {
CloudFormationExecutionPolicies: cloudFormationExecutionPolicies.join(','),
Qualifier: params.qualifier,
PublicAccessBlockConfiguration: params.publicAccessBlockConfiguration || params.publicAccessBlockConfiguration === undefined ? 'true' : 'false',
InputPermissionsBoundary: policyName,
}, {
...options,
terminationProtection: options.terminationProtection ?? current.terminationProtection,
});
}

private async getPolicyName(
environment: cxapi.Environment,
sdk: ISDK,
permissionsBoundary: string,
partition: string,
params: BootstrappingParameters): Promise<string> {

if (permissionsBoundary !== CDK_BOOTSTRAP_PERMISSIONS_BOUNDARY) {
this.validatePolicyName(permissionsBoundary);
return Promise.resolve(permissionsBoundary);
}
// if no Qualifier is supplied, resort to the default one
const arn = await this.getExamplePermissionsBoundary(params.qualifier ?? 'hnb659fds', partition, environment.account, sdk);
const policyName = arn.split('/').pop();
if (!policyName) {
throw new Error('Could not retrieve the example permission boundary!');
}
return Promise.resolve(policyName);
}

private async getExamplePermissionsBoundary(qualifier: string, partition: string, account: string, sdk: ISDK): Promise<string> {
const iam = sdk.iam();

let policyName = `cdk-${qualifier}-permissions-boundary`;
const arn = `arn:${partition}:iam::${account}:policy/${policyName}`;

try {
let getPolicyResp = await iam.getPolicy({ PolicyArn: arn }).promise();
if (getPolicyResp.Policy) {
return arn;
}
} catch (e) {
// https://docs.aws.amazon.com/IAM/latest/APIReference/API_GetPolicy.html#API_GetPolicy_Errors
if (e.name === 'NoSuchEntity') {
//noop, proceed with creating the policy
} else {
throw e;
}
}

const policyDoc = {
Version: '2012-10-17',
Statement: [
{
Action: ['*'],
Resource: '*',
Effect: 'Allow',
Sid: 'ExplicitAllowAll',
},
{
Condition: {
StringEquals: {
'iam:PermissionsBoundary': `arn:${partition}:iam::${account}:policy/cdk-${qualifier}-permissions-boundary`,
},
},
Action: [
'iam:CreateUser',
'iam:CreateRole',
'iam:PutRolePermissionsBoundary',
'iam:PutUserPermissionsBoundary',
],
Resource: '*',
Effect: 'Allow',
Sid: 'DenyAccessIfRequiredPermBoundaryIsNotBeingApplied',
},
{
Action: [
'iam:CreatePolicyVersion',
'iam:DeletePolicy',
'iam:DeletePolicyVersion',
'iam:SetDefaultPolicyVersion',
],
Resource: `arn:${partition}:iam::${account}:policy/cdk-${qualifier}-permissions-boundary`,
Effect: 'Deny',
Sid: 'DenyPermBoundaryIAMPolicyAlteration',
},
{
Action: [
'iam:DeleteUserPermissionsBoundary',
'iam:DeleteRolePermissionsBoundary',
],
Resource: '*',
Effect: 'Deny',
Sid: 'DenyRemovalOfPermBoundaryFromAnyUserOrRole',
},
],
};
const request = {
PolicyName: policyName,
PolicyDocument: JSON.stringify(policyDoc),
};
const createPolicyResponse = await iam.createPolicy(request).promise();
if (createPolicyResponse.Policy?.Arn) {
return createPolicyResponse.Policy.Arn;
} else {
throw new Error(`Could not retrieve the example permission boundary ${arn}!`);
}
}

private validatePolicyName(permissionsBoundary: string) {
// https://docs.aws.amazon.com/IAM/latest/APIReference/API_CreatePolicy.html
const regexp: RegExp = /[\w+=,.@-]+/;
const matches = regexp.exec(permissionsBoundary);
if (!(matches && matches.length === 1 && matches[0] === permissionsBoundary)) {
throw new Error(`The permissions boundary name ${permissionsBoundary} does not match the IAM conventions.`);
}
}

private async customBootstrap(
environment: cxapi.Environment,
sdkProvider: SdkProvider,
Expand Down Expand Up @@ -179,14 +305,18 @@ export class Bootstrapper {
}

/**
* Magic parameter value that will cause the bootstrap-template.yml to NOT create a CMK but use the default keyo
* Magic parameter value that will cause the bootstrap-template.yml to NOT create a CMK but use the default key
*/
const USE_AWS_MANAGED_KEY = 'AWS_MANAGED_KEY';

/**
* Magic parameter value that will cause the bootstrap-template.yml to create a CMK
*/
const CREATE_NEW_KEY = '';
/**
* Parameter value indicating the use of the default, CDK provided permissions boundary for bootstrap-template.yml
*/
const CDK_BOOTSTRAP_PERMISSIONS_BOUNDARY = 'CDK_BOOTSTRAP_PERMISSIONS_BOUNDARY';

/**
* Split an array-like CloudFormation parameter on ,
Expand Down
16 changes: 15 additions & 1 deletion packages/aws-cdk/lib/api/bootstrap/bootstrap-props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,4 +101,18 @@ export interface BootstrappingParameters {
*/
readonly publicAccessBlockConfiguration?: boolean;

}
/**
* Flag for using the default permissions boundary for bootstrapping
*
* @default - No value, optional argument
*/
readonly examplePermissionsBoundary?: boolean;

/**
* Name for the customer's custom permissions boundary for bootstrapping
*
* @default - No value, optional argument
*/
readonly customPermissionsBoundary?: string;

}
77 changes: 77 additions & 0 deletions packages/aws-cdk/lib/api/bootstrap/bootstrap-template.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,14 @@ Parameters:
Default: 'true'
Type: 'String'
AllowedValues: ['true', 'false']
InputPermissionsBoundary:
Description: Whether or not to use either the CDK supplied or custom permissions boundary
Default: ''
Type: 'String'
UseExamplePermissionsBoundary:
Default: 'false'
AllowedValues: [ 'true', 'false' ]
Type: String
Conditions:
HasTrustedAccounts:
Fn::Not:
Expand Down Expand Up @@ -77,6 +85,15 @@ Conditions:
Fn::Equals:
- 'AWS_MANAGED_KEY'
- Ref: FileAssetsBucketKmsKeyId
ShouldCreatePermissionsBoundary:
Fn::Equals:
- 'true'
- Ref: UseExamplePermissionsBoundary
PermissionsBoundarySet:
Fn::Not:
- Fn::Equals:
- ''
- Ref: InputPermissionsBoundary
HasCustomContainerAssetsRepositoryName:
Fn::Not:
- Fn::Equals:
Expand Down Expand Up @@ -500,6 +517,66 @@ Resources:
- - Fn::Sub: "arn:${AWS::Partition}:iam::aws:policy/AdministratorAccess"
RoleName:
Fn::Sub: cdk-${Qualifier}-cfn-exec-role-${AWS::AccountId}-${AWS::Region}
PermissionsBoundary:
Fn::If:
- PermissionsBoundarySet
- Fn::Sub: 'arn:${AWS::Partition}:iam::${AWS::AccountId}:policy/${InputPermissionsBoundary}'
- Ref: AWS::NoValue
CdkBoostrapPermissionsBoundaryPolicy:
# Edit the template prior to boostrap in order to have this example policy created
Condition: ShouldCreatePermissionsBoundary
Type: AWS::IAM::ManagedPolicy
Properties:
PolicyDocument:
Statement:
# If permission boundaries do not have an explicit `allow`, then the effect is `deny`
- Sid: ExplicitAllowAll
Action:
- "*"
Effect: Allow
Resource: "*"
# Default permissions to prevent privilege escalation
- Sid: DenyAccessIfRequiredPermBoundaryIsNotBeingApplied
Action:
- iam:CreateUser
- iam:CreateRole
- iam:PutRolePermissionsBoundary
- iam:PutUserPermissionsBoundary
Condition:
StringNotEquals:
iam:PermissionsBoundary:
Fn::Sub: arn:${AWS::Partition}:iam::${AWS::AccountId}:policy/cdk-${Qualifier}-permissions-boundary-${AWS::AccountId}-${AWS::Region}
Effect: Deny
Resource: "*"
# Forbid the policy itself being edited
- Sid: DenyPermBoundaryIAMPolicyAlteration
Action:
- iam:CreatePolicyVersion
- iam:DeletePolicy
- iam:DeletePolicyVersion
- iam:SetDefaultPolicyVersion
Effect: Deny
Resource:
Fn::Sub: arn:${AWS::Partition}:iam::${AWS::AccountId}:policy/cdk-${Qualifier}-permissions-boundary-${AWS::AccountId}-${AWS::Region}
# Forbid removing the permissions boundary from any user or role that has it associated
- Sid: DenyRemovalOfPermBoundaryFromAnyUserOrRole
Action:
- iam:DeleteUserPermissionsBoundary
- iam:DeleteRolePermissionsBoundary
Effect: Deny
Resource: "*"
# Add your specific organizational security policy here
# Uncomment the example to deny access to AWS Config
#- Sid: OrganizationalSecurityPolicy
# Action:
# - "config:*"
# Effect: Deny
# Resource: "*"
Version: "2012-10-17"
Description: "Bootstrap Permission Boundary"
ManagedPolicyName:
Fn::Sub: cdk-${Qualifier}-permissions-boundary-${AWS::AccountId}-${AWS::Region}
Path: /
# The SSM parameter is used in pipeline-deployed templates to verify the version
# of the bootstrap resources.
CdkBootstrapVersion:
Expand Down
Loading

0 comments on commit 6224b6d

Please sign in to comment.