Skip to content
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

fix(pipelines): Reduce template size by combining IAM roles and policies #9243

Merged
merged 3 commits into from
Jul 28, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,13 @@ export interface PublishAssetsActionProps {
* @default - Automatically generated
*/
readonly projectName?: string;

/**
* Role to use for CodePipeline and CodeBuild to build and publish the assets.
*
* @default - Automatically generated
*/
readonly role?: iam.IRole;
}

/**
Expand Down Expand Up @@ -87,6 +94,7 @@ export class PublishAssetsAction extends Construct implements codepipeline.IActi
}),
// Needed to perform Docker builds
environment: props.assetType === AssetType.DOCKER_IMAGE ? { privileged: true } : undefined,
role: props.role,
});

const rolePattern = props.assetType === AssetType.DOCKER_IMAGE
Expand All @@ -102,6 +110,7 @@ export class PublishAssetsAction extends Construct implements codepipeline.IActi
actionName: props.actionName,
project,
input: this.props.cloudAssemblyInput,
role: props.role,
});
}

Expand Down
19 changes: 16 additions & 3 deletions packages/@aws-cdk/pipelines/lib/pipeline.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as codepipeline from '@aws-cdk/aws-codepipeline';
import { App, CfnOutput, Construct, Stack, Stage } from '@aws-cdk/core';
import * as path from 'path';
import * as codepipeline from '@aws-cdk/aws-codepipeline';
import * as iam from '@aws-cdk/aws-iam';
import { App, CfnOutput, Construct, PhysicalName, Stack, Stage } from '@aws-cdk/core';
import { AssetType, DeployCdkStackAction, PublishAssetsAction, UpdatePipelineAction } from './actions';
import { appOf, assemblyBuilderOf } from './private/construct-internals';
import { AddStageOptions, AssetPublishingCommand, CdkStage, StackOutput } from './stage';
Expand Down Expand Up @@ -244,6 +245,7 @@ class AssetPublishing extends Construct {
private readonly myCxAsmRoot: string;

private readonly stage: codepipeline.IStage;
private assetRole?: iam.Role;
private _fileAssetCtr = 1;
private _dockerAssetCtr = 1;

Expand All @@ -267,6 +269,17 @@ class AssetPublishing extends Construct {
// FIXME: this is silly, we need the relative path here but no easy way to get it
const relativePath = path.relative(this.myCxAsmRoot, command.assetManifestPath);

// This role is used by both the CodePipeline build action and related CodeBuild project. Consolidating these two
// roles into one, and re-using across all assets, saves significant size of the final synthesized output.
// Modeled after the CodePipeline role and 'CodePipelineActionRole' roles.
// Late-binding here to prevent creating the role in cases where no asset actions are created.
if (!this.assetRole) {
this.assetRole = new iam.Role(this, 'Role', {
Copy link
Contributor

Choose a reason for hiding this comment

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

What is the eventual IAM policy that will be generated for this role?

We need to be careful here because this role is an important component of our security model so the policy must be least privilege.

The main risk is docker builds in which arbitrary 3rd party code can run.

There are a few things that we should ensure (ideally enforce through unit tests):

  1. Docker publishing roles must not be able to access the s3 assets bucket in order to mitigate the threat of injecting compromised templates that can deploy malicious resources through nested stacks.
  2. The role must be able to assume only the specific cross-account roles in the specified target environments in which it needs to publish into to mitigate the blast radius of a compromised publisher).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think this change breaks point 1 above; point 2 I believe is already broken, given the overly-permissive sts:AssumeRole statement in the pipeline asset policy (assuming arn:*:iam::*:role/*-file-publishing-role-*).

Here is a gist with the before/after of the relevant roles and policies: https://gist.github.com/njlynch/b3364ace98559757a1ca4a1a3aab2f6d

I've replaced my account ID with __$PIPELINEACCOUNT__ in the output, but otherwise it's exactly as it appears in the synthesized output. As you can see, there are no other account(s)/environment(s) explicitly referenced, despite this template being for a cross-account, multi-region pipeline.

Copy link
Contributor

Choose a reason for hiding this comment

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

We should raise an issue to fix this policy. The remote role ARNs should actually come from the user app and not be hard coded in the pipelines library. Users are allowed to specify any role ARNs they want for publishing.

I can't seem to find the s3:PutObject permissions in the referenced gist. Are these policies for publishing both docker & s3 assets? We should still separate these into two policies in order to ensure that docker publishers cannot upload files to the S3 asset store.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We should raise an issue to fix this policy.

Ok. I created #9271 to address this.

I can't seem to find the s3:PutObject permissions in the referenced gist.

s3:PutObject appears as an action on both the "PipelineRoleDefaultPolicy" (used by CodePipeline), the "PipelineBuildSynthCdkBuildProjectRoleDefaultPolicy" (used by CodeBuild as part of the build action), as well as in the policy attached to the *-file-publishing-role-* Role. I believe it's the latter that enables the actual asset publishing here.

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 roles being touched in this PR are not the ones that do the actual publishing; instead, they are the roles that enable the roles that do the actual publishing.

The first role/policy (PipelineAssetsFileAssetNCodePipelineActionRole* in the "before" example) is used by CodePipeline to start/stop the CodeBuild builds. This role is the one assumed by the CodePipeline CodeBuild action. Lumping these all together means there is a single role with the ability to start/stop the CodeBuild builds; I don't think there's a security risk here.

The second role is the one used by CodeBuild itself (PipelineAssetsFileAssetNCodePipelineActionRoleDefaultPolicy* in the "before" example). This role has permissions to create asset-named log groups, CodeBuild report groups, has S3 and KMS get permissions on the entire asset bucket to read and decrypt the manifest, and lastly has permissions to assume the bootstrapped file and image publishing roles. The only elements of this policy that are in any way asset-specific are the named log groups and report groups. So risks here would be a malicious build writing to the wrong logs, maybe?

The main risk is docker builds in which arbitrary 3rd party code can run.

There are a few things that we should ensure (ideally enforce through unit tests):

  1. Docker publishing roles must not be able to access the s3 assets bucket in order to mitigate the threat of injecting compromised templates that can deploy malicious resources through nested stacks.
  2. The role must be able to assume only the specific cross-account roles in the specified target environments in which it needs to publish into to mitigate the blast radius of a compromised publisher).

So the actual asset publishing is controlled by the PublishAssetsAction class, which creates a CodeBuild project that installs cdk-assets, and then executes a series of publish commands. For example:

cdk-assets --path "assembly-CdkpipelinesDemoPipelineStack-PreProdAccount1/CdkpipelinesDemoPipelineStackPreProdAccount1WebService1F433423.assets.json" --verbose publish "0a1ddcc82faef3c47aaafb9516057a34961187be09202edaf14bb4b28a6cb115:ACCOUNT1-eu-west-1"

This reads from the generated *.assets.json files, which have encoded within them the source asset name, destination bucket, object, region, and environment-specific (account & region) role ARN to assume to do the publishing. cdk-assets itself does the STS AssumeRole call, gets environment-specific credentials, and then does the publishing. This is where the above two restrictions are being enforced; a combination of the code that generates the assets.json files and cdk-asset executing it. I don't see a path here for combining the initial two roles causing a breakdown in that chain.

roleName: PhysicalName.GENERATE_IF_NEEDED,
assumedBy: new iam.CompositePrincipal(new iam.ServicePrincipal('codebuild.amazonaws.com'), new iam.AccountPrincipal(Stack.of(this).account)),
});
}

let action = this.publishers[command.assetId];
if (!action) {
// The asset ID would be a logical candidate for the construct path and project names, but if the asset
Expand All @@ -275,7 +288,6 @@ class AssetPublishing extends Construct {
//
// FIXME: The ultimate best solution is probably to generate a single Project per asset type
// and reuse that for all assets.

const id = command.assetType === AssetType.FILE ? `FileAsset${this._fileAssetCtr++}` : `DockerAsset${this._dockerAssetCtr++}`;

// NOTE: It's important that asset changes don't force a pipeline self-mutation.
Expand All @@ -286,6 +298,7 @@ class AssetPublishing extends Construct {
cloudAssemblyInput: this.props.cloudAssemblyInput,
cdkCliVersion: this.props.cdkCliVersion,
assetType: command.assetType,
role: this.assetRole,
});
this.stage.addAction(action);
}
Expand Down