コンテナサプライチェーンの闇:署名なきイメージは「トロイの木馬」である
おい、最近のKubernetes環境の構築はどうだい?「とりあえず公式イメージをPullして、Helmでデプロイすれば動く!」なんて浮かれていないか?
インフラエンジニアやSaaS開発者からよく聞くセリフだが、セキュリティチーフの俺から言わせれば、それは「中身の分からないカプセル剤を、医者の診断もなしに一気飲みしている」ようなものだ。
近年のサプライチェーン攻撃の手口は巧妙を極めている。攻撃者は、Docker HubやGitHub Container Registry(GHCR)などの公開レジストリにある人気のあるベースイメージ(AlpineやUbuntu、あるいは一般的なミドルウェア)のタグを乗っ取り、あるいは同名で巧妙に偽装した悪意あるイメージ(タイポスクワッティングなど)を紛れ込ませる。
もし、その「毒入りイメージ」を君のプロダクション環境が知らずにデプロイしてしまったらどうなるか?コンテナが立ち上がった瞬間にリバースシェルが外部のC2サーバーへ繋がり、内部ネットワークへの横展開(Lateral Movement)が始まる。WAFやクラウドIAMをどれだけ厳重に固めていこうとも、コンテナのランタイム自体が乗っ取られてしまえば、城壁の内側から門を開け放たれたも同然なのだ。
この脅威を防ぐ唯一にして最強の盾が、「コンテナイメージの暗号署名(Cosign)」と、「Admission Controllerによる強制検証(Policy Controller / Kyverno等)」である。
今回は、現場の泥臭いインシデントを防ぐために、署名なき野良イメージのデプロイをK8sクラスターレベルで完全にブロックする実務的な要塞化手法を伝授しよう。
—
1. 攻撃者が好む「汚染されたイメージ」のデプロイメント・リスク
従来の脆弱性スキャナー(TrivyやClairなど)は、確かにCVEデータベースに基づいた既知の脆弱性を見つけるのには役立つ。しかし、「攻撃者が意図的にバックドアを仕込んだ、CVEが存在しないゼロデイの改ざんコード」や、「レジストリ上で密かにすり替えられたイメージ」をスキャナーだけで検知するのは不可能に近い。
ここで、Kubernetesにおける一般的な脆弱なデプロイメント設定を見てみよう。
# 【危険な例】署名検証を行わない野良イメージのデプロイメント
apiVersion: apps/v1
kind: Deployment
metadata:
name: vulnerable-app
namespace: production
spec:
replicas: 3
selector:
matchLabels:
app: backend
template:
metadata:
labels:
app: backend
spec:
containers:
- name: api-server
# 誰がビルドしたか分からない、あるいはタグが改ざんされた可能性のある外部イメージ
image: docker.io/suspect-vendor/app-backend:v1.0.0
ports:
- containerPort: 8080
このマニフェストを適用すると、Kubernetesはイメージの正当性を一切検証せず、指定されたURLからホイホイとイメージをダウンロードしてコンテナを起動してしまう。ここに、サプライチェーン攻撃の致命的な隙がある。
—
2. 徹底防御:Cosignによるイメージ署名とKyvernoによる強制検証
このリスクを根絶するためには、以下の2ステップをパイプラインとクラスターに強制する。
1. CI/CDパイプライン(GitHub Actions等)での署名: 開発者が安全にビルドしたイメージに対し、秘密鍵を使ってCosignで暗号署名を付与し、レジストリにプッシュする。
2. Kubernetes(Admission Controller)での検証: クラスター側(今回は柔軟性の高いKyvernoを使用)で、デプロイしようとしているイメージに「信頼できる公開鍵による正しい署名」が存在するかをチェックし、ない場合はAPIリクエスト自体を拒否(Deny)する。
ステップ①:CI/CDでのCosign署名スクリプト(GitHub Actions等での実装例)
まずは、イメージをビルド・プッシュした後に、シークレット(秘密鍵)を使って署名を行うPythonスクリプト、あるいはシェルスクリプトの断片だ。実務ではこれをCIパイプラインに組み込む。
#!/bin/bash
set -euo pipefail
# 環境変数の定義
IMAGE="ghcr.io/my-org/secure-backend:v1.0.0"
COSIGN_PRIVATE_KEY_PATH="./cosign.key"
echo "[INFO] コンテナイメージのビルドとプッシュが完了しました: ${IMAGE}"
# 秘密鍵を用いたイメージの署名
# ※実環境ではパスワード(COSIGN_PASSWORD)をGitHub Secrets等から安全に渡すこと
cosign sign \
--key "${COSIGN_PRIVATE_KEY_PATH}" \
"${IMAGE}"
echo "[SUCCESS] イメージへの暗号署名が正常に完了しました。"
ステップ②:KyvernoによるAdmission Controllerの設定(完全防御の要)
次に、Kubernetesクラスター側で「署名のないイメージのデプロイを物理的に許さない」ためのポリシーを設定する。Kyvernoを使用すると、YAML定義だけで強力なポリシーを適用できる。
以下のマニフェストをクラスターに適用せよ。
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: enforce-image-signature-cosign
spec:
# 検証に失敗した場合、デプロイ(Podの作成)を即座にブロックする
validationFailureAction: Enforce
background: false
rules:
- name: verify-cosign-signature
match:
any:
- resources:
kinds:
- Pod
verifyImages:
- imageReferences:
- "ghcr.io/my-org/*" # 自社組織のレジストリ配下のイメージを対象とする
attestors:
- count: 1
entries:
- secret:
name: cosign-pub-secret # クラスター内に配置した公開鍵のシークレット
namespace: kyverno
# 署名の検証に成功しなければエラーとする
mutateDigest: true
このポリシーを適用しておけば、仮に開発者や外部の攻撃者がどれだけ巧妙にマニフェストを書き換えてghcr.io/my-org/secure-backend:v1.0.0をデプロイしようとしても、レジストリ上に正当な署名(Attestation)が紐付いていない限り、KubernetesのAPIサーバーは以下のようなエラーを返し、デプロイを門前払いする。
> admission webhook "validate.kyverno.spec" denied the request: policy enforce-image-signature-cosign failed: ... no matching signatures
—
3. 現場のセキュリティチーフからの実践的なアドバイス
この仕組みを導入する際、現場のエンジニアからよく「デプロイの手間が増える」「緊急時のホットフィックスで面倒くさい」という不満の声が上がる。だが、思い出してほしい。セキュリティの利便性を過度に追うことは、自らバックドアを歓迎しているようなものだ。
運用上のトラブルを防ぐための実務的なTipsをいくつか授けておこう。
- 鍵管理の厳格化:
Cosignの秘密鍵(cosign.key)は、絶対にGitリポジトリにコミットしてはならない。HashiCorp VaultやAWS KMS、GitHub Actions Secretsなどのセキュアなストレージに厳重に保管し、アクセス権を最小限に絞ること。 - ステージング環境での段階的適用: 最初から
validationFailureAction: Enforce(強制拒否)にするのが怖いなら、最初はAuditモードにしておき、監査ログに違反がどのように記録されるかを確認してから移行するとスムーズだ。 - サードパーティ製イメージの扱い: 公式のRedisやPostgreSQLなどのイメージをそのまま使う場合も、自社のCIパイプラインで一度プルしてリタグし、自社管理のレジストリにプッシュした上で自社鍵で署名し直す(Re-signing)プロセスを標準化せよ。
コンテナセキュリティの基本は「信用するな、検証せよ(Zero Trust)」だ。誰もが使うオープンなエコシステムだからこそ、自社のクラスターの門番は自分たちの手で堅固に作り上げなければならない。
今日の仕事を終える前に、今一度自社のK8sクラスターで「署名なきイメージが動いていないか」、確認してみることを強くおすすめする。
コメント