Skip to content

Commit 6224b6d

Browse files
authored
feat: bootstrap arguments for permissions boundary (#22792)
#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*
1 parent 0bfce89 commit 6224b6d

File tree

10 files changed

+340
-14
lines changed

10 files changed

+340
-14
lines changed

packages/aws-cdk/README.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -568,8 +568,9 @@ $ cdk bootstrap --app='node bin/main.js' foo bar
568568
By default, bootstrap stack will be protected from stack termination. This can be disabled using
569569
`--termination-protection` argument.
570570

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

575576
```console
@@ -582,6 +583,13 @@ $ cdk bootstrap --show-template > bootstrap-template.yaml
582583
$ cdk bootstrap --template bootstrap-template.yaml
583584
```
584585

586+
Out of the box customization options are also available as arguments. To use a permissions boundary:
587+
588+
- `--example-permissions-boundary` indicates the example permissions boundary, supplied by CDK
589+
- `--custom-permissions-boundary` specifies, by name a predefined, customer maintained, boundary
590+
591+
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.
592+
585593
### `cdk doctor`
586594

587595
Inspect the current command-line environment and configurations, and collect information that can be useful for

packages/aws-cdk/lib/api/aws-auth/sdk.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { Account } from './sdk-provider';
99
// We need to map regions to domain suffixes, and the SDK already has a function to do this.
1010
// It's not part of the public API, but it's also unlikely to go away.
1111
//
12-
// Reuse that function, and add a safety check so we don't accidentally break if they ever
12+
// Reuse that function, and add a safety check, so we don't accidentally break if they ever
1313
// refactor that away.
1414

1515
/* eslint-disable @typescript-eslint/no-require-imports */
@@ -53,6 +53,7 @@ export interface ISDK {
5353
lambda(): AWS.Lambda;
5454
cloudFormation(): AWS.CloudFormation;
5555
ec2(): AWS.EC2;
56+
iam(): AWS.IAM;
5657
ssm(): AWS.SSM;
5758
s3(): AWS.S3;
5859
route53(): AWS.Route53;
@@ -163,6 +164,10 @@ export class SDK implements ISDK {
163164
return this.wrapServiceErrorHandling(new AWS.EC2(this.config));
164165
}
165166

167+
public iam(): AWS.IAM {
168+
return this.wrapServiceErrorHandling(new AWS.IAM(this.config));
169+
}
170+
166171
public ssm(): AWS.SSM {
167172
return this.wrapServiceErrorHandling(new AWS.SSM(this.config));
168173
}
@@ -415,4 +420,4 @@ function allChainedExceptionMessages(e: Error | undefined) {
415420
*/
416421
export function isUnrecoverableAwsError(e: Error) {
417422
return (e as any).code === 'ExpiredToken';
418-
}
423+
}

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

Lines changed: 137 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import * as cxapi from '@aws-cdk/cx-api';
44
import { warning } from '../../logging';
55
import { loadStructuredFile, serializeStructure } from '../../serialize';
66
import { rootDir } from '../../util/directories';
7-
import { SdkProvider } from '../aws-auth';
7+
import { ISDK, Mode, SdkProvider } from '../aws-auth';
88
import { DeployStackResult } from '../deploy-stack';
99
import { BootstrapEnvironmentOptions, BootstrappingParameters } from './bootstrap-props';
1010
import { BootstrapStack, bootstrapVersionFromTemplate } from './deploy-bootstrap';
@@ -79,6 +79,7 @@ export class Bootstrapper {
7979
const bootstrapTemplate = await this.loadTemplate();
8080

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

8384
if (params.createCustomerMasterKey !== undefined && params.kmsKeyId) {
8485
throw new Error('You cannot pass \'--bootstrap-kms-key-id\' and \'--bootstrap-customer-key\' together. Specify one or the other');
@@ -102,7 +103,7 @@ export class Bootstrapper {
102103
if (trustedAccounts.length === 0 && cloudFormationExecutionPolicies.length === 0) {
103104
// For self-trust it's okay to default to AdministratorAccess, and it improves the usability of bootstrapping a lot.
104105
//
105-
// We don't actually make the implicity policy a physical parameter. The template will infer it instead,
106+
// We don't actually make the implicitly policy a physical parameter. The template will infer it instead,
106107
// we simply do the UI advertising that behavior here.
107108
//
108109
// If we DID make it an explicit parameter, we wouldn't be able to tell the difference between whether
@@ -113,7 +114,7 @@ export class Bootstrapper {
113114
//
114115
// Would leave AdministratorAccess policies with a trust relationship, without the user explicitly
115116
// approving the trust policy.
116-
const implicitPolicy = `arn:${await current.partition()}:iam::aws:policy/AdministratorAccess`;
117+
const implicitPolicy = `arn:${partition}:iam::aws:policy/AdministratorAccess`;
117118
warning(`Using default execution policy of '${implicitPolicy}'. Pass '--cloudformation-execution-policies' to customize.`);
118119
} else if (cloudFormationExecutionPolicies.length === 0) {
119120
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>\'.');
@@ -130,9 +131,25 @@ export class Bootstrapper {
130131
// * '-' if this is the first time we're deploying this stack (or upgrading from old to new bootstrap)
131132
const currentKmsKeyId = current.parameters.FileAssetsBucketKmsKeyId;
132133
const kmsKeyId = params.kmsKeyId ??
133-
(params.createCustomerMasterKey === true ? CREATE_NEW_KEY :
134-
params.createCustomerMasterKey === false || currentKmsKeyId === undefined ? USE_AWS_MANAGED_KEY :
135-
undefined);
134+
(params.createCustomerMasterKey === true ? CREATE_NEW_KEY :
135+
params.createCustomerMasterKey === false || currentKmsKeyId === undefined ? USE_AWS_MANAGED_KEY : undefined);
136+
137+
/* A permissions boundary can be provided via:
138+
* - the flag indicating the example one should be used
139+
* - the name indicating the custom permissions boundary to be used
140+
* Re-bootstrapping will NOT be blocked by either tightening or relaxing the permissions' boundary.
141+
*/
142+
const currentPermissionsBoundary = current.parameters.InputPermissionsBoundary;
143+
const inputPolicyName = params.examplePermissionsBoundary ? CDK_BOOTSTRAP_PERMISSIONS_BOUNDARY : params.customPermissionsBoundary;
144+
let policyName;
145+
if (inputPolicyName) {
146+
// If the example policy is not already in place, it must be created.
147+
const sdk = (await sdkProvider.forEnvironment(environment, Mode.ForWriting)).sdk;
148+
policyName = await this.getPolicyName(environment, sdk, inputPolicyName, partition, params);
149+
}
150+
if (currentPermissionsBoundary !== policyName) {
151+
warning(`Switching from ${currentPermissionsBoundary} to ${policyName} as permissions boundary`);
152+
}
136153

137154
return current.update(
138155
bootstrapTemplate,
@@ -145,12 +162,121 @@ export class Bootstrapper {
145162
CloudFormationExecutionPolicies: cloudFormationExecutionPolicies.join(','),
146163
Qualifier: params.qualifier,
147164
PublicAccessBlockConfiguration: params.publicAccessBlockConfiguration || params.publicAccessBlockConfiguration === undefined ? 'true' : 'false',
165+
InputPermissionsBoundary: policyName,
148166
}, {
149167
...options,
150168
terminationProtection: options.terminationProtection ?? current.terminationProtection,
151169
});
152170
}
153171

172+
private async getPolicyName(
173+
environment: cxapi.Environment,
174+
sdk: ISDK,
175+
permissionsBoundary: string,
176+
partition: string,
177+
params: BootstrappingParameters): Promise<string> {
178+
179+
if (permissionsBoundary !== CDK_BOOTSTRAP_PERMISSIONS_BOUNDARY) {
180+
this.validatePolicyName(permissionsBoundary);
181+
return Promise.resolve(permissionsBoundary);
182+
}
183+
// if no Qualifier is supplied, resort to the default one
184+
const arn = await this.getExamplePermissionsBoundary(params.qualifier ?? 'hnb659fds', partition, environment.account, sdk);
185+
const policyName = arn.split('/').pop();
186+
if (!policyName) {
187+
throw new Error('Could not retrieve the example permission boundary!');
188+
}
189+
return Promise.resolve(policyName);
190+
}
191+
192+
private async getExamplePermissionsBoundary(qualifier: string, partition: string, account: string, sdk: ISDK): Promise<string> {
193+
const iam = sdk.iam();
194+
195+
let policyName = `cdk-${qualifier}-permissions-boundary`;
196+
const arn = `arn:${partition}:iam::${account}:policy/${policyName}`;
197+
198+
try {
199+
let getPolicyResp = await iam.getPolicy({ PolicyArn: arn }).promise();
200+
if (getPolicyResp.Policy) {
201+
return arn;
202+
}
203+
} catch (e) {
204+
// https://docs.aws.amazon.com/IAM/latest/APIReference/API_GetPolicy.html#API_GetPolicy_Errors
205+
if (e.name === 'NoSuchEntity') {
206+
//noop, proceed with creating the policy
207+
} else {
208+
throw e;
209+
}
210+
}
211+
212+
const policyDoc = {
213+
Version: '2012-10-17',
214+
Statement: [
215+
{
216+
Action: ['*'],
217+
Resource: '*',
218+
Effect: 'Allow',
219+
Sid: 'ExplicitAllowAll',
220+
},
221+
{
222+
Condition: {
223+
StringEquals: {
224+
'iam:PermissionsBoundary': `arn:${partition}:iam::${account}:policy/cdk-${qualifier}-permissions-boundary`,
225+
},
226+
},
227+
Action: [
228+
'iam:CreateUser',
229+
'iam:CreateRole',
230+
'iam:PutRolePermissionsBoundary',
231+
'iam:PutUserPermissionsBoundary',
232+
],
233+
Resource: '*',
234+
Effect: 'Allow',
235+
Sid: 'DenyAccessIfRequiredPermBoundaryIsNotBeingApplied',
236+
},
237+
{
238+
Action: [
239+
'iam:CreatePolicyVersion',
240+
'iam:DeletePolicy',
241+
'iam:DeletePolicyVersion',
242+
'iam:SetDefaultPolicyVersion',
243+
],
244+
Resource: `arn:${partition}:iam::${account}:policy/cdk-${qualifier}-permissions-boundary`,
245+
Effect: 'Deny',
246+
Sid: 'DenyPermBoundaryIAMPolicyAlteration',
247+
},
248+
{
249+
Action: [
250+
'iam:DeleteUserPermissionsBoundary',
251+
'iam:DeleteRolePermissionsBoundary',
252+
],
253+
Resource: '*',
254+
Effect: 'Deny',
255+
Sid: 'DenyRemovalOfPermBoundaryFromAnyUserOrRole',
256+
},
257+
],
258+
};
259+
const request = {
260+
PolicyName: policyName,
261+
PolicyDocument: JSON.stringify(policyDoc),
262+
};
263+
const createPolicyResponse = await iam.createPolicy(request).promise();
264+
if (createPolicyResponse.Policy?.Arn) {
265+
return createPolicyResponse.Policy.Arn;
266+
} else {
267+
throw new Error(`Could not retrieve the example permission boundary ${arn}!`);
268+
}
269+
}
270+
271+
private validatePolicyName(permissionsBoundary: string) {
272+
// https://docs.aws.amazon.com/IAM/latest/APIReference/API_CreatePolicy.html
273+
const regexp: RegExp = /[\w+=,.@-]+/;
274+
const matches = regexp.exec(permissionsBoundary);
275+
if (!(matches && matches.length === 1 && matches[0] === permissionsBoundary)) {
276+
throw new Error(`The permissions boundary name ${permissionsBoundary} does not match the IAM conventions.`);
277+
}
278+
}
279+
154280
private async customBootstrap(
155281
environment: cxapi.Environment,
156282
sdkProvider: SdkProvider,
@@ -179,14 +305,18 @@ export class Bootstrapper {
179305
}
180306

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

186312
/**
187313
* Magic parameter value that will cause the bootstrap-template.yml to create a CMK
188314
*/
189315
const CREATE_NEW_KEY = '';
316+
/**
317+
* Parameter value indicating the use of the default, CDK provided permissions boundary for bootstrap-template.yml
318+
*/
319+
const CDK_BOOTSTRAP_PERMISSIONS_BOUNDARY = 'CDK_BOOTSTRAP_PERMISSIONS_BOUNDARY';
190320

191321
/**
192322
* Split an array-like CloudFormation parameter on ,

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

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,4 +101,18 @@ export interface BootstrappingParameters {
101101
*/
102102
readonly publicAccessBlockConfiguration?: boolean;
103103

104-
}
104+
/**
105+
* Flag for using the default permissions boundary for bootstrapping
106+
*
107+
* @default - No value, optional argument
108+
*/
109+
readonly examplePermissionsBoundary?: boolean;
110+
111+
/**
112+
* Name for the customer's custom permissions boundary for bootstrapping
113+
*
114+
* @default - No value, optional argument
115+
*/
116+
readonly customPermissionsBoundary?: string;
117+
118+
}

packages/aws-cdk/lib/api/bootstrap/bootstrap-template.yaml

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,14 @@ Parameters:
4242
Default: 'true'
4343
Type: 'String'
4444
AllowedValues: ['true', 'false']
45+
InputPermissionsBoundary:
46+
Description: Whether or not to use either the CDK supplied or custom permissions boundary
47+
Default: ''
48+
Type: 'String'
49+
UseExamplePermissionsBoundary:
50+
Default: 'false'
51+
AllowedValues: [ 'true', 'false' ]
52+
Type: String
4553
Conditions:
4654
HasTrustedAccounts:
4755
Fn::Not:
@@ -77,6 +85,15 @@ Conditions:
7785
Fn::Equals:
7886
- 'AWS_MANAGED_KEY'
7987
- Ref: FileAssetsBucketKmsKeyId
88+
ShouldCreatePermissionsBoundary:
89+
Fn::Equals:
90+
- 'true'
91+
- Ref: UseExamplePermissionsBoundary
92+
PermissionsBoundarySet:
93+
Fn::Not:
94+
- Fn::Equals:
95+
- ''
96+
- Ref: InputPermissionsBoundary
8097
HasCustomContainerAssetsRepositoryName:
8198
Fn::Not:
8299
- Fn::Equals:
@@ -500,6 +517,66 @@ Resources:
500517
- - Fn::Sub: "arn:${AWS::Partition}:iam::aws:policy/AdministratorAccess"
501518
RoleName:
502519
Fn::Sub: cdk-${Qualifier}-cfn-exec-role-${AWS::AccountId}-${AWS::Region}
520+
PermissionsBoundary:
521+
Fn::If:
522+
- PermissionsBoundarySet
523+
- Fn::Sub: 'arn:${AWS::Partition}:iam::${AWS::AccountId}:policy/${InputPermissionsBoundary}'
524+
- Ref: AWS::NoValue
525+
CdkBoostrapPermissionsBoundaryPolicy:
526+
# Edit the template prior to boostrap in order to have this example policy created
527+
Condition: ShouldCreatePermissionsBoundary
528+
Type: AWS::IAM::ManagedPolicy
529+
Properties:
530+
PolicyDocument:
531+
Statement:
532+
# If permission boundaries do not have an explicit `allow`, then the effect is `deny`
533+
- Sid: ExplicitAllowAll
534+
Action:
535+
- "*"
536+
Effect: Allow
537+
Resource: "*"
538+
# Default permissions to prevent privilege escalation
539+
- Sid: DenyAccessIfRequiredPermBoundaryIsNotBeingApplied
540+
Action:
541+
- iam:CreateUser
542+
- iam:CreateRole
543+
- iam:PutRolePermissionsBoundary
544+
- iam:PutUserPermissionsBoundary
545+
Condition:
546+
StringNotEquals:
547+
iam:PermissionsBoundary:
548+
Fn::Sub: arn:${AWS::Partition}:iam::${AWS::AccountId}:policy/cdk-${Qualifier}-permissions-boundary-${AWS::AccountId}-${AWS::Region}
549+
Effect: Deny
550+
Resource: "*"
551+
# Forbid the policy itself being edited
552+
- Sid: DenyPermBoundaryIAMPolicyAlteration
553+
Action:
554+
- iam:CreatePolicyVersion
555+
- iam:DeletePolicy
556+
- iam:DeletePolicyVersion
557+
- iam:SetDefaultPolicyVersion
558+
Effect: Deny
559+
Resource:
560+
Fn::Sub: arn:${AWS::Partition}:iam::${AWS::AccountId}:policy/cdk-${Qualifier}-permissions-boundary-${AWS::AccountId}-${AWS::Region}
561+
# Forbid removing the permissions boundary from any user or role that has it associated
562+
- Sid: DenyRemovalOfPermBoundaryFromAnyUserOrRole
563+
Action:
564+
- iam:DeleteUserPermissionsBoundary
565+
- iam:DeleteRolePermissionsBoundary
566+
Effect: Deny
567+
Resource: "*"
568+
# Add your specific organizational security policy here
569+
# Uncomment the example to deny access to AWS Config
570+
#- Sid: OrganizationalSecurityPolicy
571+
# Action:
572+
# - "config:*"
573+
# Effect: Deny
574+
# Resource: "*"
575+
Version: "2012-10-17"
576+
Description: "Bootstrap Permission Boundary"
577+
ManagedPolicyName:
578+
Fn::Sub: cdk-${Qualifier}-permissions-boundary-${AWS::AccountId}-${AWS::Region}
579+
Path: /
503580
# The SSM parameter is used in pipeline-deployed templates to verify the version
504581
# of the bootstrap resources.
505582
CdkBootstrapVersion:

0 commit comments

Comments
 (0)