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 つの層に付く #
securityContext は Pod レベル と コンテナレベル の両方に宣言できます。この 2 つは位置も異なり、適用範囲も異なります。
| 位置 | パス | 適用範囲 |
|---|---|---|
| Pod レベル | spec.securityContext | Pod 内のすべてのコンテナのデフォルト値 |
| コンテナレベル | 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 つ重要な区別があります。runAsUser や capabilities のように一部のフィールドは コンテナレベルにのみ あり、fsGroup や supplementalGroups のように一部のフィールドは Pod レベルにのみ あります。どのフィールドがどの層に属するか迷ったら、kubectl explain で即座に確認します。
k explain pod.spec.securityContext
k explain pod.spec.containers.securityContext核心フィールド: どのユーザーで実行するか #
最もよく出題される組み合わせは 実行ユーザーの制御 です。
| フィールド | 層 | 意味 |
|---|---|---|
runAsUser | 両方 | プロセスの UID を指定 |
runAsGroup | 両方 | プロセスのデフォルト GID を指定 |
runAsNonRoot | 両方 | true なら root (UID 0) 実行を拒否 |
fsGroup | Pod | マウントされたボリュームの所有グループ GID |
supplementalGroups | Pod | プロセスに追加で付与する補助 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: 3000runAsNonRoot: 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/run は emptyDir なので書き込みが可能です。このパターンは「ルートファイルシステムを読み取り専用にしつつアプリを動作し続けさせよ」という形で出題されます。
allowPrivilegeEscalation #
allowPrivilegeEscalation: false は、プロセスが自分より高い権限を得ること (例: setuid バイナリの実行) を防ぎます。非 root 実行と組み合わせると、権限昇格の経路をもう 1 つ閉じます。コンテナレベルのフィールドです。
securityContext:
allowPrivilegeEscalation: falseLinux 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_SERVICE | 1024 未満のポートへのバインド |
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 出力の uid・gid・groups がマニフェストに書いた値と一致すれば、適用は完了です。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/run に emptyDir を付けて動作に支障がありません。
試験ポイント #
securityContextは Pod レベル (spec.securityContext) とコンテナレベル (spec.containers[].securityContext) の 2 か所にあり、コンテナレベルが Pod レベルを上書きします。runAsUser・runAsGroup・runAsNonRoot・capabilities・readOnlyRootFilesystem・allowPrivilegeEscalation・privilegedは コンテナレベル、fsGroup・supplementalGroupsは Pod レベル 専用です。- capability の値は
CAP_プレフィックスを外して書きます。模範解答はdrop: ["ALL"]の後に必要なものだけaddです。 readOnlyRootFilesystem: trueで書き込みが塞がれたら、emptyDirを書き込みパスにマウント して回避します。runAsNonRoot: trueだけ置いて非 root UID を与えないと root イメージが起動に失敗することがあるので、runAsUserを一緒に指定します。- 検証は
k exec -- idとk exec -- whoamiです。採点前に実際の UID/GID を目で確認します。 - フィールドの位置が迷ったら
k explain pod.spec.securityContextとk explain pod.spec.containers.securityContextで即座に確認します。
まとめ #
この記事で押さえたこと:
- securityContext の 2 つの層。Pod レベルは共通のデフォルト値、コンテナレベルは例外であり、コンテナレベルが優先
- 実行ユーザーの制御。
runAsUser・runAsGroup・runAsNonRootで非 root を強制、fsGroupでボリュームへの書き込み権限を確保 - ファイルシステムの制御。
readOnlyRootFilesystem+emptyDir回避、allowPrivilegeEscalation: false - capabilities。
drop: ["ALL"]の後に最小のadd、privileged: trueは隔離を崩すので回避 - 検証。
k exec -- id・whoamiで実際のアイデンティティを確認
次へ: リソース管理 #
コンテナの権限を狭めたので、次はコンテナが どれだけ多くのリソースを使えるか を制御する番です。
#16 リソース管理: requests/limits、QoS class、LimitRange では、CPU・メモリの requests と limits がスケジューリングと OOM に与える影響、その組み合わせで決まる QoS class (Guaranteed・Burstable・BestEffort)、ネームスペース単位のデフォルト値をかける LimitRange と ResourceQuota まで自分で作りながらまとめます。