概要
Workload Identity Federation + 最小権限IAM
GitHub ActionsのJSONキー認証(secrets.GCP_SA_KEY)を廃止し、OIDCトークンによる短命な権限借用に置き換える。CI用サービスアカウントも roles/editor から artifactregistry.writer / cloudkms.signerVerifier のみに絞る。
イミュータブルダイジェスト固定 + 脆弱性ゲート
:latest 可変タグを廃止し、push直後に取得したダイジェスト(sha256:...)で以降の全工程を固定する。Artifact Registryの脆弱性スキャン結果を確認し、CRITICALがあればビルドを失敗させる。
Binary Authorization × cosign keyless署名
Cloud KMSの署名鍵でBinary Authorizationアテステーションを作成し、GKEクラスタ側で「署名の無いイメージ」のデプロイを拒否する。加えてcosign keyless署名でSigstore Rekor透明性ログにも記録する。
break-glass 監査ログ
ENFORCED_BLOCK_AND_AUDIT_LOG で拒否イベントを必ず監査ログに残し、ログベースメトリクス + アラートで緊急バイパスの発生をリアルタイムに検知・通知する。
問題
ECサイト MOps チームでは、キャンペーン管理API(campaign-api)のコンテナイメージを GitHub Actions でビルドし、Artifact Registry へ push した後、GKE Autopilot クラスタへデプロイしている。以下の GitHub Actions ワークフローと Terraform コードには 7つの設計上の問題 が潜んでいる。
問題点を全て洗い出し、Workload Identity Federation(静的キー廃止)× 最小権限IAM × イミュータブルダイジェスト固定 × 脆弱性スキャンゲート × Cloud KMS署名によるアテステーション(Binary Authorization)× cosign keyless署名(Sigstore)× break-glass監査ログ を考慮した Bad→Good リファクタリングを行ってください。
制約・前提条件
- Terraform 1.8+(
googleプロバイダー 5.x)、GKE Autopilot 1.30+、リージョンasia-northeast1 - CI/CD は GitHub Actions。GitHub リポジトリは
org/campaign-apiの1つに限定してよい - コンテナレジストリは Artifact Registry(
asia-northeast1-docker.pkg.dev) - 現状は GitHub Actions が長期間有効なサービスアカウントJSONキー(
secrets.GCP_SA_KEY)で認証し、そのサービスアカウントにはroles/editorが付与されている - イメージは
:latestの可変タグのみで push・デプロイされており、Artifact Registry の脆弱性スキャン結果は一切確認されないままデプロイに進む - イメージの署名・アテステーションが存在せず、GKE Autopilot クラスタには Binary Authorization ポリシーが設定されていないため、出所不明のイメージでもそのままデプロイできてしまう
悪いコード (Before)
name: build-and-deploy
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# 問題①: 長期間有効な静的JSONキーで認証(WIF未使用)
- name: Authenticate to GCP
run: |
echo "${{ secrets.GCP_SA_KEY }}" > sa-key.json
gcloud auth activate-service-account --key-file=sa-key.json
- name: Build and push
run: |
docker build -t $IMAGE:latest .
# 問題③: latest 可変タグのみ push(ダイジェスト固定なし)
docker push $IMAGE:latest
# 問題④: 脆弱性スキャン結果を一切確認せずデプロイに進む
# 問題⑤: イメージ署名・アテステーションが存在しない
- name: Deploy to GKE
run: |
kubectl set image deployment/campaign-api \
campaign-api=$IMAGE:latest
# 問題②: CI/CD用SAにプロジェクト全体の編集権限
resource "google_project_iam_member" "cicd_sa" {
project = var.project_id
role = "roles/editor"
member = "serviceAccount:${google_service_account.cicd.email}"
}
# 問題⑥: GKE Autopilot クラスタに Binary Authorization 未設定
resource "google_container_cluster" "campaign" {
name = "mops-campaign-cluster"
location = "asia-northeast1"
enable_autopilot = true
# binary_authorization ブロックなし → 任意のイメージをデプロイ可能
}
# 問題⑦: break-glass(緊急バイパス)発生を検知する仕組みが無い
ヒント(段階的開示)
ヒント1 — 方向性
roles/editor)、(2) イメージの検証可能性 — 可変タグ(:latest)によるロールバック・追跡性の欠如、脆弱性スキャン結果を無視したデプロイ、署名・アテステーションの不在、(3) デプロイ時の強制 — GKEクラスタ側にBinary Authorizationポリシーがなく誰でも任意のイメージをデプロイできる状態、緊急時のポリシーバイパス(break-glass)の監査ログ設計の欠如。ソフトウェアサプライチェーンセキュリティは「ビルド時に何を検証したか」と「デプロイ時にそれをどう強制するか」の両輪で設計する必要がある。
ヒント2 — アプローチ
- 問題①:
secrets.GCP_SA_KEY(静的JSONキー)で認証 → Workload Identity Federation(OIDC)でGitHub Actionsのトークンを直接連携し、静的キーを廃止する - 問題②: CI用サービスアカウントに
roles/editor→roles/artifactregistry.writerとroles/cloudkms.signerVerifierのみの最小権限に絞る - 問題③:
:latestの可変タグのみで push・デプロイ → push直後にダイジェスト(sha256:...)を取得し、以降の全工程をダイジェスト参照に統一する - 問題④: 脆弱性スキャン結果を確認せずデプロイ → スキャン結果を確認し、CRITICAL重大度があればビルドを失敗させるゲートを挟む
- 問題⑤: 署名・アテステーションが存在しない → Cloud KMSの署名鍵でBinary Authorizationアテステーションを作成し、cosign keylessでSigstore Rekorにも記録する
- 問題⑥: GKEクラスタにBinary Authorizationポリシーがない →
google_binary_authorization_policyでクラスタ単位に「アテステーションが無いイメージは拒否」を強制する - 問題⑦: break-glass運用の監査ログがない → Binary Authorization違反・バイパスをCloud Loggingベースのメトリクス・アラートで検知できるようにする
ヒント3 — コードの骨格
# Binary Authorization ポリシー スケルトン
resource "google_binary_authorization_policy" "policy" {
default_admission_rule {
evaluation_mode = "REQUIRE_ATTESTATION" # 修正⑥
enforcement_mode = "ENFORCED_BLOCK_AND_AUDIT_LOG" # 修正⑦: 監査ログ必須
require_attestations_by = [google_binary_authorization_attestor.build_attestor.name]
}
}
# GitHub Actions スケルトン
- uses: google-github-actions/auth@v2 # 修正①: WIF
with:
workload_identity_provider: ${{ vars.WIF_PROVIDER }}
service_account: campaign-api-cicd@${{ vars.PROJECT_ID }}.iam.gserviceaccount.com
- run: |
docker push $IMAGE:${{ github.sha }}
DIGEST=$(gcloud artifacts docker images describe $IMAGE:${{ github.sha }} \
--format='get(image_summary.digest)') # 修正③: ダイジェスト固定
- run: ./scripts/fail_on_critical_cve.sh "$IMAGE@$DIGEST" # 修正④: 脆弱性ゲート
- run: gcloud beta container binauthz attestations sign-and-create ... # 修正⑤
- run: cosign sign --yes "$IMAGE@$DIGEST" # 修正⑤: keyless署名
問題点分析(7点)
| # | 問題点 | 分類 | 改善方法 |
|---|---|---|---|
| 1 | 静的サービスアカウントJSONキー認証 | CI認証 | Workload Identity Federation(OIDC短命トークン) |
| 2 | CI用SAに roles/editor(過剰権限) | 権限 | artifactregistry.writer + cloudkms.signerVerifier |
| 3 | latest 可変タグのみで push/デプロイ | 追跡性 | ダイジェスト(sha256)固定 |
| 4 | 脆弱性スキャン結果を無視 | 検証 | CRITICAL遮断ゲート |
| 5 | 署名・アテステーションが存在しない | 検証可能性 | KMS署名アテステーション + cosign keyless |
| 6 | クラスタにBinary Authorization未設定 | 強制 | google_binary_authorization_policy |
| 7 | break-glass監査ログの欠如 | 監査 | ログベースメトリクス + アラート |
サプライチェーン図 — Bad vs Good の変換フロー
模範解答
name: build-and-deploy
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Authenticate to GCP # 問題①
run: |
echo "${{ secrets.GCP_SA_KEY }}" > sa-key.json
gcloud auth activate-service-account --key-file=sa-key.json
- name: Build and push
run: |
docker build -t $IMAGE:latest .
docker push $IMAGE:latest # 問題③
- name: Deploy to GKE # 問題④⑤: スキャン確認・署名なし
run: |
kubectl set image deployment/campaign-api \
campaign-api=$IMAGE:latest
# .github/workflows/build-and-deploy.yml
name: build-and-deploy
on:
push:
branches: [main]
permissions:
id-token: write # 修正①: WIFのOIDCトークン発行に必要
contents: read
env:
IMAGE: asia-northeast1-docker.pkg.dev/${{ vars.PROJECT_ID }}/mops/campaign-api
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# 修正①: Workload Identity Federation(静的キーファイル廃止)
- id: auth
uses: google-github-actions/auth@v2
with:
workload_identity_provider: ${{ vars.WIF_PROVIDER }}
service_account: campaign-api-cicd@${{ vars.PROJECT_ID }}.iam.gserviceaccount.com
- uses: google-github-actions/setup-gcloud@v2
- name: Configure Docker for Artifact Registry
run: gcloud auth configure-docker asia-northeast1-docker.pkg.dev
- name: Build and push(コミットSHAタグ + ダイジェスト取得)
id: build
run: |
docker build -t "$IMAGE:${{ github.sha }}" .
docker push "$IMAGE:${{ github.sha }}"
# 修正③: 以降は可変タグではなくダイジェストで一意に固定する
DIGEST=$(gcloud artifacts docker images describe \
"$IMAGE:${{ github.sha }}" --format='get(image_summary.digest)')
echo "digest=$DIGEST" >> "$GITHUB_OUTPUT"
# 修正④: 脆弱性スキャン結果を確認し、CRITICALがあればビルドを失敗させる
- name: Vulnerability scan gate
run: |
CRITICAL_COUNT=$(gcloud artifacts docker images describe \
"$IMAGE@${{ steps.build.outputs.digest }}" \
--show-package-vulnerability \
--format='value(package_vulnerability_summary.severityCount.CRITICAL)')
if [ "${CRITICAL_COUNT:-0}" -gt 0 ]; then
echo "::error::CRITICAL 脆弱性が ${CRITICAL_COUNT} 件検出されました。デプロイを中止します。"
exit 1
fi
# 修正⑤: Binary Authorization アテステーション作成(Cloud KMS署名)
- name: Create Binary Authorization attestation
run: |
gcloud beta container binauthz attestations sign-and-create \
--artifact-url="$IMAGE@${{ steps.build.outputs.digest }}" \
--attestor="projects/${{ vars.PROJECT_ID }}/attestors/campaign-api-build-attestor" \
--keyversion="projects/${{ vars.PROJECT_ID }}/locations/asia-northeast1/keyRings/binauthz-keyring/cryptoKeys/campaign-api-attestor-key/cryptoKeyVersions/1"
# 修正⑤: cosign keyless署名(Sigstore Rekor 透明性ログへの公開記録)
- name: Sign image with cosign (keyless)
uses: sigstore/cosign-installer@v3
- run: cosign sign --yes "$IMAGE@${{ steps.build.outputs.digest }}"
# 修正③: 可変タグ(:latest)ではなくダイジェストでデプロイ
- name: Deploy to GKE (digest pinned)
run: |
gcloud container clusters get-credentials mops-campaign-cluster \
--region asia-northeast1 --project "${{ vars.PROJECT_ID }}"
kubectl set image deployment/campaign-api \
campaign-api="$IMAGE@${{ steps.build.outputs.digest }}"
resource "google_project_iam_member" "cicd_sa" {
project = var.project_id
role = "roles/editor" # 問題②
member = "serviceAccount:${google_service_account.cicd.email}"
}
resource "google_container_cluster" "campaign" {
name = "mops-campaign-cluster"
location = "asia-northeast1"
enable_autopilot = true
# 問題⑥: binary_authorization ブロックなし
}
# 問題⑦: break-glass検知の仕組みが無い
# ── Workload Identity Federation for GitHub Actions ──────────
resource "google_iam_workload_identity_pool" "github" {
project = var.project_id
workload_identity_pool_id = "github-actions-pool"
}
resource "google_iam_workload_identity_pool_provider" "github" {
project = var.project_id
workload_identity_pool_id = google_iam_workload_identity_pool.github.workload_identity_pool_id
workload_identity_pool_provider_id = "github-provider"
attribute_mapping = {
"google.subject" = "assertion.sub"
"attribute.repository" = "assertion.repository"
}
attribute_condition = "assertion.repository == 'org/campaign-api'" # 修正①
oidc {
issuer_uri = "https://token.actions.githubusercontent.com"
}
}
resource "google_service_account" "cicd" {
project = var.project_id
account_id = "campaign-api-cicd"
display_name = "campaign-api CI/CD (WIF)"
}
# 修正②: roles/editor ではなく最小権限のみ付与
resource "google_project_iam_member" "cicd_artifact_writer" {
project = var.project_id
role = "roles/artifactregistry.writer"
member = "serviceAccount:${google_service_account.cicd.email}"
}
resource "google_project_iam_member" "cicd_kms_signer" {
project = var.project_id
role = "roles/cloudkms.signerVerifier"
member = "serviceAccount:${google_service_account.cicd.email}"
}
resource "google_service_account_iam_member" "wif_binding" {
service_account_id = google_service_account.cicd.name
role = "roles/iam.workloadIdentityUser"
member = "principalSet://iam.googleapis.com/${google_iam_workload_identity_pool.github.name}/attribute.repository/org/campaign-api"
}
# ── Cloud KMS(Binary Authorization アテステーション署名鍵)──────
resource "google_kms_key_ring" "binauthz" {
project = var.project_id
name = "binauthz-keyring"
location = "asia-northeast1"
}
resource "google_kms_crypto_key" "attestor_key" {
name = "campaign-api-attestor-key"
key_ring = google_kms_key_ring.binauthz.id
purpose = "ASYMMETRIC_SIGN"
version_template { algorithm = "EC_SIGN_P256_SHA256" }
}
data "google_kms_crypto_key_version" "attestor_key_version" {
crypto_key = google_kms_crypto_key.attestor_key.id
}
# ── Binary Authorization Attestor ──────────────────────────────
resource "google_container_analysis_note" "attestor_note" {
project = var.project_id
name = "campaign-api-attestor-note"
attestation_authority {
hint { human_readable_name = "campaign-api build attestor" }
}
}
resource "google_binary_authorization_attestor" "build_attestor" {
project = var.project_id
name = "campaign-api-build-attestor"
attestation_authority_note {
note_reference = google_container_analysis_note.attestor_note.name
public_keys {
id = data.google_kms_crypto_key_version.attestor_key_version.id
pkix_public_key {
public_key_pem = data.google_kms_crypto_key_version.attestor_key_version.public_key[0].pem
signature_algorithm = "ECDSA_P256_SHA256"
}
}
}
}
# ── Binary Authorization Policy(修正⑥)───────────────────────
resource "google_binary_authorization_policy" "policy" {
project = var.project_id
global_policy_evaluation_mode = "ENABLE"
default_admission_rule {
evaluation_mode = "ALWAYS_DENY"
enforcement_mode = "ENFORCED_BLOCK_AND_AUDIT_LOG"
require_attestations_by = []
}
cluster_admission_rules {
cluster = "asia-northeast1.mops-campaign-cluster"
evaluation_mode = "REQUIRE_ATTESTATION"
enforcement_mode = "ENFORCED_BLOCK_AND_AUDIT_LOG" # 修正⑦
require_attestations_by = [google_binary_authorization_attestor.build_attestor.name]
}
}
# ── GKE Autopilot(クラスタ側でも強制)──────────────────────────
resource "google_container_cluster" "campaign" {
project = var.project_id
name = "mops-campaign-cluster"
location = "asia-northeast1"
enable_autopilot = true
binary_authorization {
evaluation_mode = "PROJECT_SINGLETON_POLICY_ENFORCE" # 修正⑥
}
}
# 修正⑦: break-glass(拒否イベント)検知
resource "google_logging_metric" "binauthz_denials" {
project = var.project_id
name = "binauthz-denial-events"
filter = "resource.type=\"k8s_cluster\" AND protoPayload.response.message:\"binary authorization\""
metric_descriptor {
metric_kind = "DELTA"
value_type = "INT64"
}
}
resource "google_monitoring_alert_policy" "binauthz_denial_alert" {
project = var.project_id
display_name = "Binary Authorization 拒否イベント検知"
combiner = "OR"
conditions {
display_name = "拒否イベント発生"
condition_threshold {
filter = "metric.type=\"logging.googleapis.com/user/${google_logging_metric.binauthz_denials.name}\""
comparison = "COMPARISON_GT"
threshold_value = 0
duration = "0s"
}
}
notification_channels = [var.slack_notification_channel_id]
}
| 問題 | 修正内容 | 効果 |
|---|---|---|
| ① 静的JSONキー | Workload Identity Federation | 短命OIDCトークン、キー漏洩・ローテーション運用が不要に |
| ② roles/editor | artifactregistry.writer + cloudkms.signerVerifier | パイプライン侵害時の被害範囲を限定 |
| ③ latest可変タグ | sha256ダイジェスト固定 | デプロイ内容の一意性・確実なロールバックを保証 |
| ④ スキャン結果無視 | CRITICAL遮断ゲート | 既知の重大脆弱性を含むイメージの本番到達を阻止 |
| ⑤ 署名・証明なし | KMS署名アテステーション + cosign keyless | ビルドパイプライン経由の証明 + 公開透明性ログ記録 |
| ⑥ Binary Authorization未設定 | REQUIRE_ATTESTATION強制 | 未署名・出所不明イメージのPod作成を機械的に拒否 |
| ⑦ break-glass監査ログなし | ログベースメトリクス + アラート | 緊急バイパスの発生をリアルタイム検知・通知 |
ポイント解説
:latest のようなタグは同じ名前で中身が変わるため、障害発生時に「1つ前の状態」を指し示す方法がない。ダイジェスト固定は、CI/CDのトレーサビリティだけでなく障害対応の即応性にも直結する。ENFORCED_BLOCK_AND_AUDIT_LOG のように「バイパスは許容するが必ず記録・通知する」設計の方が、実運用の柔軟性と監査可能性を両立できる。実務への応用
MOpsチームの campaign-api に限らず、GKE Autopilot上で稼働する全てのマイクロサービス(coupon-api, notification-worker 等)に対して、同一のBinary Authorizationアテストー・ポリシーを横展開できる。特に決済・注文データを扱うサービスでは、CRITICAL脆弱性ゲートとアテステーション必須化を最優先で適用し、社内ツールなど影響範囲が小さいサービスから段階的にENFORCED化していくのが現実的なロールアウト順序になる。
Workload Identity Federationの attribute_condition でリポジトリを厳密に限定する設計は、GitHub Organization内の他リポジトリが誤って(あるいは侵害されて)同じサービスアカウントの権限を借用してしまう横展開リスクを防ぐ。複数リポジトリでCI/CDを展開する場合は、リポジトリごとに専用のサービスアカウント・WIFプロバイダー条件を用意し、権限の混在を避けるべきである。
DRYRUN_AUDIT_LOG_ONLY(拒否せずログのみ記録)で1〜2週間運用し、実際に拒否されうるイメージが無いことを確認してから ENFORCED_BLOCK_AND_AUDIT_LOG に切り替えるのが安全。
今日のまとめ
次のステップ
- 発展問題: 現在は単一クラスタ(
mops-campaign-cluster)のみを対象にしているBinary Authorizationポリシーを、複数クラスタ(本番・ステージング)で異なる強制レベル(本番はENFORCED_BLOCK_AND_AUDIT_LOG、ステージングはDRYRUN_AUDIT_LOG_ONLY)に分ける設計を検討せよ。また、SLSA(Supply-chain Levels for Software Artifacts)フレームワークのレベル定義と照らし合わせ、今回の実装がどのレベルに相当するかを評価せよ - 参考: Binary Authorization 公式ドキュメント(アテストー・ポリシー設計)/ Workload Identity Federation for GitHub Actions(
google-github-actions/auth)/ Sigstore cosign keyless signing / Artifact Registry 脆弱性スキャン / SLSA フレームワーク