Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: bootstrap arguments for permissions boundary #22792

Merged
merged 41 commits into from
Nov 23, 2022
Merged
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
e2db370
feat: bootstrap arguments for permissions boundary
Naumel Nov 4, 2022
2353ce9
Update cdk.ts
Naumel Nov 4, 2022
302d4dc
Collapsing old commits
Naumel Nov 7, 2022
8cea87e
Integ tests
Naumel Nov 7, 2022
3749a90
Conditional permissions boundary
Naumel Nov 7, 2022
767755b
Proper string used in the test for default permissions boundary
Naumel Nov 7, 2022
efc8653
Split template cases based on th supplied argument.
Naumel Nov 9, 2022
074450b
Typo fix
Naumel Nov 9, 2022
09c2bff
String-ify permissionsboundary
Naumel Nov 9, 2022
82101af
AWS::NoValue for no param permissions boundary
Naumel Nov 9, 2022
35f922a
yaml is super fun
Naumel Nov 9, 2022
c102960
Update packages/aws-cdk/lib/api/bootstrap/bootstrap-template.yaml
Naumel Nov 9, 2022
defeea8
Incorporating feedback
Naumel Nov 14, 2022
8961c3b
README rephrase
Naumel Nov 14, 2022
d47e3b5
Added policy name validation
Naumel Nov 14, 2022
9d5e667
Adjusted param name and policy name input validation
Naumel Nov 15, 2022
ee95f76
Merge branch 'main' into perm-bound
mergify[bot] Nov 15, 2022
3ddfade
SDK call to IAM to create an example policy that can be referenced by…
Naumel Nov 17, 2022
a52f790
Update packages/aws-cdk/lib/api/bootstrap/bootstrap-template.yaml
Naumel Nov 17, 2022
396cf32
Update packages/aws-cdk/lib/api/bootstrap/bootstrap-environment.ts
Naumel Nov 17, 2022
ccd90f7
Update packages/aws-cdk/lib/api/bootstrap/bootstrap-template.yaml
Naumel Nov 17, 2022
ea88b89
Intermediary work for feedback, 4 unit tests are still failing
Naumel Nov 18, 2022
1debcdf
Update packages/aws-cdk/lib/api/bootstrap/bootstrap-template.yaml
Naumel Nov 21, 2022
157415c
Fixed the build
Naumel Nov 21, 2022
e75869f
Update packages/aws-cdk/README.md
Naumel Nov 21, 2022
96a8f64
Update packages/aws-cdk/README.md
Naumel Nov 21, 2022
303ba4d
Adding the change overview notes
Naumel Nov 21, 2022
3823e1e
Merge branch 'main' into perm-bound
mergify[bot] Nov 21, 2022
2fa9d19
Update packages/aws-cdk/README.md
Naumel Nov 21, 2022
bd7fbc2
Apply suggestions from code review
Naumel Nov 21, 2022
0745549
Partition must be part of the IAM policy arn for a successful getPoli…
Naumel Nov 21, 2022
dbf096c
Swapped the two error throwing points
Naumel Nov 21, 2022
3a660ee
Have dual policy definition for the time being
Naumel Nov 21, 2022
0b994a0
Fixed the param name for the template
Naumel Nov 22, 2022
1e7d1bd
adjusted the test for the input param name
Naumel Nov 22, 2022
8e29fb7
renaming remaining
Naumel Nov 22, 2022
58510fc
Fixing bootstrap prompt when using example policy
Naumel Nov 22, 2022
7f03db2
previous commit redo
Naumel Nov 22, 2022
16cf026
Apply missed partition in arn
Naumel Nov 22, 2022
79ecf77
Bubble up any IAM call error
Naumel Nov 22, 2022
7669755
Merge branch 'main' into perm-bound
mergify[bot] Nov 23, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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:

Naumel marked this conversation as resolved.
Show resolved Hide resolved
- `--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';
}
}
76 changes: 71 additions & 5 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 @@ -102,7 +102,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 Down Expand Up @@ -130,9 +130,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.PermissionsBoundary;
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, bootstrapTemplate, params);
}
if (currentPermissionsBoundary !== policyName) {
warning(`Switching from ${currentPermissionsBoundary} to ${inputPolicyName} as permissions boundary`);
}

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

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

if (permissionsBoundary === CDK_BOOTSTRAP_PERMISSIONS_BOUNDARY) {
const arn = await this.getExamplePermissionsBoundary(bootstrapTemplate, params.qualifier, environment.account, sdk);
const policyName = arn.split('/').pop();
permissionsBoundary = policyName ?? '';
}
this.validatePolicyName(permissionsBoundary);
return Promise.resolve(permissionsBoundary);
}

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 getExamplePermissionsBoundary(bootstrapTemplate: any, qualifier: string | undefined, account: string, sdk: ISDK): Promise<string> {
const iam = sdk.iam();
// if no Qualifier is supplied, resort to the default one
const policyName = qualifier ? `cdk-${qualifier}-permissions-boundary` : 'cdk-hnb659fds-permissions-boundary';
const arn = `arn::iam::${account}:policy/${policyName}`;
Naumel marked this conversation as resolved.
Show resolved Hide resolved

let getPolicyResp = await iam.getPolicy({ PolicyArn: arn }).promise();
Naumel marked this conversation as resolved.
Show resolved Hide resolved
if (getPolicyResp.Policy) {
return arn;
}

const policyDoc = JSON.parse(serializeStructure(bootstrapTemplate, true)).CdkBoostrapPermissionsBoundaryPolicy;
const request = {
PolicyName: policyName,
PolicyDocument: JSON.stringify(policyDoc),
};
const createPolicyResponse = await iam.createPolicy(request).promise();
Naumel marked this conversation as resolved.
Show resolved Hide resolved
return createPolicyResponse.Policy?.Arn ?? '';
Naumel marked this conversation as resolved.
Show resolved Hide resolved
}

private async customBootstrap(
environment: cxapi.Environment,
sdkProvider: SdkProvider,
Expand Down Expand Up @@ -187,6 +249,10 @@ 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:
corymhall marked this conversation as resolved.
Show resolved Hide resolved
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}-BootstrapPermissionBoundary-${AWS::AccountId}-${AWS::Region}
Naumel marked this conversation as resolved.
Show resolved Hide resolved
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}-BootstrapPermissionBoundary-${AWS::AccountId}-${AWS::Region}
Naumel marked this conversation as resolved.
Show resolved Hide resolved
# 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:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @Naumel @corymhall just curious, should the CdkBootstrapVersion be also updated as part of this change? (i.e. When do we increment this version?)

We're currently encountering an issue in our bootstrap stacks where:

  • developers with CDK toolkit v.2.54.0 are creating changesets to our toolkit stack and updating the template; and
  • developers with CDK toolkit less than v.2.54.0 creating changesets to our toolkit stack and downgrading the template.

Previously, we would encounter a warning:

Not downgrading existing bootstrap stack from version 'X+n' to version 'X'

that would prevent developers using an outdated CDK toolkit from downgrading the toolkit stack.

Wondering if this was the cause

Expand Down
4 changes: 4 additions & 0 deletions packages/aws-cdk/lib/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ async function parseCommandLineArguments() {
.command('bootstrap [ENVIRONMENTS..]', 'Deploys the CDK toolkit stack into an AWS environment', (yargs: Argv) => yargs
.option('bootstrap-bucket-name', { type: 'string', alias: ['b', 'toolkit-bucket-name'], desc: 'The name of the CDK toolkit bucket; bucket will be created and must not exist', default: undefined })
.option('bootstrap-kms-key-id', { type: 'string', desc: 'AWS KMS master key ID used for the SSE-KMS encryption', default: undefined, conflicts: 'bootstrap-customer-key' })
.option('example-permissions-boundary', { type: 'boolean', alias: ['epb', 'example-permissions-boundary'], desc: 'Use the example permissions boundary.', default: undefined, conflicts: 'custom-permissions-boundary' })
.option('custom-permissions-boundary', { type: 'string', alias: ['cpb', 'custom-permissions-boundary'], desc: 'Use the permissions boundary specified by name.', default: undefined, conflicts: 'example-permissions-boundary' })
.option('bootstrap-customer-key', { type: 'boolean', desc: 'Create a Customer Master Key (CMK) for the bootstrap bucket (you will be charged but can customize permissions, modern bootstrapping only)', default: undefined, conflicts: 'bootstrap-kms-key-id' })
.option('qualifier', { type: 'string', desc: 'String which must be unique for each bootstrap stack. You must configure it on your CDK app if you change this from the default.', default: undefined })
.option('public-access-block-configuration', { type: 'boolean', desc: 'Block public access configuration on CDK toolkit bucket (enabled by default) ', default: undefined })
Expand Down Expand Up @@ -462,6 +464,8 @@ async function initCommandLine() {
createCustomerMasterKey: args.bootstrapCustomerKey,
qualifier: args.qualifier,
publicAccessBlockConfiguration: args.publicAccessBlockConfiguration,
examplePermissionsBoundary: argv.examplePermissionsBoundary,
customPermissionsBoundary: argv.customPermissionsBoundary,
trustedAccounts: arrayFromYargs(args.trust),
trustedAccountsForLookup: arrayFromYargs(args.trustForLookup),
cloudFormationExecutionPolicies: arrayFromYargs(args.cloudformationExecutionPolicies),
Expand Down
47 changes: 46 additions & 1 deletion packages/aws-cdk/test/api/bootstrap2.test.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@

const mockDeployStack = jest.fn();

jest.mock('../../lib/api/deploy-stack', () => ({
deployStack: mockDeployStack,
}));

import { IAM } from 'aws-sdk';
import { Bootstrapper, DeployStackOptions, ToolkitInfo } from '../../lib/api';
import { mockBootstrapStack, MockSdk, MockSdkProvider } from '../util/mock-sdk';

let bootstrapper: Bootstrapper;
let mockGetPolicyIamCode: (params: IAM.Types.GetPolicyRequest) => IAM.Types.GetPolicyResponse;
let mockCreatePolicyIamCode: (params: IAM.Types.CreatePolicyRequest) => IAM.Types.CreatePolicyResponse;

beforeEach(() => {
bootstrapper = new Bootstrapper({ source: 'default' });
});
Expand All @@ -29,6 +34,18 @@ describe('Bootstrapping v2', () => {
sdk = new MockSdkProvider({ realSdk: false });
// By default, we'll return a non-found toolkit info
(ToolkitInfo as any).lookup = jest.fn().mockResolvedValue(ToolkitInfo.bootstraplessDeploymentsOnly(sdk.sdk));
const value = {
Policy: {
PolicyName: 'my-policy',
Arn: 'arn:aws:iam::0123456789012:policy/my-policy',
},
};
mockGetPolicyIamCode = jest.fn().mockReturnValue(value);
mockCreatePolicyIamCode = jest.fn().mockReturnValue(value);
sdk.stubIam({
createPolicy: mockCreatePolicyIamCode,
getPolicy: mockGetPolicyIamCode,
});
});

afterEach(() => {
Expand Down Expand Up @@ -82,6 +99,34 @@ describe('Bootstrapping v2', () => {
}));
});

test('passes true to PermissionsBoundary', async () => {
await bootstrapper.bootstrapEnvironment(env, sdk, {
parameters: {
examplePermissionsBoundary: true,
},
});

expect(mockDeployStack).toHaveBeenCalledWith(expect.objectContaining({
parameters: expect.objectContaining({
PermissionsBoundary: 'cdk-hnb659fds-permissions-boundary',
}),
}));
});

test('passes value to PermissionsBoundary', async () => {
await bootstrapper.bootstrapEnvironment(env, sdk, {
parameters: {
customPermissionsBoundary: 'permissions-boundary-name',
},
});

expect(mockDeployStack).toHaveBeenCalledWith(expect.objectContaining({
parameters: expect.objectContaining({
PermissionsBoundary: 'permissions-boundary-name',
}),
}));
});

test('passing trusted accounts without CFN managed policies results in an error', async () => {
await expect(bootstrapper.bootstrapEnvironment(env, sdk, {
parameters: {
Expand Down Expand Up @@ -328,4 +373,4 @@ describe('Bootstrapping v2', () => {
}));
});
});
});
});
Loading