Skip to content

Commit

Permalink
feat(codepipeline): migrate to configurable KMS keys (cross-account w…
Browse files Browse the repository at this point in the history
…orkflow)

Use custom KMS keys, which are passed or generated by CDK
in order to encrypt artifacts stored in S3.

Customers would be able to pass own configuration of
artifacts store - including S3 Bucker and KMS Encryption Key.

Set of changes:
* add KMS key & alias (with statically generated name) to scaffold
  stack;
* reference scaffold KMS key in ArtifactStore resource;
* scaffold stack artifact names derive name from pipeline name
  (or other identifier) to prevent collision with other projects.

This change is mainly motivated to enable cross-account deployments.

The work should enable to construct stacks like described in reference architecture https://github.com/awslabs/aws-refarch-cross-account-pipeline

In such a case foreign role have to have access to stored artifacts
in S3 buckets. However in order to achieve this two things must be achieved
* foreign account (role) has to be whitelisted on artifacts' bucket policy; and
* foreign account (role) has to be whitelisted on KMS keys used to encrypt artifacts in above bucket (which is impossible to do with AWS managed one).

In order to enable above cross-region scaffold stacks will contain KMS keys. As KMS key
can't be directly reference in cross-region / cross-account the alias with known name
is generated, in similar way as it happens for S3 buckets.

Together with aws#1449 customers should be able
to create on their own roles, and import those in pipeline stack. When

Some parts of this workflow could be automated, however even without it users would
be enabled to create cross-account pipelines.

* Create stack for deployments in foreign account
   * create *jump* role which would be set on action (aws#1449) this role would mainly allow passing Cloud Formation deployment role;
  * grant *jump* role read / write (as required) permissions to S3 buckets and KMS keys (right now whole foreign account is whitelisted)
  * create deployment role;
* In pipeline account
  * import both roles (require role ARNs to be known)
  * set imported *jump* role on Cloud Formation actions;
  * set imported deployment role as Cloud Formation role
  • Loading branch information
Rado Smogura committed Jan 12, 2019
1 parent dac9bfa commit 5b4c5c7
Show file tree
Hide file tree
Showing 8 changed files with 542 additions and 79 deletions.
23 changes: 23 additions & 0 deletions packages/@aws-cdk/aws-codepipeline-api/lib/action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,13 @@ export interface ActionProps extends CommonActionProps, CommonActionConstructPro
*/
region?: string;

/**
* The account this Action resides in.
*
* @default the Action resides in the same account as the Pipeline
*/
account?: string;

artifactBounds: ActionArtifactBounds;
configuration?: any;
version?: string;
Expand Down Expand Up @@ -197,6 +204,22 @@ export abstract class Action extends cdk.Construct {
*/
public readonly region?: string;

/**
* The AWS account the given Action resides in.
*
* Note that a cross-account Pipeline requires:
* * replication buckets to function correctly,
* * KMS keys used to encrypt artifacts;
* * properly set IAM roles for pipeline account and roles in target account.
*
* You can provide their names with the {@link PipelineProps#crossRegionReplicationBuckets} property.
* If you don't, the CodePipeline Construct will create new Stacks in your CDK app containing those buckets,
* that you will need to `cdk deploy` before deploying the main, Pipeline-containing Stack.
*
* @default the Action resides in the same region as the Pipeline
*/
public readonly account?: string;

/**
* The action's configuration. These are key-value pairs that specify input values for an action.
* For more information, see the AWS CodePipeline User Guide.
Expand Down
17 changes: 17 additions & 0 deletions packages/@aws-cdk/aws-codepipeline/lib/artifacts-store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/**
* Represents configuration of artifact store used for cross-region and cross account replication of deployment artifacts.
*
* Artifacts store is a set of AWS artifacts (like buckets and KMS keys) which are used by pipeline to store
* input and output to and from actions.
*/
export interface ArtifactsStoreProps {
/**
* The name of the S3 Bucket used for replicating the Pipeline's artifacts into the region.
*/
replicationBucketName: string;

/**
* Encryption key used to encrypt artifacts, it can represent key ARN or it can be an alias to key.
*/
replicationKeyArn?: string;
}
145 changes: 145 additions & 0 deletions packages/@aws-cdk/aws-codepipeline/lib/cross-account-scaffold-stack.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import cdk = require('@aws-cdk/cdk');
import iam = require('@aws-cdk/aws-iam');
import {App} from "@aws-cdk/cdk/lib/app";

// PENDING CLASS
// Actually customers may want to generate as much roles as possible
// CDK primarily should support setting those permission.
export interface CrossAccountStackProps {
/**
* Name of cloud formation role used to deploy and execute stack.
*/
cfDeployerRoleName: string;

/**
* Name of pipeline role used to execute stack and cloud formation.
*/
actionRoleName:string;

/**
* Id of target AWS account. Required to synthesize proper roles ARNs.
*/
account:string;
}

/**
* Represents stack on target account required to preform
* cross account deployments.
*
* This stack is typically auto generated, to make creation of
* stack easier. However it's possible not to deploy this stack,
* and configure all roles manually.
*
* The requirements for stacks are as follow:
* * in target there account there must exists cloud formation deployer
* role, which will be used to execute deployment, this role must have
* to have all required permissions, including access to artifacts buckets
* (main and / or cross region), KMS keys - those two access requires
* setting **resource** policy for account and / or role.
* * pipeline role - used by pipeline and allowing passing and assuming
* cloud formation role
*/
export class CrossAccountScaffoldStack extends cdk.Stack {
/**
* Role used to execute cloud formation.
*/
public readonly deployerRole: iam.Role;

/**
* Pipeline role used to update, build and execute stage and actions.
*/
public readonly actionRole: iam.Role;

/**
* Synthesized ARN of cloud formation deployer role used to deploy and execute stack.
*/
public readonly deployerRoleArn:string;

/**
* Synthesized ARN of of pipeline role used to update, build and execute stage and actions.
*/
public readonly actionRoleArn:string;

/**
* Id of target AWS account. Required to synthesize proper role ARNs.
*/
public readonly account:string;

/**
* Generates deployer roles used by for performing cloud formation deploy.
*
* @param props the properties used to bootsrap stack
*/
protected generateDeployerRole(props:CrossAccountStackProps): iam.Role {
return new iam.Role(this, 'CloudFormationRole', {
roleName: props.cfDeployerRoleName,
assumedBy: new iam.ServicePrincipal("cloudformation.amazonaws.com")
});
}

/**
* Generates pipeline roles used to execute deployment.
*
* @param props the properties used to bootsrap stack
*/
protected generateActionRole(props:CrossAccountStackProps): iam.Role {
return new iam.Role(this, 'PipelineRole', {
roleName: props.actionRoleName,
assumedBy: new iam.ServicePrincipal("*") // If we could specify here the pipeline role
});
}

constructor(parent: App, props:CrossAccountStackProps) {
super(parent, CrossAccountScaffoldStack.generateStackName(props), {
env: {
account: props.account
}
});

this.account = props.account;

this.deployerRole = this.generateDeployerRole(props);
this.actionRole = this.generateActionRole(props);

this.deployerRoleArn = CrossAccountScaffoldStack.synthesizeRoleArn(props.account, props.cfDeployerRoleName);
this.actionRoleArn = CrossAccountScaffoldStack.synthesizeRoleArn(props.account, props.actionRoleName);

// Policy base ond aws cross account refarch
this.actionRole.addToPolicy(new iam.PolicyStatement()
.addActions("cloudformation:*", "iam:PassRole")
.addAllResources()
);

// If possible S3 should be narrowed, same like in ref-arch, however good for now
// S3 bucket name is something we have in hand from artifacts-store, so it
// should be possible
this.actionRole.addToPolicy(new iam.PolicyStatement()
.addActions("s3:*")
.addAllResources()
);

// KMS is bit problematic here, as it's UUID based, so we can't
// statically synthesize name here, all we can do is to give access
// to key alias (but it's don't include encrypt / decrypt),
// work around would be to tag kms keys and make conditional access based on KMS key tag
//
// In any way we can narrow required permissions do decryption and alias de-ref
// however good for now
// TODO Narrow those permissions to required for encryption and decryption, exclude admin ones
this.actionRole.addToPolicy(new iam.PolicyStatement()
.addActions("kms:*")
.addAllResources()
);
}

static generateStackName(props:CrossAccountStackProps):string {
return `account-${props.account}-stack`;
}

static synthesizeRoleArn(account:string, roleName:string):string {
// Can't use ARN utils as name must be string, as it can be shared to other
// constructs
return `arn:aws:iam::${account}:role/${roleName}`;
}
}

104 changes: 85 additions & 19 deletions packages/@aws-cdk/aws-codepipeline/lib/cross-region-scaffold-stack.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import iam = require('@aws-cdk/aws-iam');
import kms = require('@aws-cdk/aws-kms');
import s3 = require('@aws-cdk/aws-s3');
import cdk = require('@aws-cdk/cdk');
import crypto = require('crypto');

/**
* Construction properties for {@link CrossRegionScaffoldStack}.
Expand All @@ -17,6 +18,16 @@ export interface CrossRegionScaffoldStackProps {
* @example '012345678901'
*/
account: string;

/**
* The name of S3 bucket to use to store artifacts
*/
artifactsBucketName: string;

/**
* The name of KMS key alias used to encrypt artifacts.
*/
artifactsKeyAlias?: string
}

/**
Expand All @@ -26,7 +37,23 @@ export class CrossRegionScaffoldStack extends cdk.Stack {
/**
* The name of the S3 Bucket used for replicating the Pipeline's artifacts into the region.
*/
public readonly replicationBucketName: string;
public readonly bucketName: string;

/**
* Generated bucket to store artifacts in given region
*/
public readonly bucket: s3.Bucket;

/**
* Encryption key used to encrypt artifacts.
*/
public readonly artifactsKey?: kms.EncryptionKey;

/**
* Artifacts encryption key alias ARN. ARN is synthesized
* from account number, region, and alias name.
*/
public readonly artifactsKeyAliasArn?: string;

constructor(scope: cdk.App, props: CrossRegionScaffoldStackProps) {
super(scope, generateStackName(props), {
Expand All @@ -36,28 +63,67 @@ export class CrossRegionScaffoldStack extends cdk.Stack {
},
});

const replicationBucketName = generateUniqueName('cdk-cross-region-codepipeline-replication-bucket-',
props.region, props.account, false, 12);
if (props.artifactsKeyAlias) {
this.artifactsKey = new kms.EncryptionKey(this, `ArtifactsKey`, {
description: `Key used to encrypt artifacts generated with code pipeline`
});

// Add alias to stack
new kms.EncryptionKeyAlias(this, `ArtifactsKeyAlias`, {
key: this.artifactsKey,
alias: props.artifactsKeyAlias
});

this.artifactsKeyAliasArn = `arn:aws:kms:${this.env.region}:${this.env.account}:${props.artifactsKeyAlias}`;
}

new s3.Bucket(this, 'CrossRegionCodePipelineReplicationBucket', {
bucketName: replicationBucketName,
this.bucket = new s3.Bucket(this, `ArtifactsBucket`, {
bucketName: props.artifactsBucketName,
});
this.replicationBucketName = replicationBucketName;

// TODO Add retention policy
this.bucketName = props.artifactsBucketName;

// TODO Consider adding managed policies to reference access to resources using synthesized names
}
}

function generateStackName(props: CrossRegionScaffoldStackProps): string {
return `aws-cdk-codepipeline-cross-region-scaffolding-${props.region}`;
}
/**
* Adds permissions to resources declared by this stack to specified
* IAM role. This method modifies **only resource policy**, so updating
* IAM role may be required as well.
*
* @param roleArn the arn of role to allow access to stack
*/
public addFullAccessToAwsAccount(account: string): void {
this.addFullAccessToIamRole(`arn:aws:iam::${account}:root`);
}

function generateUniqueName(baseName: string, region: string, account: string,
toUpperCase: boolean, hashPartLen: number = 8): string {
const sha256 = crypto.createHash('sha256')
.update(baseName)
.update(region)
.update(account);
/**
* Adds permissions to resources declared by this stack to specified
* IAM role. This method modifies **only resource policy**, so updating
* IAM role may be required as well.
*
* @param roleArn the arn of role to allow access to stack
*/
public addFullAccessToIamRole(roleArn: string): void {
// TODO Right now wildcard access, but need to split to read and write and not admin
if (this.artifactsKey) {
this.artifactsKey.addToResourcePolicy(new iam.PolicyStatement()
.addAwsPrincipal(roleArn)
.addAction('kms:*')
.addAllResources());
}

const hash = sha256.digest('hex').slice(0, hashPartLen);
if (this.bucket) {
// TODO Too wide permissions?
this.bucket.addToResourcePolicy(new iam.PolicyStatement()
.addAwsPrincipal(roleArn)
.addAction('s3:*')
.addResources(this.bucket.bucketArn, this.bucket.bucketArn + "/*"));
}
}
}

return baseName + (toUpperCase ? hash.toUpperCase() : hash.toLowerCase());
function generateStackName(props: CrossRegionScaffoldStackProps): string {
return `aws-cdk-codepipeline-cross-region-scaffolding-${props.region}`;
}
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-codepipeline/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './artifacts-store';
export * from './cross-region-scaffold-stack';
export * from './github-source-action';
export * from './jenkins-actions';
Expand Down
Loading

0 comments on commit 5b4c5c7

Please sign in to comment.