🐳
AWS×OSSツールで実現するDocker向けDevSecOpsパイプライン
導入
背景・目的
- コンテナセキュリティを検討する際、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に対して
Trivy
、Dockle
、Cosign
を組み込みます。
- AWS公式のアナウンス通り、CodeCommitは新規顧客の利用不可になりました。本番ワークロードで本構成を採用する場合には、他選択肢を候補に検討ください。
CodeBuildの仕様ファイル作成
- まずは、ビルド用CodeBuildの仕様ファイルを作成します。
- イメージのビルド、スキャン、署名を実施します。
- Trivyで脆弱性スキャン実施し、CRITICALを検出した場合にはビルドを終了します。
- 既存ベースイメージを利用する都合上、脆弱性を一切0にすることは現実的に難しいことから、検出対象をCRITICALに限定しています。また、CRITICALの脆弱性を検出した場合でも、状況次第ではデプロイ許容も視野に入ります。プロジェクト内でセキュリティ担当者交えた議論を重ねたうえで判断ください。
- スキャン時に
failed to download vulnerability DB
エラーが発生したことから、db-repository
やjava-db-repository
を明示的に指定しています。
- Dockleでセキュリティ設定スキャン実施し、Fatalを検出した場合にはビルドを終了します。
- 今回は設定しませんが、必要に応じてignore(除外設定)の実装も検討ください。
- CosignでKMSの非対称キーを用いて、Dockerイメージに署名します。
- Trivyで脆弱性スキャン実施し、CRITICALを検出した場合にはビルドを終了します。
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
の値を変更
- デプロイ用CodeBuildの環境変数
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
参考
注意事項
- 本記事は万全を期して作成していますが、お気づきの点がありましたら、ご連絡よろしくお願いします。
- なお、本記事の内容を利用した結果及び影響について、筆者は一切の責任を負いませんので、予めご了承ください。
Discussion