Certified Kubernetes Application Developer (CKAD) #15 SecurityContext と Capabilities: runAsUser, fsGroup, readOnly rootfs

#14 ServiceAccount と RBAC でコンテナが Kubernetes API に対して何ができるか を制限したなら、今回はそれよりも一段下の コンテナプロセスそのものが Linux でどの権限で動くか を制限します。デフォルト値をそのままにしておくと、多くのイメージは root で実行され、ルートファイルシステムに好きなだけ書き込めてしまいます。攻撃者がコンテナを掌握すれば、その権限をそのまま引き継ぎます。

securityContext は、コンテナが どの UID/GID で実行されるかルートファイルシステムに書き込めるかどの Linux カーネル機能 (capability) を持つか を宣言で制御するフィールドです。CKAD では「このコンテナを非 root で実行せよ」「NET_ADMIN capability だけを追加せよ」「ルートファイルシステムを読み取り専用にせよ」といった作業として直接出題されます。採点スクリプトが id やマニフェストのフィールドで結果を検査するため、フィールドの位置とスペルを正確に知ることが点数を分けます。

securityContext は 2 つの層に付く #

securityContextPod レベルコンテナレベル の両方に宣言できます。この 2 つは位置も異なり、適用範囲も異なります。

位置パス適用範囲
Pod レベルspec.securityContextPod 内のすべてのコンテナのデフォルト値
コンテナレベルspec.containers[].securityContext該当コンテナにのみ適用

両方の位置に同じフィールドがある場合、コンテナレベルが Pod レベルを上書きします (override)。つまり、Pod レベルで共通ポリシーを敷いておき、特定のコンテナだけコンテナレベルで例外を与える形で使います。

apiVersion: v1
kind: Pod
metadata:
  name: ctx-demo
spec:
  securityContext:          # Pod レベル: すべてのコンテナのデフォルト値
    runAsUser: 1000
    runAsGroup: 3000
    fsGroup: 2000
  containers:
    - name: app
      image: busybox:1.36
      command: ["sh", "-c", "sleep 3600"]
      securityContext:      # コンテナレベル: このコンテナだけ上書き
        runAsUser: 2000

上の例では app コンテナのプロセスはコンテナレベルが優先されるため UID 2000 で実行されます。同じ Pod に別のコンテナがあったなら、そのコンテナは Pod レベルの値である UID 1000 に従います。一方、fsGroup のようにコンテナレベルに存在しないフィールド (fsGroup は Pod レベル専用) は、Pod レベルの値がそのまま適用されます。

もう 1 つ重要な区別があります。runAsUsercapabilities のように一部のフィールドは コンテナレベルにのみ あり、fsGroupsupplementalGroups のように一部のフィールドは Pod レベルにのみ あります。どのフィールドがどの層に属するか迷ったら、kubectl explain で即座に確認します。

k explain pod.spec.securityContext
k explain pod.spec.containers.securityContext

核心フィールド: どのユーザーで実行するか #

最もよく出題される組み合わせは 実行ユーザーの制御 です。

フィールド意味
runAsUser両方プロセスの UID を指定
runAsGroup両方プロセスのデフォルト GID を指定
runAsNonRoot両方true なら root (UID 0) 実行を拒否
fsGroupPodマウントされたボリュームの所有グループ GID
supplementalGroupsPodプロセスに追加で付与する補助 GID のリスト

runAsUser / runAsGroup / runAsNonRoot #

runAsUser はコンテナ内のメインプロセスがどの UID で起動するかを指定します。runAsNonRoot: true はさらに一歩進んで、イメージが root で起動するように作られている場合は コンテナをそもそも起動せずに失敗 させます。非 root 実行を強制したいときに最も確実な防衛線です。

apiVersion: v1
kind: Pod
metadata:
  name: nonroot-demo
spec:
  containers:
    - name: app
      image: busybox:1.36
      command: ["sh", "-c", "id && sleep 3600"]
      securityContext:
        runAsNonRoot: true
        runAsUser: 1000
        runAsGroup: 3000

runAsNonRoot: true だけ置いて runAsUser を空けておくと、イメージのデフォルト USER が root の場合にコンテナが CreateContainerConfigError で起動しません。非 root UID を一緒に指定する方が安全です。

fsGroup: ボリュームの所有グループ #

非 root で実行すると、マウントされたボリュームに書き込めない問題がよく起こります。fsGroup を指定すると、Kubernetes が マウント時点でボリュームのグループ所有権をその GID に変え、プロセスにその GID を補助グループとして付与します。その結果、非 root プロセスもボリュームに書き込めるようになります。

apiVersion: v1
kind: Pod
metadata:
  name: fsgroup-demo
spec:
  securityContext:
    runAsUser: 1000
    fsGroup: 2000          # /data のグループ所有が 2000 に変わる
  containers:
    - name: app
      image: busybox:1.36
      command: ["sh", "-c", "touch /data/test && ls -l /data && sleep 3600"]
      volumeMounts:
        - name: scratch
          mountPath: /data
  volumes:
    - name: scratch
      emptyDir: {}

この Pod が起動すると /data のグループが 2000 に設定され、UID 1000 で動くプロセスが touch に成功します。supplementalGroups は似ていますが、ボリュームの所有権を変えず プロセスに補助 GID だけを追加 します。

ファイルシステム権限の制御 #

readOnlyRootFilesystem #

コンテナのルートファイルシステムを読み取り専用にすると、攻撃者が侵入してもバイナリを仕込んだり設定を改ざんしたりしにくくなります。readOnlyRootFilesystem: true はコンテナレベルのフィールドです。

securityContext:
  readOnlyRootFilesystem: true

問題は、多くのアプリが /tmp やキャッシュディレクトリに 書き込み をして初めて正常に動作する点です。このときはルートを読み取り専用にしたまま、書き込みが必要なパスにだけ emptyDir をマウントして回避します。

apiVersion: v1
kind: Pod
metadata:
  name: ro-rootfs
spec:
  containers:
    - name: app
      image: nginx:1.27
      securityContext:
        readOnlyRootFilesystem: true
      volumeMounts:
        - name: tmp
          mountPath: /tmp
        - name: cache
          mountPath: /var/cache/nginx
        - name: run
          mountPath: /var/run
  volumes:
    - name: tmp
      emptyDir: {}
    - name: cache
      emptyDir: {}
    - name: run
      emptyDir: {}

ルートは読み取り専用ですが /tmp/var/cache/nginx/var/runemptyDir なので書き込みが可能です。このパターンは「ルートファイルシステムを読み取り専用にしつつアプリを動作し続けさせよ」という形で出題されます。

allowPrivilegeEscalation #

allowPrivilegeEscalation: false は、プロセスが自分より高い権限を得ること (例: setuid バイナリの実行) を防ぎます。非 root 実行と組み合わせると、権限昇格の経路をもう 1 つ閉じます。コンテナレベルのフィールドです。

securityContext:
  allowPrivilegeEscalation: false

Linux capabilities #

Linux は root の権限を細かく分割した capability 単位で管理します。コンテナランタイムはデフォルトで一部の capability だけを付与しますが、securityContext.capabilities個別の追加 (add)削除 (drop) ができます。このフィールドはコンテナレベル専用です。

securityContext:
  capabilities:
    add: ["NET_ADMIN", "SYS_TIME"]
    drop: ["ALL"]

値は CAP_ プレフィックスを 外して 書きます (例: CAP_NET_ADMIN ではなく NET_ADMIN)。セキュリティのベストプラクティスは、drop: ["ALL"] ですべて削除した後、本当に必要なものだけ add する 最小権限の方式です。

capability用途の例
NET_ADMINネットワークインターフェース・ルーティング・iptables の操作
SYS_TIMEシステムクロックの変更
CHOWNファイル所有権の変更
NET_BIND_SERVICE1024 未満のポートへのバインド

drop と add を一緒に使っても、両者は衝突しません。まずすべて落とした後、明示したものだけ再び付くと理解すれば十分です。

apiVersion: v1
kind: Pod
metadata:
  name: cap-demo
spec:
  containers:
    - name: app
      image: busybox:1.36
      command: ["sh", "-c", "sleep 3600"]
      securityContext:
        capabilities:
          drop: ["ALL"]
          add: ["NET_ADMIN"]

privileged: true の危険性 #

privileged: true はコンテナに ホストのほぼすべての権限 を付与します。すべての capability を有効にし、デバイスアクセスまで開いて、事実上コンテナの隔離を崩します。試験や実務で明示的に要求されない限り、絶対に有効にしません。CKAD では「privileged を無効にせよ」あるいは「最小権限に変えよ」という方向で出るのであって、有効にせよと出ることはまれです。

securityContext:
  privileged: false   # デフォルト値であり推奨値

seccompProfile #

seccompProfile はコンテナが呼び出せるシステムコールを制限します。CKAD では RuntimeDefault プロファイルをかける一行程度を知っていれば十分で、カスタムプロファイルと詳細な運用は CKS の領域です。

securityContext:
  seccompProfile:
    type: RuntimeDefault

結果の検証 #

マニフェストを適用した後は、コンテナの中で実際の実行アイデンティティを確認し、採点基準と合っているか見ます。

# UID/GID と補助グループを確認
k exec ctx-demo -- id

# ユーザー名を確認 (UID だけで名前がなければ数字で表示されることがある)
k exec nonroot-demo -- whoami

# ボリュームのグループ所有権を確認
k exec fsgroup-demo -- ls -ld /data

# 読み取り専用ルートを確認 (書き込みを試みると失敗するのが正常)
k exec ro-rootfs -c app -- touch /test 2>&1 || echo "read-only 確認"

id 出力の uidgidgroups がマニフェストに書いた値と一致すれば、適用は完了です。runAsNonRoot: true なのにコンテナが CreateContainerConfigError で起動するなら、イメージが root で実行されるように作られているサインなので、非 root UID を明示します。

総合例 #

非 root 実行、読み取り専用ルートファイルシステム、最小 capability を一度に適用したマニフェストです。試験が要求する「セキュリティを強化した Pod」の典型的な形です。

apiVersion: v1
kind: Pod
metadata:
  name: hardened
spec:
  securityContext:
    runAsNonRoot: true
    runAsUser: 1000
    fsGroup: 2000
  containers:
    - name: app
      image: nginx:1.27
      ports:
        - containerPort: 8080
      securityContext:
        allowPrivilegeEscalation: false
        readOnlyRootFilesystem: true
        capabilities:
          drop: ["ALL"]
          add: ["NET_BIND_SERVICE"]
      volumeMounts:
        - name: tmp
          mountPath: /tmp
        - name: run
          mountPath: /var/run
  volumes:
    - name: tmp
      emptyDir: {}
    - name: run
      emptyDir: {}

この Pod は非 root UID 1000 で起動し、権限昇格が防がれており、すべての capability を落とした後に 1024 未満のポートへのバインドに必要な NET_BIND_SERVICE だけを再び付けています。ルートは読み取り専用ですが /tmp/var/runemptyDir を付けて動作に支障がありません。

試験ポイント #

  • securityContextPod レベル (spec.securityContext) とコンテナレベル (spec.containers[].securityContext) の 2 か所にあり、コンテナレベルが Pod レベルを上書きします
  • runAsUserrunAsGrouprunAsNonRootcapabilitiesreadOnlyRootFilesystemallowPrivilegeEscalationprivilegedコンテナレベルfsGroupsupplementalGroupsPod レベル 専用です。
  • capability の値は CAP_ プレフィックスを外して書きます。模範解答は drop: ["ALL"] の後に必要なものだけ add です。
  • readOnlyRootFilesystem: true で書き込みが塞がれたら、emptyDir を書き込みパスにマウント して回避します。
  • runAsNonRoot: true だけ置いて非 root UID を与えないと root イメージが起動に失敗することがあるので、runAsUser を一緒に指定します。
  • 検証は k exec -- idk exec -- whoami です。採点前に実際の UID/GID を目で確認します。
  • フィールドの位置が迷ったら k explain pod.spec.securityContextk explain pod.spec.containers.securityContext で即座に確認します。

まとめ #

この記事で押さえたこと:

  • securityContext の 2 つの層。Pod レベルは共通のデフォルト値、コンテナレベルは例外であり、コンテナレベルが優先
  • 実行ユーザーの制御runAsUserrunAsGrouprunAsNonRoot で非 root を強制、fsGroup でボリュームへの書き込み権限を確保
  • ファイルシステムの制御readOnlyRootFilesystem + emptyDir 回避、allowPrivilegeEscalation: false
  • capabilitiesdrop: ["ALL"] の後に最小の addprivileged: true は隔離を崩すので回避
  • 検証k exec -- idwhoami で実際のアイデンティティを確認

次へ: リソース管理 #

コンテナの権限を狭めたので、次はコンテナが どれだけ多くのリソースを使えるか を制御する番です。

#16 リソース管理: requests/limits、QoS class、LimitRange では、CPU・メモリの requestslimits がスケジューリングと OOM に与える影響、その組み合わせで決まる QoS class (Guaranteed・Burstable・BestEffort)、ネームスペース単位のデフォルト値をかける LimitRange と ResourceQuota まで自分で作りながらまとめます。

X