おい、ちょっと手を止めてこっちを向いてくれ。
先日、とあるクライアントのKubernetesクラスタで、見事にコンテナが踏み台にされるインシデントが発生した。原因を調べたら、「いつものお約束」が揃い踏みだったよ。脆弱性マシマシの古いベースイメージ、root権限で平然と動くアプリケーション、そして野良状態で放置されたマニフェストファイル。攻撃者は、たった一本のAPI脆弱性からコンテナのランタイムへ侵入し、ホストのカーネルを揺さぶり、見事にプライベートサブネットの奥深くへと横展開(ラテラルムーブメント)していきやがった。
「Docker使ってコンテナ化してるから安全です」なんて寝言は、もう令和の現場では通用しない。今回は、クラウドネイティブ環境の要である「コンテナセキュリティとランタイム保護」について、攻撃者の視点と、それを完封するための実務的な設定を叩き込んでやる。
—
1. 攻撃者はどこを突くのか?(コンテナ脱出のリアルな脅威)
多くの開発者は、「コンテナ=仮想マシンと同等の隔離空間」と誤解している。だが、現実はそうじゃない。コンテナはホストOSのカーネルを共有している。つまり、コンテナ内のプロセスがホスト側に牙を剥く余地はいくらでもある。
典型的な攻撃シナリオを見てみよう。
1. 脆弱なイメージの利用: 脆弱性(CVE)を抱えた古いランタイムライブラリを含むベースイメージをそのままデプロイする。
2. 初期侵入: アプリケーションの脆弱性(RCE等)を突いてコンテナ内にシェルを奪う。
3. 権限昇格とコンテナ脱出: コンテナが privileged: true で動いていたり、不適切な Capabilities(例: CAP_SYS_ADMIN)が付与されていたりすると、コンテナ内からホストのマウント名前空間を操作し、ホストのファイルを直接書き換えられる。
4. ホスト乗っ取り: /proc や /sys を通じてホストのカーネルインジェクションを行い、クラスタ全体が陥落する。
この悪夢を防ぐためには、「イメージスキャンによる事前防御」と「ランタイム保護による振る舞い検知」の二段構えが絶対に必要だ。
—
2. イメージスキャンとベースイメージの厳格化
まず大前提として、CI/CDパイプラインの段階でゴミのような脆弱性を持つイメージを本番環境へ弾き返さなければならない。だが、スキャンツールを導入するだけでは不十分だ。「どのベースイメージを選ぶか」という設計思想が命取りになる。
軽量かつ攻撃表面積(アタックサーフェス)を最小限にするために、ディストリビューションレスイメージ(Distroless)や Alpine Linux を適切に選択し、さらにマルチステージビルドを活用してビルドツールや不要なパッケージを本番イメージに残さないことだ。
以下に、セキュアなマルチステージビルドを行う Dockerfile の実例を示す。
# --- ビルドステージ ---
# ビルドには必要なツールが揃った重いイメージを使用
FROM golang:1.22-alpine AS builder
WORKDIR /app
# 依存関係のダウンロードとコンパイル
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o /app/secure-service .
# --- ランタイムステージ ---
# 本番用にはシェルすら含まない極小のディストリビューションレスイメージを採用
FROM gcr.io/distroless/static-debian12:nonroot
WORKDIR /app
# ビルドステージからバイナリのみをコピー
COPY --from=builder /app/secure-service /app/secure-service
# root以外の安全なUID(65532等)で実行するよう指定
USER 65532:65532
# アプリケーションのエントリーポイント
ENTRYPOINT ["/app/secure-service"]
この構成であれば、仮にコンテナ内でRCEを踏まれても、シェル(/bin/sh や /bin/bash)が存在しないため、攻撃者が対話型シェルを起動してスクリプトをダウンロード・実行するアタックパスを物理的に断つことができる。
—
3. Pod Security Standards (PSS) によるランタイムの強制防壁
Kubernetes環境では、かつて使われていた PodSecurityPolicy (PSP) が非推奨となり、現在は組み込みの Pod Security Standards (PSS) および Pod Security Admission (PSA) を使うのがデファクトだ。
現場で絶対に死守すべきルールは、本番ワークロードに対して restricted プロファイル(最も厳格な基準)を強制することだ。これにより、root権限での実行、特権コンテナの作成、ホストネットワークやボリュームの共有などを一網打尽に防げる。
以下に、restricted レベルの基準を完全に満たしたセキュアな Kubernetes マニフェスト(Deployment)のサンプルを示す。
apiVersion: apps/v1
kind: Deployment
metadata:
name: secured-backend-app
namespace: production
labels:
app.kubernetes.io/name: backend
spec:
replicas: 3
selector:
matchLabels:
app: backend
template:
metadata:
labels:
app: backend
spec:
# ホストの名前空間へのアクセスを完全に遮断
hostNetwork: false
hostPID: false
hostIPC: false
containers:
- name: api-server
image: 123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/secure-service:v1.2.0
securityContext:
# rootユーザーでの実行を禁止
runAsNonRoot: true
runAsUser: 65532
runAsGroup: 65532
allowPrivilegeEscalation: false # 特権昇格の防止
# すべてのLinux Capabilitiesを一旦ドロップし、最低限必要なものだけを許可
capabilities:
drop:
- ALL
# ルートファイルシステムを読み取り専用に強制(コンテナ内からの改ざん防止)
readOnlyRootFilesystem: true
resources:
limits:
cpu: "500m"
memory: "512Mi"
requests:
cpu: "100m"
memory: "128Mi"
volumeMounts:
- mountPath: /tmp
name: tmp-dir
volumes:
- name: tmp-dir
emptyDir: {} # 一時ファイル用には一時的なメモリ/ディスク領域のみを許可
この設定のミソは readOnlyRootFilesystem: true だ。アプリケーションが動作中にローカルのソースコードや設定ファイルを書き換えようとしても、すべて弾き返される。攻撃者がマルウェアをローカルに永続化させようとしても、書き込み権限がないため即座に失敗する仕組みだ。
—
4. ランタイム脅威検知(Falcoの活用)
どれだけガチガチに固めても、ゼロディ脆弱性や、アプリケーション層(例: 認証バイパスからの意図しないファイル読込)を突いた攻撃を100%防ぐことはできない。だからこそ、「侵入された後、いかに早く気づき、いかに被害を極小化するか」のランタイム脅威検知が最後の砦になる。
そこで導入すべきなのが、オープンソースのランタイムセキュリティエンジン Falco だ。Falcoは、Linuxカーネルのシステムコール(eBPFやkernel module)を監視し、「コンテナ内でシェルが起動した」「コンテナから勝手に外向きの不審な通信が発生した」「機密ファイル /etc/shadow が読み込まれた」といった異常な振る舞いをリアルタイムで検知・アラートする。
例えば、本番コンテナ内で予期せぬシェルが起動された際のカスタム検知ルール(falco_rules.local.yaml)は以下のように記述できる。
- rule: Unexpected Shell Spawned In Container
desc: 検証済みイメージ以外のコンテナ内において、シェルプロセスが起動されたことを検知します。
condition: spawned_process and container and (proc.name = sh or proc.name = bash or proc.name = zsh)
output: "不審なシェル起動を検知 (user=%user.name command=%proc.cmdline container_id=%container.id container_name=%container.name image=%container.image.repository)"
priority: CRITICAL
tags: [container, execution, security_incident]
こうしたルールを仕込んでおき、SlackやPagerDuty、あるいはSecurity Hub等のSIEMへ連携させることで、深夜であっても「コンテナが踏み抜かれた瞬間」にエンジニアのスマホへアラートを飛ばす体制を構築できる。
—
5. チーフからの現場の心得
セキュリティ対策というのは、一度設定して終わり、というものではない。開発チームが新しい機能を追加するたびに、コンテナの肥大化や設定の緩みが再発する。
だからこそ、インフラやセキュリティチームだけに押し付けるのではなく、CI/CDパイプラインの中に自動テストと同様のセキュリティゲート(Trivyによるイメージスキャン、ConftestやKubeconformによるマニフェスト検証)を組み込むこと。これが、疲弊しないセキュアな開発組織を作る唯一にして最大の近道だ。
面倒くさい手順を嫌うエンジニアほど、一度インシデントを踏むと痛い目をみる。後輩の君たちには、ぜひ「最初から壊れにくい(セキュアな)設計」を当たり前に実装できるプロフェッショナルであってほしい。さて、手を動かしてマニフェストの見直しにかかろうか。
コメント