🐳

AWS×OSSツールで実現するDocker向けDevSecOpsパイプライン

2024/11/15に公開

導入

背景・目的

  • コンテナセキュリティを検討する際、NISTが発行した信頼性の高いガイドラインである、NIST SP800-190を活用することは非常に有効です。
  • その中で触れられているコンテナイメージへのセキュリティリスク対策について、Trivy, Dockle, Cosign 等のOSSツールの採用も有力な選択肢になります。
  • まずは、NIST SP800-190やこれらのツールの概要を解説し、その後、AWSとOSSツールを組み合わせたDocker向けDevSecOpsパイプラインの実装方法や、CDKサンプルコードについて詳しく解説します。

対象読者

  • AWS Certified Security - Specialtyレベル以上の知識を想定し、AWSセキュリティサービスに対する詳細な説明は割愛します。

前提

  • ECR及びECS on Fargateへのデプロイを想定して、解説します。

NIST SP800-190概要

  • NIST SP800-190はコンテナに対するセキュリティリスク・対策が整理されている、NISTが発行したドキュメントです。
  • イメージ・レジストリ・オーケストレータ・コンテナ・ホストOSに対して、それぞれセキュリティリスク・対策が整理されています。
リスク分類 リスク概要 ECS on Fargate利用時の主要な対策例
イメージ イメージの脆弱性 TrivyやInspector等による脆弱性スキャン
イメージ設定の不備 Dockle等によるベストプラクティスチェック
埋め込まれたマルウェア ベースイメージにはDocker HubのOfficial ImageやECR PublicのVerified publishers等、信頼できるイメージを利用
埋め込まれた平文の秘密情報 Secrets Manager等を利用した秘密情報の外部化、Dockleやアプリ向けSASTツール等を用いたハードコードされたシークレットの検出
信頼できないイメージの使用 プライベートなECR内でイメージを一元管理、Cosign等によるイメージの署名検証
レジストリ レジストリへのセキュアでない接続 ECR利用時には原則としてHTTPSアクセスのため、開発者側での追加対策は原則不要
レジストリ内の古いイメージ ECRのライフサイクルポリシー設定等による古いイメージ削除
認証・認可の不十分な制限 IAMやECRリポジトリポリシーを通じた認証・認可の制御
オーケストレータ 制限のない管理者アクセス IAMによる制御
不正アクセス IAMによる制御
コンテナ間ネットワークトラフィックの不十分な分離 SG等によるネットワーク制御
ワークロードの機微性レベルの混合 ECS on Fargate利用時はAWS側の対応範囲のため、開発者側での追加対策は原則不要
オーケストレータノードの信頼 ECS on Fargate利用時はAWS側の対応範囲のため、開発者側での追加対策は原則不要
コンテナ ランタイムソフトウェア内の脆弱性 ECS on Fargate利用時はAWS側の対応範囲のため、開発者側での追加対策は原則不要
コンテナからの無制限のネットワークアクセス SG等によるネットワーク制御
セキュアでないコンテナランタイムの設定 ECS on Fargate利用時はAWS側の対応範囲のため、開発者側での追加対策は原則不要
アプリの脆弱性 ルートファイルシステムの読み取り専用化、AWS WAF等による防御
未承認コンテナ CodePipeline等のCI/CDパイプライン整備とIAMやECRリポジトリポリシーの制御の組み合せ
ホストOS 大きなアタックサーフェス ECS on Fargate利用時はAWS側の対応範囲のため、開発者側での追加対策は原則不要
共有カーネル ECS on Fargate利用時はAWS側の対応範囲のため、開発者側での追加対策は原則不要
ホスト OS コンポーネントの脆弱性 ECS on Fargate利用時はAWS側の対応範囲のため、開発者側での追加対策は原則不要
不適切なユーザアクセス権 ECS on Fargate利用時はAWS側の対応範囲のため、開発者側での追加対策は原則不要
ホスト OS ファイルシステムの改ざん ECS on Fargate利用時はAWS側の対応範囲のため、開発者側での追加対策は原則不要
  • 本ブログではイメージに対するリスクにフォーカスし、OSSツールの概要やAWSでのDevSecOpsパイプライン実装方法について解説していきます。

Trivy概要

  • TrivyはDockerコンテナイメージに対する脆弱性スキャンツールです。
  • OSパッケージに加えて、言語パッケージ(npm等)に対する脆弱性を検出します。
# install 
sudo rpm -ivh https://github.com/aquasecurity/trivy/releases/download/v0.57.0/trivy_0.57.0_Linux-64bit.rpm

# scan
trivy image --severity CRITICAL httpd:2.4.62

Dockle概要

  • Dockleはコンテナイメージのセキュリティ設定スキャンツールです。
  • コンテナイメージ設定及びDockerfileのコマンド履歴を元にして、ベストプラクティスやCIS Benchmarksへの準拠状況をスキャンします。
# install
sudo rpm -ivh https://github.com/goodwithtech/dockle/releases/download/v0.4.14/dockle_0.4.14_Linux-64bit.rpm

# scan
dockle httpd:2.4.62

Cosign概要

  • Cosignはコンテナイメージに署名を付与し、コンテナイメージの改ざん有無を確認するソリューションです。
  • AWS KMSを利用した署名・検証をサポートしています。
# install
curl -O -L "https://github.com/sigstore/cosign/releases/latest/download/cosign-linux-amd64"
sudo mv cosign-linux-amd64 /usr/local/bin/cosign
sudo chmod +x /usr/local/bin/cosign

# Generate Key Pair
cosign generate-key-pair --kms awskms:///alias/poc-key-cosign

# Retrive Public Key
cosign public-key --key awskms:///alias/poc-key-cosign

# Tagging & Push Image
docker tag httpd:2.4.62 012345678910.dkr.ecr.ap-northeast-1.amazonaws.com/poc-ecr-httpd:2.4.62
docker push 012345678910.dkr.ecr.ap-northeast-1.amazonaws.com/poc-ecr-httpd:2.4.62

# Sign
cosign sign --key awskms:///alias/poc-key-cosign --tlog-upload=false 012345678910.dkr.ecr.ap-northeast-1.amazonaws.com/poc-ecr-httpd:2.4.62

# Verify
cosign verify --insecure-ignore-tlog --key awskms:///alias/poc-key-cosign 012345678910.dkr.ecr.ap-northeast-1.amazonaws.com/poc-ecr-httpd:2.4.62

AWSでのDevSecOps構築

環境構成

  • CodePipeline及びCodeBuildに対してTrivyDockleCosignを組み込みます。
    • AWS公式のアナウンス通り、CodeCommitは新規顧客の利用不可になりました。本番ワークロードで本構成を採用する場合には、他選択肢を候補に検討ください。

CodeBuildの仕様ファイル作成

  • まずは、ビルド用CodeBuildの仕様ファイルを作成します。
  • イメージのビルド、スキャン、署名を実施します。
    • Trivyで脆弱性スキャン実施し、CRITICALを検出した場合にはビルドを終了します。
      • 既存ベースイメージを利用する都合上、脆弱性を一切0にすることは現実的に難しいことから、検出対象をCRITICALに限定しています。また、CRITICALの脆弱性を検出した場合でも、状況次第ではデプロイ許容も視野に入ります。プロジェクト内でセキュリティ担当者交えた議論を重ねたうえで判断ください。
      • スキャン時にfailed to download vulnerability DBエラーが発生したことから、db-repositoryjava-db-repositoryを明示的に指定しています。
    • Dockleでセキュリティ設定スキャン実施し、Fatalを検出した場合にはビルドを終了します。
    • CosignでKMSの非対称キーを用いて、Dockerイメージに署名します。

version: 0.2
env:
  exported-variables:
    - ECR_URI
phases:
  install:
    commands:
      - echo "Installing Trivy..."
      - sudo rpm -ivh https://github.com/aquasecurity/trivy/releases/download/v0.57.0/trivy_0.57.0_Linux-64bit.rpm
      - echo "Installing Dockle..."
      - sudo rpm -ivh https://github.com/goodwithtech/dockle/releases/download/v0.4.14/dockle_0.4.14_Linux-64bit.rpm
      - echo "Installing Cosign..."
      - curl -O -L "https://github.com/sigstore/cosign/releases/latest/download/cosign-linux-amd64"
      - sudo mv cosign-linux-amd64 /usr/local/bin/cosign
      - sudo chmod +x /usr/local/bin/cosign
      - echo "Logging in to Amazon ECR..."
      - aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin ${ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com
  build:
    commands:
      - echo "Building Docker image..."
      - TIMESTAMP=$(date +%Y%m%d%H%M%S)
      - IMAGE_TAG="$(echo ${CODEBUILD_RESOLVED_SOURCE_VERSION} | head -c 7)-${TIMESTAMP}"
      - docker build -t ${IMAGE_NAME}:${IMAGE_TAG} .
  post_build:
    commands:
      - echo "Scanning Docker image by Trivy..."
      - trivy image
        --severity CRITICAL
        --exit-code 1
        --quiet
        --db-repository public.ecr.aws/aquasecurity/trivy-db:2
        --java-db-repository public.ecr.aws/aquasecurity/trivy-java-db:1
        ${IMAGE_NAME}:${IMAGE_TAG}
      - echo "Scanning Docker image by Dockle..."
      - dockle --exit-code 1 --exit-level fatal ${IMAGE_NAME}:${IMAGE_TAG}
      - echo "Tagging and pushing the Docker image..."
      - ECR_URI=${ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com/${IMAGE_NAME}:${IMAGE_TAG}
      - docker tag ${IMAGE_NAME}:${IMAGE_TAG} ${ECR_URI}
      - docker push ${ECR_URI}
      - echo "Signing Docker image by Cosign..."
      - cosign sign --key awskms:///alias/${COSIGN_KMS_ALIAS} --tlog-upload=false ${ECR_URI}
  • 次に、ビルド用CodeBuildの仕様ファイルを作成します。
  • CosignでKMSの非対称キーを用いて、Dockerイメージが改ざんされていないかを検証し、成功後に後続のデプロイ処理を実施します。
version: 0.2
phases:
  install:
    commands:
      - echo "Installing Cosign..."
      - curl -O -L "https://github.com/sigstore/cosign/releases/latest/download/cosign-linux-amd64"
      - sudo mv cosign-linux-amd64 /usr/local/bin/cosign
      - sudo chmod +x /usr/local/bin/cosign
      - echo "Logging in to Amazon ECR..."
      - aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin ${ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com
  build:
    commands:
      - echo "Verifying Docker image by Cosign..."
      - cosign verify --insecure-ignore-tlog --key awskms:///alias/${COSIGN_KMS_ALIAS} ${ECR_URI}
  post_build:
    commands:
      - echo "Deploying ECS..."
      - (以下、略)

作成した仕様ファイルはCDKプロジェクトのassetsフォルダ配下に格納しておきます。

CDKを用いたAWS資源構築

  • CDKを用いて、AWS資源を構築していきます。
    • ECR
    • CodeBuild(ビルド用)
    • CodeBuild(デプロイ用)
    • CodePipeline
  • CodeBuildの構築時には、assetsフォルダ配下に格納した仕様ファイルを指定します。
import * as cdk from 'aws-cdk-lib';
import { Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import {
  aws_ecr as ecr,
  aws_iam as iam,
  aws_codebuild as codebuild,
  aws_codepipeline as codepipeline,
  aws_codecommit as codecommit,
  aws_codepipeline_actions as codepipeline_actions,
} from 'aws-cdk-lib';
import * as path from 'path';

export interface DockerDevSecOpsStackProps extends StackProps {
  readonly prefix: string;
  readonly envName: string;
  readonly codecommitArn: string;
  readonly cosignKmsAlias: string;
}

export class DockerDevSecOpsStack extends Stack {

  constructor(scope: Construct, id: string, props: DockerDevSecOpsStackProps) {
    super(scope, id, props)

    // ------------ Amazon ECR ---------------
    const ecrRepo = new ecr.Repository(this, "EcrRepo", {
      repositoryName: `poc-devsecops`,
      imageTagMutability: ecr.TagMutability.IMMUTABLE,
      removalPolicy: cdk.RemovalPolicy.DESTROY // 検証用のため、スタック削除時にECR削除
    })

    // ------------ AWS CodeSeries ---------------
    // ---- AWS CodeCommit
    const gitRepo = codecommit.Repository.fromRepositoryArn(this, "GitRepo", props.codecommitArn)

    // ---- AWS CodeBuild (Scan & Build)
    // Create IAM Role
    const buildRole = new iam.Role(this, 'BuildRole', {
      roleName: `${props.prefix}-${props.envName}-role-build-docker`,
      assumedBy: new iam.ServicePrincipal('codebuild.amazonaws.com'),
    });
    // 検証用のため、広めの権限を付与
    buildRole.addToPolicy(new iam.PolicyStatement({
      actions: [
        'ecr:*',
        'kms:*',
      ],
      resources: ['*'],
    }));

    // Create Docker Build Project
    const buildProject = new codebuild.Project(this, 'BuildProject', {
      projectName: `${props.prefix}-${props.envName}-build-docker`,
      buildSpec: codebuild.BuildSpec.fromAsset(path.join(__dirname, "../../assets/docker/buildspec.yml")),
      role: buildRole,
      environment: {
        privileged: true, // Dockerビルド用に特権付与
        buildImage: codebuild.LinuxBuildImage.AMAZON_LINUX_2_5,
      },
      environmentVariables: {
        ACCOUNT_ID: {
          type: codebuild.BuildEnvironmentVariableType.PLAINTEXT,
          value: props.env?.account
        },
        IMAGE_NAME: {
          type: codebuild.BuildEnvironmentVariableType.PLAINTEXT,
          value: ecrRepo.repositoryName
        },
        COSIGN_KMS_ALIAS: {
          type: codebuild.BuildEnvironmentVariableType.PLAINTEXT,
          value: props.cosignKmsAlias
        }
      },
    });

    // ---- AWS CodeBuild (Deploy)
    // Create IAM Role
    const deployRole = new iam.Role(this, 'DeployRole', {
      roleName: `${props.prefix}-${props.envName}-role-deploy-docker`,
      assumedBy: new iam.ServicePrincipal('codebuild.amazonaws.com'),
    });
    // 検証用のため、広めの権限を付与
    deployRole.addToPolicy(new iam.PolicyStatement({
      actions: [
        'ecr:*',
        'kms:*',
      ],
      resources: ['*'],
    }));

    // Create Deploy Project
    const deployProject = new codebuild.Project(this, 'DeployProject', {
      projectName: `${props.prefix}-${props.envName}-deploy-docker`,
      buildSpec: codebuild.BuildSpec.fromAsset(path.join(__dirname, "../../assets/docker/deployspec.yml")),
      role: deployRole,
      environment: {
        buildImage: codebuild.LinuxBuildImage.AMAZON_LINUX_2_5,
      },
      environmentVariables: {
        ACCOUNT_ID: {
          type: codebuild.BuildEnvironmentVariableType.PLAINTEXT,
          value: props.env?.account
        },
        COSIGN_KMS_ALIAS: {
          type: codebuild.BuildEnvironmentVariableType.PLAINTEXT,
          value: props.cosignKmsAlias
        }
      },
    });

    // ---- AWS CodePipeline
    const pipeline = new codepipeline.Pipeline(this, 'Pipeline', {
      pipelineName: `${props.prefix}-${props.envName}-pipeline`,
    });
    // Add Source Stage
    const sourceOutput = new codepipeline.Artifact();
    const sourceAction = new codepipeline_actions.CodeCommitSourceAction({
      actionName: 'CodeCommit_Source',
      repository: gitRepo,
      branch: "main",
      output: sourceOutput,
    })
    pipeline.addStage({
      stageName: 'Source',
      actions: [sourceAction],
    });
    // Add Build Stage
    const buildAction = new codepipeline_actions.CodeBuildAction({
      actionName: 'CodeBuild_Scan_Build_Docker',
      project: buildProject,
      input: sourceOutput,
    })
    pipeline.addStage({
      stageName: 'Scan_Build',
      actions: [buildAction],
    });
    // Add Deploy Stage
    const deployAction = new codepipeline_actions.CodeBuildAction({
      actionName: 'CodeBuild_Deploy',
      project: deployProject,
      input: sourceOutput,
      environmentVariables: {
        ECR_URI: {
          value: buildAction.variable("ECR_URI"),
        },
      },
    })
    pipeline.addStage({
      stageName: 'Deploy',
      actions: [deployAction],
    });
  }
}

動作確認

正常系

  • Trivy, DockleによるスキャンやCosignでの署名検証に成功した場合、パイプライン実行が成功しました。

異常系

  • TrivyでCRITICALが検出されるよう、Dockerfileのベースイメージにpython:3.14-rc-slimを利用したところ、パイプライン実行が中断されました。

poc-devsecops:f12c392-20241114014311 (debian 12.8)
==================================================
Total: 1 (CRITICAL: 1)

Command did not exit successfully trivy image --severity CRITICAL --exit-code 1 --quiet --db-repository public.ecr.aws/aquasecurity/trivy-db:2 --java-db-repository public.ecr.aws/aquasecurity/trivy-java-db:1 ${IMAGE_NAME}:${IMAGE_TAG} exit status 1
  • DockleでFatalが検出されるよう、Dockerfile内でADDコマンドを利用したところ、パイプライン実行が中断されました。
FATAL   - CIS-DI-0009: Use COPY instead of ADD in Dockerfile
    * Use COPY : ADD test.txt /tmp/test.txt # buildkit
Command did not exit successfully dockle --exit-code 1 --exit-level fatal ${IMAGE_NAME}:${IMAGE_TAG} exit status 1
  • Cosignでの署名検証に失敗するよう、署名時とは異なるKMSを用いて署名検証を試みたところ、パイプライン実行が中断されました。
    • デプロイ用CodeBuildの環境変数COSIGN_KMS_ALIASの値を変更
Error: no matching signatures: invalid signature when validating ASN.1 encoded signature
Command did not exit successfully cosign verify --insecure-ignore-tlog --key awskms:///alias/${COSIGN_KMS_ALIAS} ${ECR_URI} exit status 12

参考

https://www.ipa.go.jp/security/reports/oversea/nist/ug65p90000019cp4-att/000085279.pdf
https://github.com/aquasecurity/trivy
https://github.com/goodwithtech/dockle?tab=readme-ov-file
https://gallery.ecr.aws/aquasecurity?page=1
https://github.com/aquasecurity/trivy/discussions/7538
https://qiita.com/tomoyamachi/items/bb6ac5788bb734c91282
https://aws.amazon.com/jp/blogs/opensource/supply-chain-security-on-amazon-elastic-kubernetes-service-amazon-eks-using-aws-key-management-service-aws-kms-kyverno-and-cosign/
https://github.com/sigstore/cosign/blob/main/doc/cosign_sign.md

注意事項

  • 本記事は万全を期して作成していますが、お気づきの点がありましたら、ご連絡よろしくお願いします。
  • なお、本記事の内容を利用した結果及び影響について、筆者は一切の責任を負いませんので、予めご了承ください。
Accenture Japan (有志)

Discussion