Skip to content

Commit

Permalink
feat(pipelines): confirm IAM changes before starting the deployment (#…
Browse files Browse the repository at this point in the history
…15441)

Add an option under `addApplication` for a given stage to create a CodeBuild that checks if there are any security changes within the stage's assembly. 

* If the changes exist: **manual approval is required**
* else: a lambda function will automatically approve the manual approval action

Adding a security check to an application creates two actions that precede the prepare
and deploy actions of an application:
1. A CodeBuild Project that runs a security diff on the stage
2. A Manual Approval Action that can be approved via a shared Lambda function.

```txt
Pipeline
├── Stage: Build
│   └── ...
├── Stage: Synth
│   └── ...
├── Stage: UpdatePipeline
│   └── ...
├── Stage: MyApplicationStage
│   └── Actions
│       ├── MyApplicationSecurityCheck       // Security Diff Action
│       ├── MyApplicationManualApproval      // Manual Approval Action
│       ├── Stack.Prepare
│       └── Stack.Deploy
└── ...
```

<details>
<summary>Example Usage</summary>
You can enable the security check in one of two ways:

1. Enable security check across the entire `CdkStage`

    ```ts
    const pipeline = new CdkPipeline(app, 'Pipeline', {
      // ...source and build information here (see above)
    });
    const stage = pipeline.addApplicationStage(new MyApplication(this, 'Testing'), {
      securityCheck: true,
    });
    // The 'PreProd' application is also run against a security diff because we configured
    // the stage to enable security checks
    stage.addApplication(new MyApplication(this, 'PreProd'));
    ```

2. Enable security check for a single application

    ```ts
    const pipeline = new CdkPipeline(app, 'Pipeline', {
      // ...source and build information here (see above)
    });
    const stage = pipeline.addApplicationStage(new MyApplication(this, 'NoCheck'));
    stage.addApplication(new MyApplication(this, 'RunSecurityDiff'), {
      securityCheck: true,
    });
    ```

</details>

Fixes: #12748

----

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
BryanPan342 authored Jul 16, 2021
1 parent 22f2499 commit ebba618
Show file tree
Hide file tree
Showing 15 changed files with 3,526 additions and 36 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@
"@aws-cdk/core/minimatch/**",
"@aws-cdk/cx-api/semver",
"@aws-cdk/cx-api/semver/**",
"@aws-cdk/pipelines/aws-sdk",
"@aws-cdk/pipelines/aws-sdk/**",
"@aws-cdk/yaml-cfn/yaml",
"@aws-cdk/yaml-cfn/yaml/**",
"aws-cdk-lib/@balena/dockerignore",
Expand Down
58 changes: 57 additions & 1 deletion packages/@aws-cdk/pipelines/ORIGINAL_API.md
Original file line number Diff line number Diff line change
Expand Up @@ -495,4 +495,60 @@ const validationAction = new ShellScriptAction({
// 'test.js' was produced from 'test/test.ts' during the synth step
commands: ['node ./test.js'],
});
```
```

### Confirm permissions broadening

To keep tabs on the security impact of changes going out through your pipeline,
you can insert a security check before any stage deployment. This security check
will check if the upcoming deployment would add any new IAM permissions or
security group rules, and if so pause the pipeline and require you to confirm
the changes.

The security check will appear as two distinct actions in your pipeline: first
a CodeBuild project that runs `cdk diff` on the stage that's about to be deployed,
followed by a Manual Approval action that pauses the pipeline. If it so happens
that there no new IAM permissions or security group rules will be added by the deployment,
the manual approval step is automatically satisfied. The pipeline will look like this:

```txt
Pipeline
├── ...
├── MyApplicationStage
│   ├── MyApplicationSecurityCheck // Security Diff Action
│   ├── MyApplicationManualApproval // Manual Approval Action
│   ├── Stack.Prepare
│   └── Stack.Deploy
└── ...
```

You can enable the security check by passing `confirmBroadeningPermissions` to
`addApplicationStage`:

```ts
const stage = pipeline.addApplicationStage(new MyApplication(this, 'PreProd'), {
confirmBroadeningPermissions: true,
});
```

To get notified when there is a change that needs your manual approval,
create an SNS Topic, subscribe your own email address, and pass it in via
`securityNotificationTopic`:

```ts
import * as sns from '@aws-cdk/aws-sns';
import * as subscriptions from '@aws-cdk/aws-sns-subscriptions';
import * as pipelines from '@aws-cdk/pipelines';

const topic = new sns.Topic(this, 'SecurityChangesTopic');
topic.addSubscription(new subscriptions.EmailSubscription('[email protected]'));

const pipeline = new CdkPipeline(app, 'Pipeline', { /* ... */ });
const stage = pipeline.addApplicationStage(new MyApplication(this, 'PreProd'), {
confirmBroadeningPermissions: true,
securityNotificationTopic: topic,
});
```

**Note**: Manual Approvals notifications only apply when an application has security
check enabled.
64 changes: 63 additions & 1 deletion packages/@aws-cdk/pipelines/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -840,7 +840,7 @@ and orphan the old bucket. You should manually delete the orphaned bucket
after you are sure you have redeployed all CDK applications and there are no
more references to the old asset bucket.

## Security Tips
## Security Considerations

It's important to stay safe while employing Continuous Delivery. The CDK Pipelines
library comes with secure defaults to the best of our ability, but by its
Expand All @@ -862,6 +862,68 @@ We therefore expect you to mind the following:
changes can be deployed through git. Avoid the chances of credentials leaking
by not having them in the first place!

### Confirm permissions broadening

To keep tabs on the security impact of changes going out through your pipeline,
you can insert a security check before any stage deployment. This security check
will check if the upcoming deployment would add any new IAM permissions or
security group rules, and if so pause the pipeline and require you to confirm
the changes.

The security check will appear as two distinct actions in your pipeline: first
a CodeBuild project that runs `cdk diff` on the stage that's about to be deployed,
followed by a Manual Approval action that pauses the pipeline. If it so happens
that there no new IAM permissions or security group rules will be added by the deployment,
the manual approval step is automatically satisfied. The pipeline will look like this:

```txt
Pipeline
├── ...
├── MyApplicationStage
│   ├── MyApplicationSecurityCheck // Security Diff Action
│   ├── MyApplicationManualApproval // Manual Approval Action
│   ├── Stack.Prepare
│   └── Stack.Deploy
└── ...
```

You can insert the security check by using a `ConfirmPermissionsBroadening` step:

```ts
const stage = new MyApplicationStage(this, 'MyApplication');
pipeline.addStage(stage, {
pre: [
new ConfirmPermissionsBroadening('Check', { stage }),
],
});
```

To get notified when there is a change that needs your manual approval,
create an SNS Topic, subscribe your own email address, and pass it in as
as the `notificationTopic` property:

```ts
import * as sns from '@aws-cdk/aws-sns';
import * as subscriptions from '@aws-cdk/aws-sns-subscriptions';
import * as pipelines from '@aws-cdk/pipelines';

const topic = new sns.Topic(this, 'SecurityChangesTopic');
topic.addSubscription(new subscriptions.EmailSubscription('[email protected]'));

const stage = new MyApplicationStage(this, 'MyApplication');
pipeline.addStage(stage, {
pre: [
new ConfirmPermissionsBroadening('Check', {
stage,
notificationTopic: topic,
}),
],
});
```

**Note**: Manual Approvals notifications only apply when an application has security
check enabled.

## Troubleshooting

Here are some common errors you may encounter while using this library.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import * as cb from '@aws-cdk/aws-codebuild';
import * as cp from '@aws-cdk/aws-codepipeline';
import { Construct } from 'constructs';
import { PipelineBase } from '../main';
import { ArtifactMap } from './artifact-map';
import { CodeBuildOptions } from './codepipeline';
import { CodeBuildOptions, CodePipeline } from './codepipeline';

/**
* Options for the `CodePipelineActionFactory.produce()` method.
Expand Down Expand Up @@ -43,7 +42,7 @@ export interface ProduceActionOptions {
/**
* The pipeline the action is being generated for
*/
readonly pipeline: PipelineBase;
readonly pipeline: CodePipeline;

/**
* If this action factory creates a CodeBuild step, default options to inherit
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { IStage } from '@aws-cdk/aws-codepipeline';
import * as cpa from '@aws-cdk/aws-codepipeline-actions';
import * as sns from '@aws-cdk/aws-sns';
import { Stage } from '@aws-cdk/core';
import { Node } from 'constructs';
import { Step } from '../blueprint';
import { ApplicationSecurityCheck } from '../private/application-security-check';
import { CodePipeline } from './codepipeline';
import { CodePipelineActionFactoryResult, ICodePipelineActionFactory, ProduceActionOptions } from './codepipeline-action-factory';

/**
* Properties for a `PermissionsBroadeningCheck`
*/
export interface PermissionsBroadeningCheckProps {
/**
* The CDK Stage object to check the stacks of
*
* This should be the same Stage object you are passing to `addStage()`.
*/
readonly stage: Stage;

/**
* Topic to send notifications when a human needs to give manual confirmation
*
* @default - no notification
*/
readonly notificationTopic?: sns.ITopic
}

/**
* Pause the pipeline if a deployment would add IAM permissions or Security Group rules
*
* This step is only supported in CodePipeline pipelines.
*/
export class ConfirmPermissionsBroadening extends Step implements ICodePipelineActionFactory {
constructor(id: string, private readonly props: PermissionsBroadeningCheckProps) {
super(id);
}

public produceAction(stage: IStage, options: ProduceActionOptions): CodePipelineActionFactoryResult {
const sec = this.getOrCreateSecCheck(options.pipeline);
this.props.notificationTopic?.grantPublish(sec.cdkDiffProject);

const variablesNamespace = Node.of(this.props.stage).addr;

const approveActionName = `${options.actionName}.Confirm`;
stage.addAction(new cpa.CodeBuildAction({
runOrder: options.runOrder,
actionName: `${options.actionName}.Check`,
input: options.artifacts.toCodePipeline(options.pipeline.cloudAssemblyFileSet),
project: sec.cdkDiffProject,
variablesNamespace,
environmentVariables: {
STAGE_PATH: { value: Node.of(this.props.stage).path },
STAGE_NAME: { value: stage.stageName },
ACTION_NAME: { value: approveActionName },
...this.props.notificationTopic ? {
NOTIFICATION_ARN: { value: this.props.notificationTopic.topicArn },
NOTIFICATION_SUBJECT: { value: `Confirm permission broadening in ${this.props.stage.stageName}` },
} : {},
},
}));

stage.addAction(new cpa.ManualApprovalAction({
actionName: approveActionName,
runOrder: options.runOrder + 1,
additionalInformation: `#{${variablesNamespace}.MESSAGE}`,
externalEntityLink: `#{${variablesNamespace}.LINK}`,
}));

return { runOrdersConsumed: 2 };
}

private getOrCreateSecCheck(pipeline: CodePipeline): ApplicationSecurityCheck {
const id = 'PipelinesSecurityCheck';
const existing = Node.of(pipeline).tryFindChild(id);
if (existing) {
if (!(existing instanceof ApplicationSecurityCheck)) {
throw new Error(`Expected '${Node.of(existing).path}' to be 'ApplicationSecurityCheck' but was '${existing}'`);
}
return existing;
}

return new ApplicationSecurityCheck(pipeline, id, {
codePipeline: pipeline.pipeline,
});
}
}
1 change: 1 addition & 0 deletions packages/@aws-cdk/pipelines/lib/codepipeline/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from './artifact-map';
export * from './codebuild-step';
export * from './confirm-permissions-broadening';
export * from './codepipeline';
export * from './codepipeline-action-factory';
export * from './codepipeline-source';
27 changes: 23 additions & 4 deletions packages/@aws-cdk/pipelines/lib/legacy/pipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@ import { Annotations, App, Aws, CfnOutput, Fn, Lazy, PhysicalName, Stack, Stage
import { Construct } from 'constructs';
import { AssetType } from '../blueprint/asset-type';
import { dockerCredentialsInstallCommands, DockerCredential, DockerCredentialUsage } from '../docker-credentials';
import { ApplicationSecurityCheck } from '../private/application-security-check';
import { appOf, assemblyBuilderOf } from '../private/construct-internals';
import { DeployCdkStackAction, PublishAssetsAction, UpdatePipelineAction } from './actions';
import { AddStageOptions, AssetPublishingCommand, CdkStage, StackOutput } from './stage';
import { AddStageOptions, AssetPublishingCommand, BaseStageOptions, CdkStage, StackOutput } from './stage';
import { SimpleSynthAction } from './synths';

// v2 - keep this import as a separate section to reduce merge conflict when forward merging with the v2 branch.
// eslint-disable-next-line
import { Construct as CoreConstruct } from '@aws-cdk/core';
import { SimpleSynthAction } from './synths';

const CODE_BUILD_LENGTH_LIMIT = 100;
/**
Expand Down Expand Up @@ -184,6 +185,7 @@ export class CdkPipeline extends CoreConstruct {
private readonly _outputArtifacts: Record<string, codepipeline.Artifact> = {};
private readonly _cloudAssemblyArtifact: codepipeline.Artifact;
private readonly _dockerCredentials: DockerCredential[];
private _applicationSecurityCheck?: ApplicationSecurityCheck;

constructor(scope: Construct, id: string, props: CdkPipelineProps) {
super(scope, id);
Expand Down Expand Up @@ -288,6 +290,22 @@ export class CdkPipeline extends CoreConstruct {
return this._pipeline.stage(stageName);
}

/**
* Get a cached version of an Application Security Check, which consists of:
* - CodeBuild Project to check for security changes in a stage
* - Lambda Function that approves the manual approval if no security changes are detected
*
* @internal
*/
public _getApplicationSecurityCheck(): ApplicationSecurityCheck {
if (!this._applicationSecurityCheck) {
this._applicationSecurityCheck = new ApplicationSecurityCheck(this, 'PipelineApplicationSecurityCheck', {
codePipeline: this._pipeline,
});
}
return this._applicationSecurityCheck;
}

/**
* Add pipeline stage that will deploy the given application stage
*
Expand All @@ -300,7 +318,7 @@ export class CdkPipeline extends CoreConstruct {
* publishing stage.
*/
public addApplicationStage(appStage: Stage, options: AddStageOptions = {}): CdkStage {
const stage = this.addStage(appStage.stageName);
const stage = this.addStage(appStage.stageName, options);
stage.addApplication(appStage, options);
return stage;
}
Expand All @@ -312,7 +330,7 @@ export class CdkPipeline extends CoreConstruct {
* application, but you can use this method if you want to add other kinds of
* Actions to a pipeline.
*/
public addStage(stageName: string) {
public addStage(stageName: string, options?: BaseStageOptions) {
const pipelineStage = this._pipeline.addStage({
stageName,
});
Expand All @@ -325,6 +343,7 @@ export class CdkPipeline extends CoreConstruct {
publishAsset: this._assets.addPublishAssetAction.bind(this._assets),
stackOutputArtifact: (artifactId) => this._outputArtifacts[artifactId],
},
...options,
});
this._stages.push(stage);
return stage;
Expand Down
Loading

0 comments on commit ebba618

Please sign in to comment.