おい、新人。ちょっとこっちに来てくれ。
昨日、うちのチームが担当しているクラウド基盤のインシデントレビューをやっていたんだが、冷や汗もののログを見つけたんだよ。
開発環境のプライベートコンテナレジストリ(HarborやDocker Registry)に、なんと認証なし(匿名アクセス)で外からグローバルIP経由でアクセスできる状態になっていた。しかも、そこにはDBのパスワードや内部APIのトークンがベタ書きされたプロダクション用のイメージが平然とプッシュされていた。幸い、今回は外部の脆弱性スキャナーのボットがクロールしただけのフェーズ違いだったが、これが本物のAPTグループやランサムウェアの自動化スクリプトだったらどうなっていたか……想像するだけで背筋が凍るだろ?
「Kubernetesを使っているから安全」「プライベートネットワーク内だからパスワードは不要」なんて甘い考えは、今すぐゴミ箱に捨ててくれ。現代のクラウドネイティブ環境において、コンテナレジストリの認証不備とイメージの改ざんは、インフラ全体の「全権掌握」を意味する最大の急所の一つだ。
今日は、攻撃者がどのようにその盲点を突き、私たちがどうやってそれを完璧に叩き潰すのか、実務で使えるコードと設定を交えて徹底的に叩き込んでやる。心して聞け。
—
1. 攻撃者の視点:レジストリの踏み台化とサプライチェーンハイジャック
レッドチームの視点から言わせてもらうと、コンテナレジストリは「宝の山」であり「最高の侵入経路(踏み台)」だ。攻撃者はターゲットのクラウド環境に対して、以下のようなシナリオで容赦なく牙を剥く。
1-1. 認証不備(Anonymous Pull/Push)の突撃
多くの開発現場では、CI/CDパイプライン(GitHub ActionsやGitLab CI)からのデプロイを簡略化したいがために、レジストリのアクセス制御を緩くしがちだ。
攻撃者は、ShodanやCensys、あるいは独自のスキャナーを用いて、ポート 5000 や 443 で稼働するDocker RegistryやHarborのエンドポイントをスキャンする。
もし、以下のようなエンドポイントへのリクエストが 200 OK を返し、イメージ一覧が返ってきたら、そこがゲームセットの合図だ。
# 攻撃者が実行する、認証なしでのレジストリ探索コマンドの例
curl -X GET "https://registry.example.com/v2/_catalog"
この脆弱性により、攻撃者は社内の非公開ソースコードや環境変数、DB接続文字列をごっそり盗み出す(情報漏洩)。さらに最悪なのは、「悪意あるコードを仕込んだ改ざんイメージを、正規のタグ名で上書きプッシュする」というサプライチェーン攻撃だ。
1-2. イメージ改ざん(Image Poisoning)の恐怖
仮にレジストリにアクセス権限(パスワード)が必要だったとしても、開発者の端末がマルウェアに感染してクレデンシャルが盗まれたり、CI/CDの権限管理がガバガバだったりすれば、攻撃者は容易にイメージをすり替えることができる。
Webアプリケーションのベースイメージ(例えば node:18-alpine や python:3.10)や、社内共通のミドルウェアイメージにバックドア(リバースシェルを仕込んだ entrypoint.sh など)を混入させ、そのままレジストリにプッシュする。
Kubernetesなどのオーケストレーターが、その汚染されたイメージを「最新版」として本番環境に自動デプロイしてしまった瞬間、社内ネットワークへの永続的な足場(Persistence)が完成するというわけだ。
「じゃあ、どうやってこれを防ぐのか?」
ここからが本題だ。アクセス制御の厳格化はもちろんのこと、「仮にイメージが改ざんされても、デプロイ時に拒絶する仕組み(署名検証)」を強制しなければならない。
—
2. 対策の核心:Cosignを用いたコンテナイメージの署名と検証
イメージの改ざんを防ぐ決定打が、Sigstoreの Cosign を使った暗号学的署名と検証だ。
簡単に言うと、「このコンテナイメージは、信頼できる公式のCI/CDパイプラインがビルドしたものであり、途中で一バイトたりとも改ざんされていません」という証明書(デジタル署名)をイメージに付与し、Kubernetes側でそれを検証してからでないと起動させない、という仕組みだ。
口で言うのは簡単だが、実際に手を動かしてパイプラインとインフラに組み込んでいこう。
2-1. 【開発・CI/CD側】ビルドと署名の自動化(GitHub Actionsの例)
まずは、イメージをビルドしてレジストリにプッシュし、その直後にCosignで秘密鍵を使って署名を行うGitHub Actionsワークフローの設定例だ。
name: Build, Push and Sign Container Image
on:
push:
branches: [ main ]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build-and-sign:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
id-token: write # OIDC認証やCosignの鍵レス署名に必要
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log into Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
- name: Build and push Docker image
id: build-and-push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
# --- ここからがセキュリティの要:Cosignによる署名 ---
- name: Install Cosign
uses: sigstore/cosign-installer@v3.5.0
- name: Sign the published Docker image
env:
# ダイジェスト(SHA256ハッシュ)付きのイメージに対して署名を行うことで、タグの付け替え攻撃を防ぐ
TAGS: ${{ steps.meta.outputs.tags }}
DIGEST: ${{ steps.build-and-push.outputs.digest }}
run: |
# 実際の運用では秘密鍵(cosign.key)またはOIDC(Keyless)を使用する
# ここでは分かりやすく環境変数にパスワードを設定した秘密鍵を用いる例
echo "${{ secrets.COSIGN_PRIVATE_KEY }}" > cosign.key
for TAG in ${TAGS}; do
# タグからイメージ名部分を抽出し、ダイジェストを指定して署名
IMAGE="${TAG%:*}"
cosign sign --yes --key cosign.key "${IMAGE}@${DIGEST}"
done
rm -f cosign.key
ここで重要なポイントは、イメージの「タグ(例: latest)」ではなく、「ダイジェスト(例: @sha256:abc123...)」に対して署名を行っている点だ。タグは後から何度でも書き換え可能だが、ダイジェストはコンテンツのハッシュ値であるため、中身が1バイトでも変わればハッシュが一致せず、改ざんが一発で検知できる。
—
2-2. 【インフラ・Kubernetes側】Connaisseur(Admission Controller)による強制検証
レジストリに署名付きイメージを上げても、Kubernetesがそれをチェックせずにデプロイしてしまったら意味がない。Kubernetesの ValidatingWebhookConfiguration またはポリシーエンジン(KyvernoやConnaisseurなど)を使い、「署名のないイメージ、または改ざんされたイメージのPod作成を強制拒否(Deny)」する設定を入れよう。
以下は、Kubernetesネイティブのポリシーエンジンである Kyverno を用いて、特定のレジストリにあるイメージの署名検証を強制するポリシー設定ファイルだ。
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: verify-image-signatures
annotations:
policies.kyverno.io/title: Verify Container Image Signatures
policies.kyverno.io/severity: high
policies.kyverno.io/subject: Pod
description: >-
社内プライベートレジストリにデプロイされるすべてのコンテナイメージに対し、
Cosignによるデジタル署名の検証を強制します。署名のないイメージはデプロイを拒絶します。
spec:
validationFailureAction: Enforce # 違反時は警告ではなく確実にブロックする
background: false
rules:
- name: verify-cosign-signature
match:
any:
- resources:
kinds:
- Pod
verifyImages:
- imageReferences:
- "ghcr.io/my-organization/*" # 検証対象とするレジストリ・リポジトリのパターン
attestors:
- count: 1
entries:
- keys:
publicKeys: |
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0... (ここに公開鍵を配置)
-----END PUBLIC KEY-----
このポリシーをクラスターに適用しておけば、万が一攻撃者がレジストリのアクセス権を奪い、不正なイメージをプッシュしてKubernetesにデプロイしようとしても、KubernetesのAPIサーバーが立ち上がる直前に「署名が無効、または存在しません」と弾き返し、コンテナの起動を完全に阻止してくれる。これが、ゼロトラストインフラストラクチャの基本だ。
—
3. 現場で使えるセキュア設計チェックリスト
最後に、今日から君のプロジェクトですぐに点検・実装してほしいポイントをチェックリストとしてまとめておく。自分の担当システムのインフラを見直してくれ。
1. レジストリの認証確認
- インターネット側から認証なし(匿名)で
docker pullやdocker pushができないよう、アクセス制御(ACL / ネットワークポリシー)が厳格にかかっているか?
2. 最小権限の原則(Least Privilege)
- CI/CDパイプライン用のトークンやクレデンシャルは、特定のレジストリ・特定のイメージリポジトリに対する
Push権限だけに絞られているか?(全権限を持ったマスターキーを使い回していないか?)
3. 脆弱性スキャンの自動化
- レジストリへのプッシュ時に、TrivyやClairなどのスキャナーが自動で走り、CriticalやHighの脆弱性が見つかった場合にビルドやデプロイが失敗するようになっているか?
4. イメージ署名の導入
- 本番環境へデプロイされるイメージについて、Cosign等を用いた署名検証プロセスがCI/CDおよびKubernetes側で義務付けられているか?
セキュリティとは、魔法の盾が一枚あればいいという世界じゃない。アクセス制御という「門番」、脆弱性スキャンという「健康診断」、そして署名検証という「身元証明」の何重もの防壁(ディフェンス・イン・ディープ)を張り巡らせることで初めて、悪意ある攻撃者の侵入を防ぐことができるんだ。
手を動かして実装する中で分からないことがあれば、いつでも俺のところに来い。一緒にコードレビューをしてやる。よし、さっそく今日の作業に戻ろうか。
コメント