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 633b7aa
Show file tree
Hide file tree
Showing 9 changed files with 672 additions and 98 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
192 changes: 192 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,192 @@
import * as cdk from '@aws-cdk/cdk';

// import * as iam from '@aws-cdk/aws-iam';
import * as kms from '@aws-cdk/aws-kms';
import * as s3 from '@aws-cdk/aws-s3';

import * as utils from './utils';

export interface IArtifactsStore {
/**
* Bucket to store artifacts in given region
*/
readonly bucket: s3.IBucket;

/**
* KMS key **or alias** used to encrypt artifacts. Key can be optional for regions not
* supporting KMS.
*/
readonly artifactsKey?: kms.IEncryptionKey;
}

export interface ArtifactsStoreProps {
/**
* Bucket to store artifacts in given region
*/
bucketName: string;

/**
* Alias of `key` to use when passing key to other stacks.
* As KMS keys are constructed in other stacks. Keys are passed between
* stacks, regions and accounts using aliases.
*/
artifactsKeyAlias?: string;
}

/**
* Represents artifacts store.
*
* Artifacts store is composed from bucket and eventually KMS key (or alias), which is used to encrypt or
* decrypt artifacts.
*/
export class ArtifactsStore extends cdk.Construct implements IArtifactsStore {
/**
* Generates artifact store using `baseName`.
*
* The `baseName` is used to bucket and kms key alias names.
*/
public static fromBaseName(parent: cdk.Construct, id: string, props: {
baseName: string, region: string, account?: string}) {

// Truncate to satisfy S3 (and KMS) len requirements and keep consistent naming
const maxLen = 30;
let baseName = props.baseName;

if (baseName.length > maxLen) {
baseName = props.baseName.substr(0, maxLen);
}
baseName = utils.generateUniqueName(baseName + '-', "", props.account || '', false, 8);

// Generate S3 bucket name
const bucketName = `${baseName}-artifacts-${props.region}`.toLowerCase();

// Generate key alias; keys and aliases are region and account bound, don't add that value to name
let artifactsKeyAlias;
if (!props.region.startsWith('cn-')) {
artifactsKeyAlias = `alias/${baseName}-artifacts-key`;
}

return new ArtifactsStore(parent, id, {
bucketName,
artifactsKeyAlias,
});
}

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

/**
* The name of bucket used to store artifacts.
* If store has been created within stack with known account and region
* this value will fully represent physical name, and should not contain
* pseudo parameters.
*/
public readonly bucketName: string;

/**
* Encryption key used to encrypt artifacts. In this class
* this attribute represent physical key created in stack.
*/
public readonly artifactsKey?: kms.EncryptionKey;

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

/**
* Constructs new artifacts store with given properties.
* **Consider using `fromBaseName`**
*/
constructor(parent: cdk.Construct, id: string, props: ArtifactsStoreProps) {
super(parent, id);

const env = cdk.Stack.find(parent).env;
const account = env.account || new cdk.Aws().accountId;
const region = env.region || new cdk.Aws().region;

if (props.artifactsKeyAlias) {
const tags: any = {};
// if (props.keyAlias) {
// tags[props.keyAlias] = '';
// }
this.artifactsKey = new kms.EncryptionKey(this, `ArtifactsKey`, {
description: `Key used to encrypt artifacts generated with code pipeline`,
tags
});

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

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

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

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

/**
* 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 ImportedArtifactsStoreProps {
/**
* The name of the S3 Bucket used for replicating the Pipeline's artifacts into the region.
*/
bucketName: string;

/**
* Encryption key used to encrypt artifacts, it can represent key ARN or it can be an alias to key.
*/
artifactsKeyArn?: string;

// /**
// * Tag used to identify artifacts key. If it's provided will
// * be used to generate access policies, otherwise some policies
// * can generate access to any key.
// */
// artifactKeyTag?: string;
}

/**
* Represents imported artifacts store.
*/
export class ImportedArtifactsStore extends cdk.Construct implements IArtifactsStore {
public readonly bucket: s3.IBucket;
/**
* Represents key or alias used to encrypt artifacts in bucket.
*/
// TODO Any better name to capture 'key or alias'
// TODO Is IEncryptionKey good type, maybe ARN would be enough
public readonly artifactsKey?: kms.IEncryptionKey;

// public readonly artifactKeyTag?: string; // For managing keys by tags

constructor(parent: cdk.Construct, id: string, props: ImportedArtifactsStoreProps) {
super(parent, id);

this.bucket = s3.Bucket.import(this, `${id}-Bucket`, {
bucketName: props.bucketName
});

if (props.artifactsKeyArn) {
this.artifactsKey = kms.EncryptionKey.import(this, `${id}-ImportedKey`, {
keyArn: props.artifactsKeyArn
});
}
}
}
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}`;
}
}

Loading

0 comments on commit 633b7aa

Please sign in to comment.