Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
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
2 changes: 1 addition & 1 deletion packages/@aws-cdk/assert/lib/assertions/have-resource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export class HaveResourceAssertion extends JestFriendlyAssertion<StackInspector>
for (const logicalId of Object.keys(inspector.value.Resources || {})) {
const resource = inspector.value.Resources[logicalId];
if (resource.Type === this.resourceType) {
const propsToCheck = this.part === ResourcePart.Properties ? resource.Properties : resource;
const propsToCheck = this.part === ResourcePart.Properties ? (resource.Properties ?? {}) : resource;

// Pass inspection object as 2nd argument, initialize failure with default string,
// to maintain backwards compatibility with old predicate API.
Expand Down
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-codebuild/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export * from './cache';
export * from './build-spec';
export * from './file-location';
export * from './linux-gpu-build-image';
export * from './untrusted-code-boundary-policy';

// AWS::CodeBuild CloudFormation Resources:
export * from './codebuild.generated';
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import * as iam from '@aws-cdk/aws-iam';
import { Construct } from 'constructs';

/**
* Construction properties for UntrustedCodeBoundaryPolicy
*/
export interface UntrustedCodeBoundaryPolicyProps {
/**
* Additional statements to add to the default set of statements
*
* @default - No additional statements
*/
readonly additionalStatements?: iam.PolicyStatement[];
}

/**
* Permissions Boundary for a CodeBuild Project running untrusted code
*
* This class is a Policy, intended to be used as a Permissions Boundary
* for a CodeBuild project. It allows most of the actions necessary to run
* the CodeBuild project, but disallows reading from Parameter Store
* and Secrets Manager.
*
* Use this when your CodeBuild project is running untrusted code (for
* example, if you are using one to automatically build Pull Requests
* that anyone can submit), and you want to prevent your future self
* from accidentally exposing Secrets to this build.
*
* (The reason you might want to do this is because otherwise anyone
* who can submit a Pull Request to your project can write a script
* to email those secrets to themselves).
*
* @example
*
* iam.PermissionsBoundary.of(project).apply(new UntrustedCodeBoundaryPolicy(this, 'Boundary'));
*/
export class UntrustedCodeBoundaryPolicy extends iam.ManagedPolicy {
constructor(scope: Construct, id: string, props: UntrustedCodeBoundaryPolicyProps = {}) {
super(scope, id, {
description: 'Permissions Boundary Policy for CodeBuild Projects running untrusted code',
statements: [
new iam.PolicyStatement({
actions: [
// For logging
'logs:CreateLogGroup',
'logs:CreateLogStream',
'logs:PutLogEvents',

// For test reports
'codebuild:CreateReportGroup',
'codebuild:CreateReport',
'codebuild:UpdateReport',
'codebuild:BatchPutTestCases',
'codebuild:BatchPutCodeCoverages',

// For batch builds
'codebuild:StartBuild',
'codebuild:StopBuild',
'codebuild:RetryBuild',

// For pulling ECR images
'ecr:GetDownloadUrlForLayer',
'ecr:BatchGetImage',
'ecr:BatchCheckLayerAvailability',

// For running in a VPC
'ec2:CreateNetworkInterfacePermission',
'ec2:CreateNetworkInterface',
'ec2:DescribeNetworkInterfaces',
'ec2:DeleteNetworkInterface',
'ec2:DescribeSubnets',
'ec2:DescribeSecurityGroups',
'ec2:DescribeDhcpOptions',
'ec2:DescribeVpcs',

// NOTABLY MISSING:
Copy link
Contributor

Choose a reason for hiding this comment

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

Would it make sense to add a "forbid" statements for these?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not really. It does the same thing.

// - Reading secrets
// - Reading parameterstore
],
resources: ['*'],
}),
...props.additionalStatements ?? [],
],
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { expect, haveResourceLike, arrayWith } from '@aws-cdk/assert';
import * as iam from '@aws-cdk/aws-iam';
import * as cdk from '@aws-cdk/core';
import { Test } from 'nodeunit';
import * as codebuild from '../lib';

export = {
'can attach permissions boundary to Project'(test: Test) {
// GIVEN
const stack = new cdk.Stack();

// WHEN
const project = new codebuild.Project(stack, 'Project', {
source: codebuild.Source.gitHub({ owner: 'a', repo: 'b' }),
});
iam.PermissionsBoundary.of(project).apply(new codebuild.UntrustedCodeBoundaryPolicy(stack, 'Boundary'));

// THEN
expect(stack).to(haveResourceLike('AWS::IAM::Role', {
PermissionsBoundary: { Ref: 'BoundaryEA298153' },
}));

test.done();
},

'can add additional statements Boundary'(test: Test) {
// GIVEN
const stack = new cdk.Stack();

// WHEN
const project = new codebuild.Project(stack, 'Project', {
source: codebuild.Source.gitHub({ owner: 'a', repo: 'b' }),
});
iam.PermissionsBoundary.of(project).apply(new codebuild.UntrustedCodeBoundaryPolicy(stack, 'Boundary', {
additionalStatements: [
new iam.PolicyStatement({
actions: ['a:a'],
resources: ['b'],
}),
],
}));

// THEN
expect(stack).to(haveResourceLike('AWS::IAM::ManagedPolicy', {
PolicyDocument: {
Statement: arrayWith({
Effect: 'Allow',
Action: 'a:a',
Resource: 'b',
}),
},
}));

test.done();
},
};
33 changes: 33 additions & 0 deletions packages/@aws-cdk/aws-iam/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,39 @@ const newPolicy = new Policy(stack, 'MyNewPolicy', {
});
```

## Permissions Boundaries

[Permissions
Boundaries](https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_boundaries.html)
can be used as a mechanism to prevent privilege esclation by creating new
`Role`s. Permissions Boundaries are a Managed Policy, attached to Roles or
Users, that represent the *maximum* set of permissions they can have. The
effective set of permissions of a Role (or User) will be the intersection of
the Identity Policy and the Permissions Boundary attached to the Role (or
User). Permissions Boundaries are typically created by account
Administrators, and their use on newly created `Role`s will be enforced by
IAM policies.

It is possible to attach Permissions Boundaries to all Roles created in a construct
tree all at once:

```ts
// This imports an existing policy. You can also create a new one.
Copy link
Contributor

Choose a reason for hiding this comment

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

Don't we also want to show how to define a new permission boundary using the CDK?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The 99.9% use case is that a PB will have been created for you by an administrator. It only makes sense in very select cases to create one yourself. But sure.

const boundary = iam.ManagedPolicy.fromManagedPolicyArn(this, 'Boundary', 'arn:aws:iam::123456789012:policy/boundary');

// Directly apply the boundary to a Role you create
iam.PermissionsBoundary.of(role).apply(boundary);

// Apply the boundary to an Role that was implicitly created for you
iam.PermissionsBoundary.of(lambdaFunction).apply(boundary);

// Apply the boundary to all Roles in a stack
iam.PermissionsBoundary.of(stack).apply(boundary);

// Remove a Permissions Boundary that is inherited, for example from the Stack level
iam.PermissionsBoundary.of(customResource).clear();
```

## OpenID Connect Providers

OIDC identity providers are entities in IAM that describe an external identity
Expand Down
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-iam/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export * from './identity-base';
export * from './grant';
export * from './unknown-principal';
export * from './oidc-provider';
export * from './permissions-boundary';

// AWS::IAM CloudFormation Resources:
export * from './iam.generated';
53 changes: 53 additions & 0 deletions packages/@aws-cdk/aws-iam/lib/permissions-boundary.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { Node, IConstruct } from 'constructs';
import { CfnRole, CfnUser } from './iam.generated';
import { IManagedPolicy } from './managed-policy';

/**
* Modify the Permissions Boundaries of Users and Roles in a construct tree
*
* @example
*
* const policy = ManagedPolicy.fromAwsManagedPolicyName('ReadOnlyAccess');
* PermissionsBoundary.of(stack).apply(policy);
*/
export class PermissionsBoundary {
/**
* Access the Permissions Boundaries of a construct tree
*/
public static of(scope: IConstruct): PermissionsBoundary {
return new PermissionsBoundary(scope);
}

private constructor(private readonly scope: IConstruct) {
}

/**
* Apply the given policy as Permissions Boundary to all Roles in the scope
*
* Will override any Permissions Boundaries configured previously; in case
* a Permission Boundary is applied in multiple scopes, the Boundary applied
* closest to the Role wins.
*/
public apply(boundaryPolicy: IManagedPolicy) {
Node.of(this.scope).applyAspect({
visit(node: IConstruct) {
if (node instanceof CfnRole || node instanceof CfnUser) {
node.permissionsBoundary = boundaryPolicy.managedPolicyArn;
}
},
});
}

/**
* Remove previously applied Permissions Boundaries
*/
public clear() {
Node.of(this.scope).applyAspect({
visit(node: IConstruct) {
if (node instanceof CfnRole || node instanceof CfnUser) {
node.permissionsBoundary = undefined;
}
},
});
}
}
101 changes: 101 additions & 0 deletions packages/@aws-cdk/aws-iam/test/permissions-boundary.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { ABSENT } from '@aws-cdk/assert';
import '@aws-cdk/assert/jest';
import { App, Stack } from '@aws-cdk/core';
import * as iam from '../lib';

let app: App;
let stack: Stack;
beforeEach(() => {
app = new App();
stack = new Stack(app, 'Stack');
});

test('apply imported boundary to a role', () => {
// GIVEN
const role = new iam.Role(stack, 'Role', {
assumedBy: new iam.ServicePrincipal('service.amazonaws.com'),
});

// WHEN
iam.PermissionsBoundary.of(role).apply(iam.ManagedPolicy.fromAwsManagedPolicyName('ReadOnlyAccess'));

// THEN
expect(stack).toHaveResource('AWS::IAM::Role', {
PermissionsBoundary: {
'Fn::Join': ['', [
'arn:',
{ Ref: 'AWS::Partition' },
':iam::aws:policy/ReadOnlyAccess',
]],
},
});
});

test('apply imported boundary to a user', () => {
// GIVEN
const user = new iam.User(stack, 'User');

// WHEN
iam.PermissionsBoundary.of(user).apply(iam.ManagedPolicy.fromAwsManagedPolicyName('ReadOnlyAccess'));

// THEN
expect(stack).toHaveResource('AWS::IAM::User', {
PermissionsBoundary: {
'Fn::Join': ['', [
'arn:',
{ Ref: 'AWS::Partition' },
':iam::aws:policy/ReadOnlyAccess',
]],
},
});
});

test('apply newly created boundary to a role', () => {
// GIVEN
const role = new iam.Role(stack, 'Role', {
assumedBy: new iam.ServicePrincipal('service.amazonaws.com'),
});

// WHEN
iam.PermissionsBoundary.of(role).apply(new iam.ManagedPolicy(stack, 'Policy', {
statements: [
new iam.PolicyStatement({
actions: ['*'],
resources: ['*'],
}),
],
}));

// THEN
expect(stack).toHaveResource('AWS::IAM::Role', {
PermissionsBoundary: { Ref: 'Policy23B91518' },
});
});

test('unapply inherited boundary from a user: order 1', () => {
// GIVEN
const user = new iam.User(stack, 'User');

// WHEN
iam.PermissionsBoundary.of(stack).apply(iam.ManagedPolicy.fromAwsManagedPolicyName('ReadOnlyAccess'));
iam.PermissionsBoundary.of(user).clear();

// THEN
expect(stack).toHaveResource('AWS::IAM::User', {
PermissionsBoundary: ABSENT,
});
});

test('unapply inherited boundary from a user: order 2', () => {
// GIVEN
const user = new iam.User(stack, 'User');

// WHEN
iam.PermissionsBoundary.of(user).clear();
iam.PermissionsBoundary.of(stack).apply(iam.ManagedPolicy.fromAwsManagedPolicyName('ReadOnlyAccess'));

// THEN
expect(stack).toHaveResource('AWS::IAM::User', {
PermissionsBoundary: ABSENT,
});
});