🛠️

AWS CDK + cdk-nag + AWS CodePipelineでセキュアなIaCを実現する

2023/02/25に公開

はじめに

「AWS CDK + cdk-nag + AWS CodePipeline = セキュアなIaCを実現できるのでは?」と考えました。
その際、cdk-nagとCodePipelineを組み合わせるにはエラー抑制が必要だったので、記事にしました。

cdk-nagとは

cdk-nagは、AWS CDKで構築したリソースがセキュリティルール群に準拠しているか検証することができます。
これによって、AWSリソースのセキュリティに穴を作りにくくして、セキュアなインフラを構築することができます。

セキュリティルール群は下記のようなものがすでに用意されています。
また、独自にセキュリティルールを記述することもできます。

  • AWS Solutions
  • HIPAA Security
  • NIST 800-53 rev 4
  • NIST 800-53 rev 5
  • PCI DSS 3.2.1

手順

2ステップに分けて説明します。

  1. AWS CDK + cdk-nag で AWSリソースにセキュリティルールを適用する
  2. AWS CDK で AWS CodePipeline を構築する

1. AWS CDK + cdk-nag で AWSリソースにセキュリティルールを適用する

このステップでは、下記を実施していきます。

  • AWS CDKでS3を作成する
  • cdk-nagでAwsSolutionsのセキュリティ群を適用する

参考:AWS Cloud Development Kit と cdk-nag でアプリケーションのセキュリティとコンプライアンスを管理する

AWS CDKでリソースの作成

AWS CDKでリソースを作成します。

mkdir cdk-nag-pipeline
cd cdk-nag-pipeline
cdk init app --language typescript

lib/cdk-nag-pipeline-stack.tsを、下記の内容に書き換えます。

import * as cdk from "aws-cdk-lib";
+ import { Bucket } from "aws-cdk-lib/aws-s3";
import { Construct } from "constructs";

export class CdkNagPipelineStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);
+    const bucket = new Bucket(this, "Bucket");
  }
}

以下のコマンドを実行して、依存関係のインストールとテンプレートの生成をします。

npm install
npx cdk synth

成功すれば、CloudFormationテンプレートが表示されます。

NagPackを適用する

cdk-nagをインストールする。

npm install cdk-nag

bin/cdk-nag-pipeline.tsを、下記の内容に書き換えます。

#!/usr/bin/env node
import "source-map-support/register";
import * as cdk from "aws-cdk-lib";
import { CdkNagPipelineStack } from "../lib/cdk-nag-pipeline-stack";
+ import { Aspects } from "aws-cdk-lib";
+ import { AwsSolutionsChecks } from "cdk-nag";

const app = new cdk.App();
+ Aspects.of(app).add(new AwsSolutionsChecks({ verbose: true }));
new CdkNagPipelineStack(app, "CdkNagPipelineStack", {});

以下のコマンドを実行して出力結果を表示し、コンプライアンスレポートを作成します。

npx cdk synth

出力結果は、以下のようにcdk-nagのエラーが表示されます。

[Error at /CdkNagPipelineStack/Bucket/Resource] AwsSolutions-S1: The S3 Bucket has server access logs disabled. The bucket should have server access logging enabled to provide detailed records for the requests that are made to the bucket.

[Error at /CdkNagPipelineStack/Bucket/Resource] AwsSolutions-S2: The S3 Bucket does not have public access restricted and blocked. The bucket should have public access restricted and blocked to prevent unauthorized access.

[Error at /CdkNagPipelineStack/Bucket/Resource] AwsSolutions-S10: The S3 Bucket or bucket policy does not require requests to use SSL. You can use HTTPS (TLS) to help prevent potential attackers from eavesdropping on or manipulating network traffic using person-in-the-middle or similar attacks. You should allow only encrypted connections over HTTPS (TLS) using the aws:SecureTransport condition on Amazon S3 bucket policies.


Found errors

エラーの修正と抑制

ここでは、下記のようなエラーの修正と抑制を行います。

Rule ID 概要 対応
AwsSolutions-S1 S3 Bucketのサーバーアクセスログが無効になっている Stackレベルのエラーの抑制
AwsSolutions-S2 S3 Bucketのパブリックアクセスが制限及びブロックされていない Resourceレベルのエラーの抑制
AwsSolutions-S10 S3バケットまたはバケットポリシーでは、リクエストにSSLを使用する必要はない エラーの修正

lib/cdk-nag-pipeline-stack.tsを、下記のように書き換えます。

import * as cdk from "aws-cdk-lib";
import { Bucket } from "aws-cdk-lib/aws-s3";
+ import { NagSuppressions } from "cdk-nag";
import { Construct } from "constructs";

export class CdkNagPipelineStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);
+    // The local scope 'this' is the Stack.
+    NagSuppressions.addStackSuppressions(this, [
+      {
+        id: "AwsSolutions-S1",
+        reason: "Demonstrate a stack level suppression.",
+      },
+    ]);
+    // Remediating AwsSolutions-S10 by enforcing SSL on the bucket.
+    const bucket = new Bucket(this, "Bucket", { enforceSSL: true });
-    const bucket = new Bucket(this, "Bucket");
+    NagSuppressions.addResourceSuppressions(bucket, [
+      {
+        id: "AwsSolutions-S2",
+        reason: "Demonstrate a resource level suppression.",
+      },
+    ]);
  }
}

下記コマンドを再度実行します。

npx cdk synth

成功すれば、CloudFormationテンプレートが表示されます。

2. AWS CDK で AWS CodePipeline を構築する

今回は下記の条件で構築します。

  • AWS CodeCommitにリポジトリを作成する
  • CDK PipelinesでCI/CDパイプラインを構築する(1アカウント・1環境)
  • cdk-nagのエラー制御を適用する

参考:aws-cdk-lib.pipelines module

もし、マルチアカウント・マルチリージョンでCodePipelineを構築する場合は、CDK Pipelines: AWS CDK アプリケーションの継続的デリバリを参考に構築すると良いです。

CodeCommitにリポジトリを作成する

AWS CodePipelineのソースコードはGitHubなども接続できるが、今回はAWS CodeCommitにリポジトリを作成して接続します。

AWS CLIのコマンドでリポジトリを作成します(AWS Console上で作成してもOK)。

aws codecommit create-repository --repository-name cdk-nag-pipeline --region ap-northeast-1

下記のような出力結果が表示されるので、cloneUrlHttpのURLをコピーします。

{
    "repositoryMetadata": {
        "accountId": "046363776660",
        "repositoryId": "b6b25655-f4f3-44aa-82a7-a173bb85d078",
        "repositoryName": "cdk-nag-pipeline",
        "lastModifiedDate": "2023-02-25T02:10:43.727000+09:00",
        "creationDate": "2023-02-25T02:10:43.727000+09:00",
        "cloneUrlHttp": "https://git-codecommit.ap-northeast-1.amazonaws.com/v1/repos/cdk-nag-pipeline",
        "cloneUrlSsh": "ssh://git-codecommit.ap-northeast-1.amazonaws.com/v1/repos/cdk-nag-pipeline",
        "Arn": "arn:aws:codecommit:ap-northeast-1:046363776660:cdk-nag-pipeline"
    }
}

コピーしたURLを設定して、ローカルリポジトリをCodeCommitリポジトリに接続します。

git remote add origin https://git-codecommit.ap-northeast-1.amazonaws.com/v1/repos/cdk-nag-pipeline  

以下のコマンドで、CodeCommitリポジトリにソースコードをコミットします。

 git add . 
 git commit -m "first commit"
 git push -u origin main

CDK Pipelinesを構築する

lib/code-pipeline-stack.tsを作成して、下記をソースコードを記述します。

import * as cdk from "aws-cdk-lib";
import { Stage } from "aws-cdk-lib";
import { NagSuppressions } from "cdk-nag";
import { Construct } from "constructs";
import { CdkNagPipelineStack } from "./cdk-nag-pipeline-stack";

export class CodePipelineStack extends cdk.Stack {
 constructor(scope: Construct, id: string, props?: cdk.StackProps) {
   super(scope, id, props);

   // CodeCommit Repository
   const repository = cdk.aws_codecommit.Repository.fromRepositoryName(
     this,
     "Repository",
     "cdk-nag-pipeline"
   );

   // Pipeline
   const pipeline = new cdk.pipelines.CodePipeline(this, "Pipeline", {
     synth: new cdk.pipelines.ShellStep("Synth", {
       input: cdk.pipelines.CodePipelineSource.codeCommit(repository, "main"),
       commands: ["npm ci", "npm run build", "npx cdk synth"],
     }),
   });

   // Stages
   pipeline.addStage(new MyApplication(this, "Prod", {}));

   // Suppress aws-cdk-lib/pipelines violations
   pipeline.buildPipeline();
   const reason = "Suppress aws-cdk-lib/pipelines violations";
   NagSuppressions.addStackSuppressions(this, [
     { id: "AwsSolutions-S1", reason: reason },
     { id: "AwsSolutions-IAM5", reason: reason },
     { id: "AwsSolutions-CB4", reason: reason },
   ]);
 }
}

class MyApplication extends Stage {
 constructor(scope: Construct, id: string, props?: cdk.StageProps) {
   super(scope, id, props);
   const cdkNagPipeline = new CdkNagPipelineStack(
     this,
     "CdkNagPipelineStack",
     {}
   );
 }
}

bin/cdk-nag-pipeline.tsを、下記の内容に書き換えます。

#!/usr/bin/env node
import "source-map-support/register";
import * as cdk from "aws-cdk-lib";
-import { CdkNagPipelineStack } from "../lib/cdk-nag-pipeline-stack";
import { Aspects } from "aws-cdk-lib";
import { AwsSolutionsChecks } from "cdk-nag";
+ import { CodePipelineStack } from "../lib/code-pipeline-stack";

const app = new cdk.App();
Aspects.of(app).add(new AwsSolutionsChecks({ verbose: true }));
+
+ const codePipeline = new CodePipelineStack(app, "CodePipelineStack", {});
- new CdkNagPipelineStack(app, "CdkNagPipelineStack", {});

下記コマンドを実行して、CloudFormationテンプレートが表示されるか確認します。

npx cdk synth

問題がなければ、CodeCommitにコミットします。

git add .
git commit -m "add: AWS CodePipelineでCI/CDできるようにした"
git push -u origin main

cdk deployする

下記のコマンドを実行します。

cdk bootstrap 
cdk deploy

次のように表示されたら、yを入力してEnterを押下します。

Do you wish to deploy these changes (y/n)? 

無事にデプロイが完了したら、AWS ConsoleのPipelineが起動しています。
あとは適宜コミットすることで、CI/CDができるようになっています。

Discussion