TOP>コラム一覧>AWS環境でのコンテナイメージへの署名

AWS環境でのコンテナイメージへの署名

はじめに

近年、ソフトウェアサプライチェーンの安全性の問題が注目を集めており、さまざまな組織から対策のためのガイドラインが提供されています。

その中で求められるプラクティスのひとつに、ビルド生成物(アーティファクト)に対する署名があります。例えば、CIS が提供しているガイドライン「 Software Supply Chain Security Guide」では、項目 4.1.1 で "Ensure all artifacts are signed by the build pipeline itself" としてビルドパイプラインによるアーティファクトへの署名を推奨しており、単純な署名検証プロセスで攻撃者によるアーティファクトの改変を検出できるであろうとしています。

また、AWSからも、「コンテナにおけるデジタル署名」というブログ記事で、特にコンテナイメージへの署名の重要性について紹介されています。

本記事では、上記ブログ記事と同じくアーティファクトとしてコンテナイメージを想定し、AWS環境のビルドパイプライン上で、コンテナイメージに対して署名や検証を行う例を示します。

Cosign

コンテナイメージに対して署名や検証を行う手段はいくつかありますが、本記事の例では Cosign というツールを使用します。

Cosign は、Sigstore というプロジェクトの一部です。Sigstore は Linux Foundation がホストするプロジェクトで、サプライチェーンの向上のため、ソフトウェアに対するデジタル署名を利用しやすくするサービスを提供しています。

Cosign はコンテナレジストリとして AWS ECR を(参照)、キー管理サービスとして KMS を(参照)サポートしており、AWS環境でのコンテナイメージへの署名と検証を比較的容易に行うことができます。

本記事では Cosign バージョン 2.0.0 を使用します。

構成

本記事でのシステム構成は以下の通りです。

本記事の構成図

ビルドパイプライン用の CodePipeline

ビルド用のパイプラインでは、以下の流れでイメージのビルドと署名を行います。

  1. 1. CodeCommit から Dockerfile を取得し、CodeBuild でイメージをビルド
  2. 2. CodeBuild から ECR にイメージをプッシュ
  3. 3. CodeBuild で、KMS管理のプライベートキーによりイメージに署名 (Cosignを使用)

デプロイパイプライン用の CodePipeline

デプロイ用のパイプラインでは、以下の流れでイメージの署名検証とデプロイを行います。

  1. 1. CodeCommit から imagedefinitions.json を取得し、デプロイ対象のイメージURIを取得
  2. 2. CodeBuild で、KMS管理のパブリックキーによりイメージの署名を検証(Cosignを使用)
  3. 3. 検証に問題なければ imagedefinitions.json の内容を ECS にデプロイ

署名用KMSキー

イメージへの署名には、署名用のプライベートキーと、それを検証するためのパブリックキーのペアが必要です。

Cosignは各種クラウドのキー管理サービスと統合されており、AWS KMSの場合も、キーIDやエイリアスを指定するだけでそのキーを使用して署名できます。これによりプライベートキーをファイルで管理する必要がなくなり、安全性と利便性が向上します。

署名用のキーであるため、KMSキーの作成時に、キーの種類「非対称」、キーの使用「署名および検証」を選択します。今回は、以下の仕様で作成しました。

KMSキーの暗号化設定

また、キーには cosign というエイリアスを設定しています。

KMSキーのエイリアス設定

以降では、それぞれのパイプラインの詳細を見ていきます。

ビルドパイプライン

ビルドパイプラインの構成は以下のようになっています。

ビルドパイプライン

Sourceステージは CodeCommit からソースコードを取得しているだけですので、イメージのビルドと署名を行っている DockerBuild ステージの内容を見ていきます。

buildspec.yaml

DockerBuild で使用する buildspec.yaml の内容は以下の通りです。

version: 0.2

env:
  variables:
    AWS_ACCOUNT_ID: "<AWSアカウントID>"
    IMAGE_NAME: "demo"
phases:
  install:
    commands:
      - echo "Installing Cosign..."
      - wget -nv "https://github.com/sigstore/cosign/releases/download/v2.0.0/cosign_2.0.0_amd64.deb" -O cosign_amd64.deb
      # 本来はパッケージの検証が必要
      - dpkg -i cosign_amd64.deb
      - cosign version
  pre_build:
    commands:
      - echo "Building image..."
      - IMAGE_TAG=$(echo "$CODEBUILD_RESOLVED_SOURCE_VERSION" | head -c 7)
      - docker build -t "${IMAGE_NAME}:${IMAGE_TAG}" .
      - echo "Logging in to ECR..."
      - IMAGE_DOMAIN="${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com"
      - aws ecr get-login-password --region ${AWS_REGION} | docker login --username AWS --password-stdin ${IMAGE_DOMAIN}
  build:
    commands:
      - echo "Pushing image..."
      - IMAGE_FULLNAME="${IMAGE_DOMAIN}/${IMAGE_NAME}"
      - docker tag "${IMAGE_NAME}:${IMAGE_TAG}" "${IMAGE_FULLNAME}:${IMAGE_TAG}"
      # プッシュしたイメージのダイジェスト値を取得
      - docker push "${IMAGE_FULLNAME}:${IMAGE_TAG}" | tee -a docker-push.log
      - |
        IMAGE_DIGEST=$(sed -ne "s/^${IMAGE_TAG}: digest: \\([^[:space:]]\\+\\).*/\\1/ip" docker-push.log)
      - test -n "${IMAGE_DIGEST}"
      - echo "Signing image..."
      - cosign sign --tlog-upload=false --key awskms:///alias/cosign "${IMAGE_FULLNAME}@${IMAGE_DIGEST}"

ビルドの各フェーズの内容について説明します。

installフェーズ

installフェーズでは、公式ドキュメントに従って Cosign をインストールしています。

pre_build フェーズ

pre_build フェーズでは、イメージのビルドとECRへのログインを行っています。

イメージのタグには、CodeBuildのコミットIDを表す $CODEBUILD_RESOLVED_SOURCE_VERSION を参照して、コミットIDの先頭を取り出して設定しています。

buildフェーズ

build フェーズでは、ビルドしたイメージをECRにプッシュし、署名を行っています。

署名は、プッシュしたイメージのURIを使用して、cosign signコマンドで実施しています。

cosign sign --tlog-upload=false --key awskms:///alias/cosign <署名対象イメージのURI>

--key は、署名用のKMSキーを指定するパラメーターです。プレフィックス awskms:/// をつけるとAWS KMSのキーを直接参照できます。

今回はエイリアスを設定しているため、キーは awskms:///alias/<KMSキーのエイリアス> という形式で指定できます。 エイリアスを使用せず awskms:///<キーID> という形式でID指定することも可能です。

署名の格納先

イメージへの署名は、 cosign sign コマンドによって、コンテナイメージのレジストリ(今回はECRリポジトリ)に <署名対象イメージのダイジェスト>.sig のようなタグで格納されます。

例えば、下記の画像では、タグ 42702b6 のイメージのダイジェスト sha256:d21ca72~ を元にして、 sha256-d21ca72~.sig というタグで署名が格納されている様子がわかります。

KMSキーのエイリアス設定

署名用のタグの命名規則や、署名の内容についての詳細な仕様は https://github.com/sigstore/cosign/blob/main/specs/SIGNATURE_SPEC.md を参照してください。

署名対象の指定

署名対象のイメージは、ダイジェストを使用して厳密に指定することが推奨されています。イメージのURIをダイジェストで指定する場合、以下の例のようにリポジトリ名と @ で区切ります。

<AWSアカウントID>.dkr.ecr.<リージョン>.amazonaws.com/demo@sha256:d21ca72c272893b457c4fe47020131af88eb018a185524a5755001c70d76220f

プッシュしたイメージのダイジェスト値を取得する簡単な方法がわからなかったため、ログをパースして取得しています。

ダイジェストではなくタグで署名対象を指定すると、Cosignは以下のように警告を出します。可能な限りダイジェストで指定した方が良いでしょう。

WARNING: Image reference <AWSアカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com/demo:0c4cc01 uses a tag, not a digest, to identify the image to sign.
  This can lead you to sign a different image than the intended one. Please use a
  digest (example.com/ubuntu@sha256:abc123...) rather than tag
  (example.com/ubuntu:latest) for the input to cosign. The ability to refer to
  images by tag will be removed in a future release.

Transparency Log

Cosign は、デフォルトでは Sigstore プロジェクトが提供する Rekor というログサービスに Transparency Log(透明性ログ)を格納し、第三者が検証できるようにしています。

ただし、今回の構成はプライベートなイメージを想定しており、イメージの情報を外部に公開したくありません。そのため、 --tlog-upload=false オプションを指定して Transparency Log を Rekor にアップロードする処理を無効にしています。

今回は適用していませんが、この場合でもTSA(Timestamping Authority Service, 時刻認証局サービス)を使用して、過去の時点で正しい署名者に署名されていたことを検証可能にすることができます。詳細は以下のページを参照してください。

https://www.chainguard.dev/unchained/how-to-sign-private-artifacts-securely

サービスロール権限

ビルド・署名用のCodeBuildのサービスロールには、デフォルトで作成される権限に加えて、以下のIAMポリシーを付与します。

{
"Version": "2012-10-17",
"Statement": [
    {
        "Effect": "Allow",
        "Action": "ecr:GetAuthorizationToken",
        "Resource": "*"
    },
    {
        "Effect": "Allow",
        "Action": [
            "ecr:BatchCheckLayerAvailability",
            "ecr:BatchGetImage"
            "ecr:CompleteLayerUpload",
            "ecr:GetDownloadUrlForLayer",
            "ecr:InitiateLayerUpload",
            "ecr:PutImage",
            "ecr:UploadLayerPart"
        ],
        "Resource": [
            "arn:aws:ecr:<リージョン>:<AWSアカウントID>:repository/<リポジトリ名>"
        ]
    },
    {
        "Effect": "Allow",
        "Action": [
            "kms:DescribeKey",
            "kms:GetPublicKey",
            "kms:Sign"
        ],
        "Resource": [
            "arn:aws:kms:<リージョン>:<AWSアカウントID>:key/<署名用のキーID>"
        ]
    }
]
}

ECRに対する権限は、リポジトリにイメージと署名を格納するためのものです。特別な内容はありません。

KMSに対する権限の目的は、Cosignが非対称KMSキーのプライベートキーを使用して署名を行うことです。エイリアスの解決のために kms:DescribeKey 、署名のために kms:Signkms:GetPublicKey の権限が必要となっています。

さて、これでイメージとその署名がECRに格納されました。次は、このイメージをデプロイするパイプラインを見ていきましょう。

デプロイパイプライン

デプロイパイプラインの構成は以下のようになっています。

デプロイパイプライン

Sourceステージは CodeCommit からソースコードを取得し、DeployステージはECSに新しいイメージを反映させているだけですので、イメージの署名検証を行っている Verify ステージの内容を見ていきます。

buildspec.yaml

Verifyステージの CodeBuild では、後続の Deploy ステージでECSにイメージをデプロイする前に、正当なビルドプロセスで署名されたイメージかどうかを検証します。

使用する buildspec.yaml の内容は以下の通りです。

version: 0.2

env:
  variables:
    AWS_ACCOUNT_ID: "<AWSアカウントID>"
phases:
  install:
    commands:
      - echo "Installing Cosign..."
      - wget -nv "https://github.com/sigstore/cosign/releases/download/v2.0.0/cosign_2.0.0_amd64.deb" -O cosign_amd64.deb
      # 本来はパッケージの検証が必要
      - dpkg -i cosign_amd64.deb
      - cosign version
  pre_build:
    commands:
      - echo "Logging in to ECR..."
      - IMAGE_DOMAIN="${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com"
      - aws ecr get-login-password --region ${AWS_REGION} | docker login --username AWS --password-stdin ${IMAGE_DOMAIN}
  build:
    commands:
      - echo "Verifying image..."
      # 今回はイメージは1つのみの想定
      - IMAGE_URI="$(jq -r .[0].imageUri imagedefinitions.json)"
      - echo "IMAGE_URL=${IMAGE_URI}"
      - test -n "${IMAGE_URI}"
      - cosign verify --insecure-ignore-tlog --key awskms:///alias/cosign "${IMAGE_URI}"
artifacts:
  files:
    - imagedefinitions.json

ビルドの各フェーズの内容について説明します。

installフェーズ

installフェーズでは、ビルド時と同じく、Cosignをインストールします。

pre_buildフェーズ

pre_buildフェーズでは、Cosignからリポジトリに保管されているイメージ署名を参照するために、ECRにログインしています。

buildフェーズ

buildフェーズで、実際に署名を検証しています。

デプロイ対象のイメージはソースコードの imagedefinitions.json に以下のように記載されており、この中の imageUri からイメージのURIを取得できます。

[{
  "name": "demo",
  "imageUri": "<AWSアカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com/demo@sha256:d21ca72c272893b457c4fe47020131af88eb018a185524a5755001c70d76220f"
}]

取得したイメージURIを使用して、以下のコマンドで署名を検証しています。

cosign verify --insecure-ignore-tlog --key awskms:///alias/<非対称KMSキーのエイリアス> <検証対象イメージのURI>

ビルド時に Transparency Log の記録をスキップしたため、 --insecure-ignore-tlog オプションを指定して検証もスキップしています。Rekor にログを記録している場合は、このオプションは不要です。

サービスロール権限

署名検証用のCodeBuildのサービスロールには、デフォルトで作成される権限に加えて、以下のIAMポリシーを付与します。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "ecr:GetAuthorizationToken",
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "ecr:GetDownloadUrlForLayer",
                "ecr:BatchGetImage"
            ],
            "Resource": [
                "arn:aws:ecr:<リージョン>:<AWSアカウントID>:repository/<リポジトリ名>"
            ]
        },
        {
            "Effect": "Allow",
            "Action": [
                "kms:DescribeKey",
                "kms:GetPublicKey"
            ],
            "Resource": [
                "arn:aws:kms:<リージョン>:<AWSアカウントID>:key/<署名用のキーID>"
            ]
        }
    ]
}

ECRに対する参照権限は、リポジトリに保管されているイメージデータを参照するためのものです。

KMSに対する権限の目的は、Cosignが署名を検証するためにパブリックキーを取得することです。エイリアスの解決のために kms:DescribeKey 、パブリックキー自体の取得のために kms:GetPublicKey の権限が必要となっています。

デプロイ

署名の検証に失敗すると、コマンドが 0以外のコードで終了するため、Verifyステージが失敗し、後続のデプロイには進まなくなります。
これにより、イメージの署名が検証できた場合のみデプロイさせることができます。

まとめ

Cosignを使用することで、AWS環境のビルドパイプラインの中に、イメージに対する署名と検証を容易に組み込むことができました。
次回は、別のプラクティスであるSBOM (Software Bill of Materials)の生成と保存を試します。

CTCは、AWSのビジネス利活用に向けて、お客様のステージに合わせた幅広い構築・運用支援サービスを提供しています。
経験豊富なエンジニアが、ワンストップかつ柔軟にご支援します。
ぜひ、お気軽にお問い合わせください。

お問い合わせ

【著者プロフィール】

大塚 聡(おおつか さとし)

伊藤忠テクノソリューションズ株式会社 システムアーキテクト

Webアプリケーションを中心としたシステムの開発者・アーキテクトとして活動中

坂 和久(ばん かずひさ)

TOP>コラム一覧>AWS環境でのコンテナイメージへの署名

pagetop