Skip to content

Commit 4713bdd

Browse files
authored
feat(cli): add --untrust option to bootstrap (#33091)
Add a new option, `--untrust`, to the `bootstrap` command. Passing a list of account IDs as values to this option removes those account IDs from the trust relationships in the bootstrap roles. Closes #22703. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent 344d916 commit 4713bdd

File tree

10 files changed

+151
-3
lines changed

10 files changed

+151
-3
lines changed

packages/@aws-cdk-testing/cli-integ/lib/with-cdk-app.ts

+11
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,10 @@ export interface CdkModernBootstrapCommandOptions extends CommonCdkBootstrapComm
290290
* @default undefined
291291
*/
292292
readonly usePreviousParameters?: boolean;
293+
294+
readonly trust?: string[];
295+
296+
readonly untrust?: string[];
293297
}
294298

295299
export interface CdkGarbageCollectionCommandOptions {
@@ -445,6 +449,13 @@ export class TestFixture extends ShellHelper {
445449
args.push('--template', options.bootstrapTemplate);
446450
}
447451

452+
if (options.trust != null) {
453+
args.push('--trust', options.trust.join(','));
454+
}
455+
if (options.untrust != null) {
456+
args.push('--untrust', options.untrust.join(','));
457+
}
458+
448459
return this.cdk(args, {
449460
...options.cliOptions,
450461
modEnv: {

packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/bootstrapping.integtest.ts

+25
Original file line numberDiff line numberDiff line change
@@ -491,3 +491,28 @@ integTest('create ECR with tag IMMUTABILITY to set on', withoutBootstrap(async (
491491
expect(ecrResponse.repositories?.[0].imageTagMutability).toEqual('IMMUTABLE');
492492
}));
493493

494+
integTest('can remove trusted account', withoutBootstrap(async (fixture) => {
495+
const bootstrapStackName = fixture.bootstrapStackName;
496+
497+
await fixture.cdkBootstrapModern({
498+
verbose: false,
499+
toolkitStackName: bootstrapStackName,
500+
cfnExecutionPolicy: 'arn:aws:iam::aws:policy/AdministratorAccess',
501+
trust: ['599757620138', '730170552321'],
502+
});
503+
504+
await fixture.cdkBootstrapModern({
505+
verbose: true,
506+
toolkitStackName: bootstrapStackName,
507+
cfnExecutionPolicy: ' arn:aws:iam::aws:policy/AdministratorAccess',
508+
untrust: ['730170552321'],
509+
});
510+
511+
const response = await fixture.aws.cloudFormation.send(
512+
new DescribeStacksCommand({ StackName: bootstrapStackName }),
513+
);
514+
515+
const trustedAccounts = response.Stacks?.[0].Parameters?.find(p => p.ParameterKey === 'TrustedAccounts')?.ParameterValue;
516+
expect(trustedAccounts).toEqual('599757620138');
517+
}));
518+

packages/aws-cdk/lib/api/bootstrap/bootstrap-environment.ts

+20-3
Original file line numberDiff line numberDiff line change
@@ -102,11 +102,24 @@ export class Bootstrapper {
102102
// Ideally we'd do this inside the template, but the `Rules` section of CFN
103103
// templates doesn't seem to be able to express the conditions that we need
104104
// (can't use Fn::Join or reference Conditions) so we do it here instead.
105-
const trustedAccounts = params.trustedAccounts ?? splitCfnArray(current.parameters.TrustedAccounts);
105+
const allTrusted = new Set([
106+
...params.trustedAccounts ?? [],
107+
...params.trustedAccountsForLookup ?? [],
108+
]);
109+
const invalid = intersection(allTrusted, new Set(params.untrustedAccounts));
110+
if (invalid.size > 0) {
111+
throw new ToolkitError(`Accounts cannot be both trusted and untrusted. Found: ${[...invalid].join(',')}`);
112+
}
113+
114+
const removeUntrusted = (accounts: string[]) =>
115+
accounts.filter(acc => !params.untrustedAccounts?.map(String).includes(String(acc)));
116+
117+
const trustedAccounts = removeUntrusted(params.trustedAccounts ?? splitCfnArray(current.parameters.TrustedAccounts));
106118
info(`Trusted accounts for deployment: ${trustedAccounts.length > 0 ? trustedAccounts.join(', ') : '(none)'}`);
107119

108-
const trustedAccountsForLookup =
109-
params.trustedAccountsForLookup ?? splitCfnArray(current.parameters.TrustedAccountsForLookup);
120+
const trustedAccountsForLookup = removeUntrusted(
121+
params.trustedAccountsForLookup ?? splitCfnArray(current.parameters.TrustedAccountsForLookup),
122+
);
110123
info(
111124
`Trusted accounts for lookup: ${trustedAccountsForLookup.length > 0 ? trustedAccountsForLookup.join(', ') : '(none)'}`,
112125
);
@@ -376,3 +389,7 @@ function splitCfnArray(xs: string | undefined): string[] {
376389
}
377390
return xs.split(',');
378391
}
392+
393+
function intersection<A>(xs: Set<A>, ys: Set<A>): Set<A> {
394+
return new Set<A>(Array.from(xs).filter(x => ys.has(x)));
395+
}

packages/aws-cdk/lib/api/bootstrap/bootstrap-props.ts

+8
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,14 @@ export interface BootstrappingParameters {
102102
*/
103103
readonly trustedAccountsForLookup?: string[];
104104

105+
/**
106+
* The list of AWS account IDs that should not be trusted by the bootstrapped environment.
107+
* If these accounts are already trusted, they will be removed on bootstrapping.
108+
*
109+
* @default - no account will be untrusted.
110+
*/
111+
readonly untrustedAccounts?: string[];
112+
105113
/**
106114
* The ARNs of the IAM managed policies that should be attached to the role performing CloudFormation deployments.
107115
* In most cases, this will be the AdministratorAccess policy.

packages/aws-cdk/lib/cli.ts

+1
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,7 @@ export async function exec(args: string[], synthesizer?: Synthesizer): Promise<n
279279
customPermissionsBoundary: argv.customPermissionsBoundary,
280280
trustedAccounts: arrayFromYargs(args.trust),
281281
trustedAccountsForLookup: arrayFromYargs(args.trustForLookup),
282+
untrustedAccounts: arrayFromYargs(args.untrust),
282283
cloudFormationExecutionPolicies: arrayFromYargs(args.cloudformationExecutionPolicies),
283284
},
284285
});

packages/aws-cdk/lib/config.ts

+1
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ export async function makeConfig(): Promise<CliConfig> {
8787
'execute': { type: 'boolean', desc: 'Whether to execute ChangeSet (--no-execute will NOT execute the ChangeSet)', default: true },
8888
'trust': { type: 'array', desc: 'The AWS account IDs that should be trusted to perform deployments into this environment (may be repeated, modern bootstrapping only)', default: [] },
8989
'trust-for-lookup': { type: 'array', desc: 'The AWS account IDs that should be trusted to look up values in this environment (may be repeated, modern bootstrapping only)', default: [] },
90+
'untrust': { type: 'array', desc: 'The AWS account IDs that should not be trusted by this environment (may be repeated, modern bootstrapping only)', default: [] },
9091
'cloudformation-execution-policies': { type: 'array', desc: 'The Managed Policy ARNs that should be attached to the role performing deployments into this environment (may be repeated, modern bootstrapping only)', default: [] },
9192
'force': { alias: 'f', type: 'boolean', desc: 'Always bootstrap even if it would downgrade template version', default: false },
9293
'termination-protection': { type: 'boolean', default: undefined, desc: 'Toggle CloudFormation termination protection on the bootstrap stacks' },

packages/aws-cdk/lib/convert-to-user-input.ts

+2
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ export function convertYargsToUserInput(args: any): UserInput {
6969
execute: args.execute,
7070
trust: args.trust,
7171
trustForLookup: args.trustForLookup,
72+
untrust: args.untrust,
7273
cloudformationExecutionPolicies: args.cloudformationExecutionPolicies,
7374
force: args.force,
7475
terminationProtection: args.terminationProtection,
@@ -309,6 +310,7 @@ export function convertConfigToUserInput(config: any): UserInput {
309310
execute: config.bootstrap?.execute,
310311
trust: config.bootstrap?.trust,
311312
trustForLookup: config.bootstrap?.trustForLookup,
313+
untrust: config.bootstrap?.untrust,
312314
cloudformationExecutionPolicies: config.bootstrap?.cloudformationExecutionPolicies,
313315
force: config.bootstrap?.force,
314316
terminationProtection: config.bootstrap?.terminationProtection,

packages/aws-cdk/lib/parse-command-line-arguments.ts

+7
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,13 @@ export function parseCommandLineArguments(args: Array<string>): any {
263263
nargs: 1,
264264
requiresArg: true,
265265
})
266+
.option('untrust', {
267+
default: [],
268+
type: 'array',
269+
desc: 'The AWS account IDs that should not be trusted by this environment (may be repeated, modern bootstrapping only)',
270+
nargs: 1,
271+
requiresArg: true,
272+
})
266273
.option('cloudformation-execution-policies', {
267274
default: [],
268275
type: 'array',

packages/aws-cdk/lib/user-input.ts

+7
Original file line numberDiff line numberDiff line change
@@ -464,6 +464,13 @@ export interface BootstrapOptions {
464464
*/
465465
readonly trustForLookup?: Array<string>;
466466

467+
/**
468+
* The AWS account IDs that should not be trusted by this environment (may be repeated, modern bootstrapping only)
469+
*
470+
* @default - []
471+
*/
472+
readonly untrust?: Array<string>;
473+
467474
/**
468475
* The Managed Policy ARNs that should be attached to the role performing deployments into this environment (may be repeated, modern bootstrapping only)
469476
*

packages/aws-cdk/test/api/bootstrap2.test.ts

+69
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,75 @@ describe('Bootstrapping v2', () => {
330330
// Did not throw
331331
});
332332

333+
test('removes trusted account when it is listed as untrusted', async () => {
334+
// GIVEN
335+
mockTheToolkitInfo({
336+
Parameters: [
337+
{
338+
ParameterKey: 'CloudFormationExecutionPolicies',
339+
ParameterValue: 'arn:aws:something',
340+
},
341+
{
342+
ParameterKey: 'TrustedAccounts',
343+
ParameterValue: '111111111111,222222222222',
344+
},
345+
],
346+
});
347+
348+
await bootstrapper.bootstrapEnvironment(env, sdk, {
349+
parameters: {
350+
untrustedAccounts: ['111111111111'],
351+
},
352+
});
353+
354+
expect(mockDeployStack).toHaveBeenCalledWith(
355+
expect.objectContaining({
356+
parameters: expect.objectContaining({
357+
TrustedAccounts: '222222222222',
358+
}),
359+
}),
360+
);
361+
});
362+
363+
test('removes trusted account for lookup when it is listed as untrusted', async () => {
364+
// GIVEN
365+
mockTheToolkitInfo({
366+
Parameters: [
367+
{
368+
ParameterKey: 'CloudFormationExecutionPolicies',
369+
ParameterValue: 'arn:aws:something',
370+
},
371+
{
372+
ParameterKey: 'TrustedAccountsForLookup',
373+
ParameterValue: '111111111111,222222222222',
374+
},
375+
],
376+
});
377+
378+
await bootstrapper.bootstrapEnvironment(env, sdk, {
379+
parameters: {
380+
untrustedAccounts: ['111111111111'],
381+
},
382+
});
383+
384+
expect(mockDeployStack).toHaveBeenCalledWith(
385+
expect.objectContaining({
386+
parameters: expect.objectContaining({
387+
TrustedAccountsForLookup: '222222222222',
388+
}),
389+
}),
390+
);
391+
});
392+
393+
test('do not allow accounts to be listed as both trusted and untrusted', async () => {
394+
await expect(bootstrapper.bootstrapEnvironment(env, sdk, {
395+
parameters: {
396+
trustedAccountsForLookup: ['123456789012'],
397+
untrustedAccounts: ['123456789012'],
398+
},
399+
})).rejects.toThrow('Accounts cannot be both trusted and untrusted. Found: 123456789012');
400+
});
401+
333402
test('Do not allow downgrading bootstrap stack version', async () => {
334403
// GIVEN
335404
mockTheToolkitInfo({

0 commit comments

Comments
 (0)