CI/CDパイプラインにおけるセキュリティの負債
KubernetesはCI/CDのデファクトスタンダードですが、その内部でDockerイメージをビルドすることは、根深いセキュリティの悩みを生み出します。多くのチームが初期段階で壁にぶつかります。それは、「クラスタノード上ですでに実行されているコンテナの中で、どうやってコンテナイメージをビルドするか」という問題です。
馴染みがあるという理由で、多くの人がDocker-in-Docker (DinD) から使い始めます。長年ローカルでDockerを使用してきたため、ポッド内でデーモンを起動することは最も抵抗の少ない道に感じられます。しかし、以前の勤務先で行った2023年のインフラ監査において、この構成が複数の高レベルなセキュリティアラートを誘発していることが判明しました。ビルドコンテナに特権アクセスを許可することは、実質的にプロダクションクラスタ全体のマスターキーを渡しているのと同じだったのです。
根本原因:なぜ特権コンテナが負債となるのか
問題はDockerの機能の仕組みにあります。Dockerにはバックグラウンドデーモン(dockerd)が必要です。コンテナ内でこのデーモンを実行するには、ファイルシステムのマウントやネットワークインターフェースの管理のために、ホストマシンのカーネルへの深いアクセス権限が必要になります。
これにより、通常は以下の2つのリスクの高い構成のいずれかになります。
- 特権モード (Privileged Mode):
--privileged=trueを設定する方法。これはコンテナとホストノード間のほぼすべてのセキュリティ境界を取り払います。もしビルドスクリプトが1つでも侵害されれば、攻撃者は基盤となる物理マシンまたは仮想マシンのルート権限を手に入れることになります。 - Dockerソケットのバインド: ホストから
/var/run/docker.sockをマウントする方法。これはさらに悪化することもあります。このソケットへのアクセス権を持つプロセスは、ホストのDockerデーモンに対して新しいコンテナの起動、ボリュームの削除、あるいは他の機密性の高いネームスペースへの侵入を命じることができるからです。
マルチテナントクラスタにおいて、これらの構成はいつ事故が起きてもおかしくない状態です。これらは「最小権限の原則」に違反し、効果的な監視がほぼ不可能な攻撃対象領域(アタックサーフェス)を作り出してしまいます。
代替案の比較
DinDの廃止を決定した際、私たちは「デーモン問題」を解決するために設計されたいくつかのツールを評価しました。それぞれに固有のトレードオフがあります。
Docker-in-Docker (DinD)
- メリット: Dockerの全機能セット。開発者にとって学習コストがゼロ。
- デメリット:
privilegedアクセスが必要。ネストされたファイルシステムのオーバーヘッドにより低速。
Dockerソケットのバインド
- メリット: ホストの既存イメージキャッシュを使用するため、非常に高速。
- デメリット: 甚大なセキュリティホール。侵害されたコンテナが1つあるだけでノード全体がダウンする可能性がある。
Buildah
- メリット: デーモン不要。OCI準拠のイメージを作成。
- デメリット: 主にルートレスPodman向けに構築されている。標準的なKubernetes環境では動作が不安定になることがある。
Kaniko
- メリット: デーモン不要。完全にユーザ空間で動作。ホストの特権昇格なしでKubernetes Pod上でネイティブに動作。
- デメリット: デフォルトでローカルキャッシュがない(レイヤーを保存するためにリモートレジストリが必要)。
解決策:なぜKanikoが最も堅牢であると証明されたのか
Kubernetesネイティブなワークフローにおいて、Kanikoは私たちの社内テストで選ばれました。Dockerとは異なり、デーモンを必要としません。代わりに、Kanikoはベースイメージのファイルシステムをコンテナのルートに展開し、Dockerfileのコマンドを実行し、各ステップの後にスナップショットを作成します。そして、これらのスナップショットをレイヤーとしてレジストリに直接プッシュします。
これを50ノードのクラスタに導入した後、ビルドの信頼性は大幅に向上しました。起動に失敗する気まぐれなサイドカーデーモンのトラブルシューティングをする必要もなくなりました。さらに重要なことに、セキュリティチームは24時間以内にプロダクション環境でのパイプライン使用を承認しました。
KubernetesでのKanikoの実装
Kanikoのセットアップには、ソースコードとレジストリの認証情報の2つの主要なコンポーネントが必要です。
1. レジストリ認証情報の構成
Kanikoは、完成したイメージをDocker Hub、GCR、またはECRにプッシュするための権限が必要です。これはKubernetes Secretを通じて処理します。
# 認証情報の準備
AUTH=$(echo -n "my_username:my_password" | base64)
cat <<EOF > config.json
{
"auths": {
"https://index.docker.io/v1/": {
"auth": "$AUTH"
}
}
}
EOF
# Kubernetesに設定を安全に保存
kubectl create secret generic regcred \
--from-file=.dockerconfigjson=config.json \
--type=kubernetes.io/dockerconfigjson
2. Kaniko Podテンプレート
この Pod 定義がビルドを実行します。この例では Dockerfile に ConfigMap を使用していますが、本番の CI/CD セットアップでは、通常 Git ボリュームや PersistentVolume を使用します。
apiVersion: v1
kind: Pod
metadata:
name: kaniko-build
spec:
containers:
- name: kaniko
image: gcr.io/kaniko-project/executor:latest
args:
- "--dockerfile=Dockerfile"
- "--context=dir:///workspace"
- "--destination=your-repo/app-name:v1.2.0"
volumeMounts:
- name: kaniko-secret
mountPath: /kaniko/.docker
- name: source-code
mountPath: /workspace
restartPolicy: Never
volumes:
- name: kaniko-secret
secret:
secretName: regcred
items:
- key: .dockerconfigjson
path: config.json
- name: source-code
configMap:
name: app-source-code
3. キャッシングによるビルド時間の短縮
デフォルトでは、Kanikoはローカルデーモンのキャッシュがないため、Dockerよりも低速です。これは --cache=true フラグを有効にすることで解決できます。私たちのテストでは、専用のキャッシュリポジトリを使用することで、小さなコード変更に対するビルド時間を6分から90秒に短縮できました。
# コンテナスペックにこれらの引数を追加
args:
- "--dockerfile=Dockerfile"
- "--context=dir:///workspace"
- "--destination=your-repo/app-name:v1.2.0"
- "--cache=true"
- "--cache-repo=your-repo/kaniko-cache"
現場からの教訓
移行時に注目すべき点が2つあります。1つ目は、Kanikoはファイルシステムを操作するために自身のコンテナ内でルートとして実行されますが、ホストレベルのルート権限は必要としない点です。これはセキュリティ監査人を納得させる重要な相違点です。
2つ目は、GitLab RunnerやGitHub Actionsでセルフホストランナーを使用している場合、docker build をKanikoコンテナの呼び出しに置き換えるのは非常に簡単である点です。最新のツールのほとんどはこれをネイティブにサポートしています。移行には通常1日の午後ほどの時間がかかりますが、コンテナの脱出(container escape)やノードの侵害といった絶え間ない不安を解消できます。デーモンレスアーキテクチャを採用することで、Kubernetesが構築された際の「セキュリティファースト」の原則にCI/CDを適合させることができます。

