-
Notifications
You must be signed in to change notification settings - Fork 4.4k
feat(iam): Permissions Boundaries #12777
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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: | ||
| // - 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(); | ||
| }, | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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. | ||
|
||
| 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 | ||
|
|
||
| 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; | ||
| } | ||
| }, | ||
| }); | ||
| } | ||
| } |
| 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, | ||
| }); | ||
| }); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.