Skip to content

Commit

Permalink
feat(cli): add --untrust option to bootstrap (#33091)
Browse files Browse the repository at this point in the history
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*
  • Loading branch information
otaviomacedo authored Jan 23, 2025
1 parent 344d916 commit 4713bdd
Show file tree
Hide file tree
Showing 10 changed files with 151 additions and 3 deletions.
11 changes: 11 additions & 0 deletions packages/@aws-cdk-testing/cli-integ/lib/with-cdk-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,10 @@ export interface CdkModernBootstrapCommandOptions extends CommonCdkBootstrapComm
* @default undefined
*/
readonly usePreviousParameters?: boolean;

readonly trust?: string[];

readonly untrust?: string[];
}

export interface CdkGarbageCollectionCommandOptions {
Expand Down Expand Up @@ -445,6 +449,13 @@ export class TestFixture extends ShellHelper {
args.push('--template', options.bootstrapTemplate);
}

if (options.trust != null) {
args.push('--trust', options.trust.join(','));
}
if (options.untrust != null) {
args.push('--untrust', options.untrust.join(','));
}

return this.cdk(args, {
...options.cliOptions,
modEnv: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -491,3 +491,28 @@ integTest('create ECR with tag IMMUTABILITY to set on', withoutBootstrap(async (
expect(ecrResponse.repositories?.[0].imageTagMutability).toEqual('IMMUTABLE');
}));

integTest('can remove trusted account', withoutBootstrap(async (fixture) => {
const bootstrapStackName = fixture.bootstrapStackName;

await fixture.cdkBootstrapModern({
verbose: false,
toolkitStackName: bootstrapStackName,
cfnExecutionPolicy: 'arn:aws:iam::aws:policy/AdministratorAccess',
trust: ['599757620138', '730170552321'],
});

await fixture.cdkBootstrapModern({
verbose: true,
toolkitStackName: bootstrapStackName,
cfnExecutionPolicy: ' arn:aws:iam::aws:policy/AdministratorAccess',
untrust: ['730170552321'],
});

const response = await fixture.aws.cloudFormation.send(
new DescribeStacksCommand({ StackName: bootstrapStackName }),
);

const trustedAccounts = response.Stacks?.[0].Parameters?.find(p => p.ParameterKey === 'TrustedAccounts')?.ParameterValue;
expect(trustedAccounts).toEqual('599757620138');
}));

23 changes: 20 additions & 3 deletions packages/aws-cdk/lib/api/bootstrap/bootstrap-environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,11 +102,24 @@ export class Bootstrapper {
// Ideally we'd do this inside the template, but the `Rules` section of CFN
// templates doesn't seem to be able to express the conditions that we need
// (can't use Fn::Join or reference Conditions) so we do it here instead.
const trustedAccounts = params.trustedAccounts ?? splitCfnArray(current.parameters.TrustedAccounts);
const allTrusted = new Set([
...params.trustedAccounts ?? [],
...params.trustedAccountsForLookup ?? [],
]);
const invalid = intersection(allTrusted, new Set(params.untrustedAccounts));
if (invalid.size > 0) {
throw new ToolkitError(`Accounts cannot be both trusted and untrusted. Found: ${[...invalid].join(',')}`);
}

const removeUntrusted = (accounts: string[]) =>
accounts.filter(acc => !params.untrustedAccounts?.map(String).includes(String(acc)));

const trustedAccounts = removeUntrusted(params.trustedAccounts ?? splitCfnArray(current.parameters.TrustedAccounts));
info(`Trusted accounts for deployment: ${trustedAccounts.length > 0 ? trustedAccounts.join(', ') : '(none)'}`);

const trustedAccountsForLookup =
params.trustedAccountsForLookup ?? splitCfnArray(current.parameters.TrustedAccountsForLookup);
const trustedAccountsForLookup = removeUntrusted(
params.trustedAccountsForLookup ?? splitCfnArray(current.parameters.TrustedAccountsForLookup),
);
info(
`Trusted accounts for lookup: ${trustedAccountsForLookup.length > 0 ? trustedAccountsForLookup.join(', ') : '(none)'}`,
);
Expand Down Expand Up @@ -376,3 +389,7 @@ function splitCfnArray(xs: string | undefined): string[] {
}
return xs.split(',');
}

function intersection<A>(xs: Set<A>, ys: Set<A>): Set<A> {
return new Set<A>(Array.from(xs).filter(x => ys.has(x)));
}
8 changes: 8 additions & 0 deletions packages/aws-cdk/lib/api/bootstrap/bootstrap-props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,14 @@ export interface BootstrappingParameters {
*/
readonly trustedAccountsForLookup?: string[];

/**
* The list of AWS account IDs that should not be trusted by the bootstrapped environment.
* If these accounts are already trusted, they will be removed on bootstrapping.
*
* @default - no account will be untrusted.
*/
readonly untrustedAccounts?: string[];

/**
* The ARNs of the IAM managed policies that should be attached to the role performing CloudFormation deployments.
* In most cases, this will be the AdministratorAccess policy.
Expand Down
1 change: 1 addition & 0 deletions packages/aws-cdk/lib/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,7 @@ export async function exec(args: string[], synthesizer?: Synthesizer): Promise<n
customPermissionsBoundary: argv.customPermissionsBoundary,
trustedAccounts: arrayFromYargs(args.trust),
trustedAccountsForLookup: arrayFromYargs(args.trustForLookup),
untrustedAccounts: arrayFromYargs(args.untrust),
cloudFormationExecutionPolicies: arrayFromYargs(args.cloudformationExecutionPolicies),
},
});
Expand Down
1 change: 1 addition & 0 deletions packages/aws-cdk/lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ export async function makeConfig(): Promise<CliConfig> {
'execute': { type: 'boolean', desc: 'Whether to execute ChangeSet (--no-execute will NOT execute the ChangeSet)', default: true },
'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: [] },
'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: [] },
'untrust': { type: 'array', desc: 'The AWS account IDs that should not be trusted by this environment (may be repeated, modern bootstrapping only)', default: [] },
'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: [] },
'force': { alias: 'f', type: 'boolean', desc: 'Always bootstrap even if it would downgrade template version', default: false },
'termination-protection': { type: 'boolean', default: undefined, desc: 'Toggle CloudFormation termination protection on the bootstrap stacks' },
Expand Down
2 changes: 2 additions & 0 deletions packages/aws-cdk/lib/convert-to-user-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export function convertYargsToUserInput(args: any): UserInput {
execute: args.execute,
trust: args.trust,
trustForLookup: args.trustForLookup,
untrust: args.untrust,
cloudformationExecutionPolicies: args.cloudformationExecutionPolicies,
force: args.force,
terminationProtection: args.terminationProtection,
Expand Down Expand Up @@ -309,6 +310,7 @@ export function convertConfigToUserInput(config: any): UserInput {
execute: config.bootstrap?.execute,
trust: config.bootstrap?.trust,
trustForLookup: config.bootstrap?.trustForLookup,
untrust: config.bootstrap?.untrust,
cloudformationExecutionPolicies: config.bootstrap?.cloudformationExecutionPolicies,
force: config.bootstrap?.force,
terminationProtection: config.bootstrap?.terminationProtection,
Expand Down
7 changes: 7 additions & 0 deletions packages/aws-cdk/lib/parse-command-line-arguments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,13 @@ export function parseCommandLineArguments(args: Array<string>): any {
nargs: 1,
requiresArg: true,
})
.option('untrust', {
default: [],
type: 'array',
desc: 'The AWS account IDs that should not be trusted by this environment (may be repeated, modern bootstrapping only)',
nargs: 1,
requiresArg: true,
})
.option('cloudformation-execution-policies', {
default: [],
type: 'array',
Expand Down
7 changes: 7 additions & 0 deletions packages/aws-cdk/lib/user-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,13 @@ export interface BootstrapOptions {
*/
readonly trustForLookup?: Array<string>;

/**
* The AWS account IDs that should not be trusted by this environment (may be repeated, modern bootstrapping only)
*
* @default - []
*/
readonly untrust?: Array<string>;

/**
* The Managed Policy ARNs that should be attached to the role performing deployments into this environment (may be repeated, modern bootstrapping only)
*
Expand Down
69 changes: 69 additions & 0 deletions packages/aws-cdk/test/api/bootstrap2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,75 @@ describe('Bootstrapping v2', () => {
// Did not throw
});

test('removes trusted account when it is listed as untrusted', async () => {
// GIVEN
mockTheToolkitInfo({
Parameters: [
{
ParameterKey: 'CloudFormationExecutionPolicies',
ParameterValue: 'arn:aws:something',
},
{
ParameterKey: 'TrustedAccounts',
ParameterValue: '111111111111,222222222222',
},
],
});

await bootstrapper.bootstrapEnvironment(env, sdk, {
parameters: {
untrustedAccounts: ['111111111111'],
},
});

expect(mockDeployStack).toHaveBeenCalledWith(
expect.objectContaining({
parameters: expect.objectContaining({
TrustedAccounts: '222222222222',
}),
}),
);
});

test('removes trusted account for lookup when it is listed as untrusted', async () => {
// GIVEN
mockTheToolkitInfo({
Parameters: [
{
ParameterKey: 'CloudFormationExecutionPolicies',
ParameterValue: 'arn:aws:something',
},
{
ParameterKey: 'TrustedAccountsForLookup',
ParameterValue: '111111111111,222222222222',
},
],
});

await bootstrapper.bootstrapEnvironment(env, sdk, {
parameters: {
untrustedAccounts: ['111111111111'],
},
});

expect(mockDeployStack).toHaveBeenCalledWith(
expect.objectContaining({
parameters: expect.objectContaining({
TrustedAccountsForLookup: '222222222222',
}),
}),
);
});

test('do not allow accounts to be listed as both trusted and untrusted', async () => {
await expect(bootstrapper.bootstrapEnvironment(env, sdk, {
parameters: {
trustedAccountsForLookup: ['123456789012'],
untrustedAccounts: ['123456789012'],
},
})).rejects.toThrow('Accounts cannot be both trusted and untrusted. Found: 123456789012');
});

test('Do not allow downgrading bootstrap stack version', async () => {
// GIVEN
mockTheToolkitInfo({
Expand Down

0 comments on commit 4713bdd

Please sign in to comment.