Skip to content
8 changes: 8 additions & 0 deletions packages/@aws-cdk/toolkit-lib/lib/actions/bootstrap/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,14 @@ export interface BootstrapParameters {
* @default - No value, optional argument
*/
readonly customPermissionsBoundary?: string;

/**
* Whether to apply the permissions boundary to all bootstrap roles
* (not just the CloudFormation execution role)
*
* @default false
*/
readonly permissionsBoundaryAllRoles?: boolean;
}

export interface EnvironmentBootstrapResult {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,13 @@ export class Bootstrapper {
}
}

// Warn if --permissions-boundary-all-roles is used without a permissions boundary
if (params.permissionsBoundaryAllRoles && !inputPolicyName) {
await this.ioHelper.defaults.warn(
'--permissions-boundary-all-roles has no effect without --custom-permissions-boundary or --example-permissions-boundary',
);
}

const bootstrapTemplateParameters: Record<string, string | undefined> = {
FileAssetsBucketName: params.bucketName,
FileAssetsBucketKmsKeyId: kmsKeyId,
Expand All @@ -228,6 +235,7 @@ export class Bootstrapper {
? 'true'
: 'false',
InputPermissionsBoundary: policyName,
PermissionsBoundaryAllRoles: params.permissionsBoundaryAllRoles ? 'true' : 'false',
};

const templateParameters = await this.templateParameters();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,14 @@ export interface BootstrappingParameters {
*/
readonly customPermissionsBoundary?: string;

/**
* Whether to apply the permissions boundary to all bootstrap roles
* (not just the CloudFormation execution role)
*
* @default false
*/
readonly permissionsBoundaryAllRoles?: boolean;

/**
* Whether to deny AssumeRole calls with an ExternalId
*
Expand Down
8 changes: 8 additions & 0 deletions packages/@aws-cdk/toolkit-lib/test/actions/bootstrap.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,10 @@ describe('bootstrap', () => {
ParameterKey: 'PublicAccessBlockConfiguration',
ParameterValue: 'true',
},
{
ParameterKey: 'PermissionsBoundaryAllRoles',
ParameterValue: 'false',
},
]));
expectSuccessfulBootstrap();
});
Expand Down Expand Up @@ -356,6 +360,10 @@ describe('bootstrap', () => {
ParameterKey: 'PublicAccessBlockConfiguration',
ParameterValue: 'true',
},
{
ParameterKey: 'PermissionsBoundaryAllRoles',
ParameterValue: 'false',
},
]));
expectSuccessfulBootstrap();
});
Expand Down
158 changes: 158 additions & 0 deletions packages/@aws-cdk/toolkit-lib/test/api/bootstrap/bootstrap2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -791,4 +791,162 @@ describe('Bootstrapping v2', () => {
});
});
});

describe('PermissionsBoundaryAllRoles', () => {
test('passes PermissionsBoundaryAllRoles parameter as true when flag is set', async () => {
await bootstrapper.bootstrapEnvironment(env, sdk, {
parameters: {
customPermissionsBoundary: 'my-boundary',
permissionsBoundaryAllRoles: true,
},
});

expect(mockDeployStack).toHaveBeenCalledWith(
expect.objectContaining({
parameters: expect.objectContaining({
PermissionsBoundaryAllRoles: 'true',
}),
}),
expect.anything(),
);
});

test('passes PermissionsBoundaryAllRoles parameter as false when flag is not set', async () => {
await bootstrapper.bootstrapEnvironment(env, sdk, {
parameters: {
customPermissionsBoundary: 'my-boundary',
},
});

expect(mockDeployStack).toHaveBeenCalledWith(
expect.objectContaining({
parameters: expect.objectContaining({
PermissionsBoundaryAllRoles: 'false',
}),
}),
expect.anything(),
);
});

test('passes PermissionsBoundaryAllRoles parameter as false by default', async () => {
await bootstrapper.bootstrapEnvironment(env, sdk, {
parameters: {},
});

expect(mockDeployStack).toHaveBeenCalledWith(
expect.objectContaining({
parameters: expect.objectContaining({
PermissionsBoundaryAllRoles: 'false',
}),
}),
expect.anything(),
);
});

test('shows warning when permissionsBoundaryAllRoles is used without permissions boundary', async () => {
await bootstrapper.bootstrapEnvironment(env, sdk, {
parameters: {
permissionsBoundaryAllRoles: true,
},
});

expect(stderrMock.mock.calls).toEqual(
expect.arrayContaining([
expect.arrayContaining([
expect.stringMatching(/--permissions-boundary-all-roles has no effect without --custom-permissions-boundary or --example-permissions-boundary/),
]),
]),
);
});

test('no warning when permissionsBoundaryAllRoles is used with customPermissionsBoundary', async () => {
await bootstrapper.bootstrapEnvironment(env, sdk, {
parameters: {
customPermissionsBoundary: 'my-boundary',
permissionsBoundaryAllRoles: true,
},
});

expect(stderrMock.mock.calls).not.toEqual(
expect.arrayContaining([
expect.arrayContaining([
expect.stringMatching(/--permissions-boundary-all-roles has no effect/),
]),
]),
);
});

test('no warning when permissionsBoundaryAllRoles is used with examplePermissionsBoundary', async () => {
await bootstrapper.bootstrapEnvironment(env, sdk, {
parameters: {
examplePermissionsBoundary: true,
permissionsBoundaryAllRoles: true,
},
});

expect(stderrMock.mock.calls).not.toEqual(
expect.arrayContaining([
expect.arrayContaining([
expect.stringMatching(/--permissions-boundary-all-roles has no effect/),
]),
]),
);
});

test('no warning when permissionsBoundaryAllRoles is false without permissions boundary', async () => {
await bootstrapper.bootstrapEnvironment(env, sdk, {
parameters: {
permissionsBoundaryAllRoles: false,
},
});

expect(stderrMock.mock.calls).not.toEqual(
expect.arrayContaining([
expect.arrayContaining([
expect.stringMatching(/--permissions-boundary-all-roles has no effect/),
]),
]),
);
});

test('bootstrap template contains PermissionsBoundaryAllRoles parameter', async () => {
const testBootstrapper = new Bootstrapper({ source: 'default' }, ioHelper);
const template = await (testBootstrapper as any).loadTemplate();

expect(template.Parameters.PermissionsBoundaryAllRoles).toBeDefined();
expect(template.Parameters.PermissionsBoundaryAllRoles.Default).toBe('false');
expect(template.Parameters.PermissionsBoundaryAllRoles.AllowedValues).toEqual(['true', 'false']);
});

test('bootstrap template contains ApplyPermissionsBoundaryToAllRoles condition', async () => {
const testBootstrapper = new Bootstrapper({ source: 'default' }, ioHelper);
const template = await (testBootstrapper as any).loadTemplate();

expect(template.Conditions.ApplyPermissionsBoundaryToAllRoles).toBeDefined();
});

test('bootstrap template has PermissionsBoundary on additional roles with condition', async () => {
const testBootstrapper = new Bootstrapper({ source: 'default' }, ioHelper);
const template = await (testBootstrapper as any).loadTemplate();

// FilePublishingRole, ImagePublishingRole, LookupRole, and DeploymentActionRole
// should have conditional PermissionsBoundary
const rolesToCheck = ['FilePublishingRole', 'ImagePublishingRole', 'LookupRole', 'DeploymentActionRole'];
for (const roleName of rolesToCheck) {
const role = template.Resources[roleName];
expect(role.Properties.PermissionsBoundary).toBeDefined();
expect(role.Properties.PermissionsBoundary['Fn::If'][0]).toBe('ApplyPermissionsBoundaryToAllRoles');
}
});

test('CloudFormationExecutionRole always uses PermissionsBoundarySet condition', async () => {
const testBootstrapper = new Bootstrapper({ source: 'default' }, ioHelper);
const template = await (testBootstrapper as any).loadTemplate();

const cfnRole = template.Resources.CloudFormationExecutionRole;
expect(cfnRole.Properties.PermissionsBoundary).toBeDefined();
// CloudFormationExecutionRole uses PermissionsBoundarySet condition, not ApplyPermissionsBoundaryToAllRoles
expect(cfnRole.Properties.PermissionsBoundary['Fn::If'][0]).toBe('PermissionsBoundarySet');
});
});
});
31 changes: 31 additions & 0 deletions packages/aws-cdk/lib/api/bootstrap/bootstrap-template.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ Parameters:
Default: 'false'
AllowedValues: [ 'true', 'false' ]
Type: String
PermissionsBoundaryAllRoles:
Description: Whether to apply the permissions boundary to all bootstrap roles (not just the CloudFormation execution role)
Default: 'false'
Type: String
AllowedValues: ['true', 'false']
BootstrapVariant:
Type: String
Default: 'AWS CDK: Default Resources'
Expand Down Expand Up @@ -112,6 +117,12 @@ Conditions:
- Fn::Equals:
- ''
- Ref: InputPermissionsBoundary
ApplyPermissionsBoundaryToAllRoles:
Fn::And:
- Condition: PermissionsBoundarySet
- Fn::Equals:
- 'true'
- Ref: PermissionsBoundaryAllRoles
HasCustomContainerAssetsRepositoryName:
Fn::Not:
- Fn::Equals:
Expand Down Expand Up @@ -358,6 +369,11 @@ Resources:
- Ref: AWS::NoValue
RoleName:
Fn::Sub: cdk-${Qualifier}-file-publishing-role-${AWS::AccountId}-${AWS::Region}
PermissionsBoundary:
Fn::If:
- ApplyPermissionsBoundaryToAllRoles
- Fn::Sub: 'arn:${AWS::Partition}:iam::${AWS::AccountId}:policy/${InputPermissionsBoundary}'
- Ref: AWS::NoValue
Tags:
- Key: aws-cdk:bootstrap-role
Value: file-publishing
Expand Down Expand Up @@ -412,6 +428,11 @@ Resources:
- Ref: AWS::NoValue
RoleName:
Fn::Sub: cdk-${Qualifier}-image-publishing-role-${AWS::AccountId}-${AWS::Region}
PermissionsBoundary:
Fn::If:
- ApplyPermissionsBoundaryToAllRoles
- Fn::Sub: 'arn:${AWS::Partition}:iam::${AWS::AccountId}:policy/${InputPermissionsBoundary}'
- Ref: AWS::NoValue
Tags:
- Key: aws-cdk:bootstrap-role
Value: image-publishing
Expand Down Expand Up @@ -492,6 +513,11 @@ Resources:
- Ref: AWS::NoValue
RoleName:
Fn::Sub: cdk-${Qualifier}-lookup-role-${AWS::AccountId}-${AWS::Region}
PermissionsBoundary:
Fn::If:
- ApplyPermissionsBoundaryToAllRoles
- Fn::Sub: 'arn:${AWS::Partition}:iam::${AWS::AccountId}:policy/${InputPermissionsBoundary}'
- Ref: AWS::NoValue
ManagedPolicyArns:
- Fn::Sub: "arn:${AWS::Partition}:iam::aws:policy/ReadOnlyAccess"
Policies:
Expand Down Expand Up @@ -721,6 +747,11 @@ Resources:
PolicyName: default
RoleName:
Fn::Sub: cdk-${Qualifier}-deploy-role-${AWS::AccountId}-${AWS::Region}
PermissionsBoundary:
Fn::If:
- ApplyPermissionsBoundaryToAllRoles
- Fn::Sub: 'arn:${AWS::Partition}:iam::${AWS::AccountId}:policy/${InputPermissionsBoundary}'
- Ref: AWS::NoValue
Tags:
- Key: aws-cdk:bootstrap-role
Value: deploy
Expand Down
1 change: 1 addition & 0 deletions packages/aws-cdk/lib/cli/cli-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ export async function makeConfig(): Promise<CliConfig> {
'bootstrap-kms-key-id': { type: 'string', desc: 'AWS KMS master key ID used for the SSE-KMS encryption (specify AWS_MANAGED_KEY to use an AWS-managed key)', default: undefined, conflicts: 'bootstrap-customer-key' },
'example-permissions-boundary': { type: 'boolean', alias: 'epb', desc: 'Use the example permissions boundary.', default: undefined, conflicts: 'custom-permissions-boundary' },
'custom-permissions-boundary': { type: 'string', alias: 'cpb', desc: 'Use the permissions boundary specified by name.', default: undefined, conflicts: 'example-permissions-boundary' },
'permissions-boundary-all-roles': { type: 'boolean', desc: 'Apply the permissions boundary to all bootstrap roles (not just the CloudFormation execution role).', default: false },
'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' },
'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 },
'public-access-block-configuration': { type: 'boolean', desc: 'Block public access configuration on CDK toolkit bucket (enabled by default) ', default: undefined },
Expand Down
5 changes: 5 additions & 0 deletions packages/aws-cdk/lib/cli/cli-type-registry.json
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,11 @@
"desc": "Use the permissions boundary specified by name.",
"conflicts": "example-permissions-boundary"
},
"permissions-boundary-all-roles": {
"type": "boolean",
"desc": "Apply the permissions boundary to all bootstrap roles (not just the CloudFormation execution role).",
"default": false
},
"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)",
Expand Down
1 change: 1 addition & 0 deletions packages/aws-cdk/lib/cli/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,7 @@ export async function exec(args: string[], synthesizer?: Synthesizer): Promise<n
publicAccessBlockConfiguration: args.publicAccessBlockConfiguration,
examplePermissionsBoundary: argv.examplePermissionsBoundary,
customPermissionsBoundary: argv.customPermissionsBoundary,
permissionsBoundaryAllRoles: args.permissionsBoundaryAllRoles,
trustedAccounts: arrayFromYargs(args.trust),
trustedAccountsForLookup: arrayFromYargs(args.trustForLookup),
untrustedAccounts: arrayFromYargs(args.untrust),
Expand Down
2 changes: 2 additions & 0 deletions packages/aws-cdk/lib/cli/convert-to-user-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export function convertYargsToUserInput(args: any): UserInput {
bootstrapKmsKeyId: args.bootstrapKmsKeyId,
examplePermissionsBoundary: args.examplePermissionsBoundary,
customPermissionsBoundary: args.customPermissionsBoundary,
permissionsBoundaryAllRoles: args.permissionsBoundaryAllRoles,
bootstrapCustomerKey: args.bootstrapCustomerKey,
qualifier: args.qualifier,
publicAccessBlockConfiguration: args.publicAccessBlockConfiguration,
Expand Down Expand Up @@ -369,6 +370,7 @@ export function convertConfigToUserInput(config: any): UserInput {
bootstrapKmsKeyId: config.bootstrap?.bootstrapKmsKeyId,
examplePermissionsBoundary: config.bootstrap?.examplePermissionsBoundary,
customPermissionsBoundary: config.bootstrap?.customPermissionsBoundary,
permissionsBoundaryAllRoles: config.bootstrap?.permissionsBoundaryAllRoles,
bootstrapCustomerKey: config.bootstrap?.bootstrapCustomerKey,
qualifier: config.bootstrap?.qualifier,
publicAccessBlockConfiguration: config.bootstrap?.publicAccessBlockConfiguration,
Expand Down
5 changes: 5 additions & 0 deletions packages/aws-cdk/lib/cli/parse-command-line-arguments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,11 @@ export function parseCommandLineArguments(args: Array<string>): any {
desc: 'Use the permissions boundary specified by name.',
conflicts: 'example-permissions-boundary',
})
.option('permissions-boundary-all-roles', {
default: false,
type: 'boolean',
desc: 'Apply the permissions boundary to all bootstrap roles (not just the CloudFormation execution role).',
})
.option('bootstrap-customer-key', {
default: undefined,
type: 'boolean',
Expand Down
7 changes: 7 additions & 0 deletions packages/aws-cdk/lib/cli/user-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,13 @@ export interface BootstrapOptions {
*/
readonly customPermissionsBoundary?: string;

/**
* Apply the permissions boundary to all bootstrap roles (not just the CloudFormation execution role).
*
* @default - false
*/
readonly permissionsBoundaryAllRoles?: boolean;

/**
* Create a Customer Master Key (CMK) for the bootstrap bucket (you will be charged but can customize permissions, modern bootstrapping only)
*
Expand Down