- 第15回スコープ・学習目標・今ここマップ
- Kubernetes セキュリティの 3 層 — Authn / Authz / Admission
- RBAC の仕組み — Role / RoleBinding と ServiceAccount の紐付け
- ClusterRole / ClusterRoleBinding — クラスタスコープの権限
- やってみよう①: fanclub-backend-sa に pod-reader Role を紐付ける
- Step 1: pod-reader Role の YAML を作成・apply する
- Step 2: RoleBinding の YAML を作成・apply する
- Step 3: RoleBinding の詳細を確認する
- Step 4: fanclub-backend-sa の Pod list 権限を kubectl auth can-i で確認する
- Step 5: 別 verb で「権限がない」ことを確認する(PoLP の効能)
- Step 6: Pod 内から SA Token を使った API Call を実機検証する
- Step 7: automountServiceAccountToken を false に戻す(クリーンアップ)
- 演習①まとめ
- SecurityContext の仕組み — Pod / Container レベルの多段防御
- やってみよう②: nginx Pod に多段防御 SecurityContext を適用する
- Step 1: nginx-secure.yaml を作成する(YAML 全量)
- Step 2: nginx-secure Pod を apply して起動を確認する
- Step 3: Pod に適用された SecurityContext を確認する
- Step 4: コンテナ内の実行 uid を id コマンドで確認する
- Step 5: ルート FS への書き込み試行で reject 確認
- Step 6: emptyDir 経由なら書ける確認
- Step 7: capabilities がすべて drop されていることを確認する
- Step 8: nginx-secure Pod を削除する(クリーンアップ)
- 演習②まとめ
- fanclub-backend を non-root 化する場合の課題 — Dockerfile レベル改修
- Admission Controller 概念 — Validating / Mutating Webhook
- CustomResourceDefinition (CRD) 利用概念 — K8s 拡張の入口
- やってみよう③: Admission Webhook + CRD の存在確認
- CKAD 試験頻出パターン + 現場ヒヤリハット 2 件
- ep15 完了後の模擬アプリ状態と ep16 への橋渡し
- 理解度チェック・第15回まとめ・次回予告・シリーズ一覧
第15回スコープ・学習目標・今ここマップ
動作確認バージョン: K8s v1.35 / kubectl v1.35.0 / kind v0.31.0 / kindest/node:v1.35.0 / Docker CE 29.4.3 / containerd 2.2.3 / AlmaLinux 10.1(kernel 6.12.0-124.55.3.el10_1)(2026-05-15 時点・k8s-ops 実機検証済・SP_vol1-pre-24 起点)
本回は Kubernetes 実践教科書 第1巻(CKAD 対応・全 19 回)の第15回です。第5部「セキュリティ基礎」の第1回(2 回中の 1 回目)として、第14回で確立した Namespace 分離・ResourceQuota・LimitRange の土台にセキュリティ層を載せていきます。
RBAC(Role / RoleBinding / ClusterRole / ClusterRoleBinding)で Service Account の API 権限を最小化し、SecurityContext(runAsNonRoot / runAsUser / readOnlyRootFilesystem / capabilities drop / allowPrivilegeEscalation: false)で Pod / Container の Linux レベルの権限を多段に絞り込みます。
さらに Admission Controller(ValidatingAdmissionWebhook / MutatingAdmissionWebhook)と CustomResourceDefinition (CRD) の概念を整理し、CKAD ドメイン D4「Application Environment, Configuration and Security」の Competency「Understand authentication, authorization, and admission control」と「Understand SecurityContexts」を完全網羅して、第14回までで未充足だった D4 の最後の項目を本回で埋めます。
第14回からの継承状態確認(SP_vol1-pre-24 状態):
| 項目 | 状態 | 出典 |
|---|---|---|
| kind クラスタ | kind-control-plane Ready(v1.35.0) | Lead 実機観察 |
| Namespace 一覧 | default / kube-node-lease / kube-public / kube-system / local-path-storage(合計 5 個・dev/prod は ep14 末で削除済) | ep14 完了状態 |
| fanclub-backend Deployment | replicas: 2 / 3 Probe 設定済 / RollingUpdate maxSurge:1 maxUnavailable:0 | ep12 完了状態を継続 |
| fanclub-db StatefulSet | fanclub-db-0 Pod Running(PostgreSQL 18) | ep9 から継続 |
| node-logger DaemonSet | 1 Pod Running | ep11 から継続 |
| ConfigMap / Secret | fanclub-config / fanclub-secret 継続 | ep10 から継続 |
| ServiceAccount | fanclub-backend-sa(automountServiceAccountToken: false)+ default | ep10 から継続 |
| Role / RoleBinding | 0 件(default ns・本回で初導入) | Lead 実機観察 |
| CRD | 0 件(Gateway API CRD は ep18 で導入予定) | Lead 実機観察 |
| Allocated CPU | 1550m / 2000m(77 %) | Lead 実機観察 |
| fanclub-backend Pod 実行 uid | uid=0(root)(fanclub-backend:0.1.0 image は USER 未指定) | Lead 実機観察 |
今ここマップ(第1巻 19 回中の現在位置):
第1部 コンテナとDocker
第1回〜第4回 [完了]
第2部 Kubernetes基礎
第5回〜第6回 [完了]
第3部 アプリリソース
第7回〜第11回 [完了]
第4部 ワークロード戦略
第12回〜第14回 [完了]
第5部 セキュリティ基礎(第15〜16回)
★ 第15回 RBAC + SecurityContext + Admission Controller 概念 + CRD 利用 ← 今ここ
第16回 NetworkPolicy 基礎
第6部 パッケージ管理 + HTTPS公開(第17〜19回)
第15回を終えると、以下を習得した状態になります。
- Kubernetes セキュリティの 3 層(Authentication / Authorization / Admission Control)の役割と実行順序を説明できる。kubectl リクエストが API Server に到達してから etcd に書き込まれるまでの間に通過する 3 つのチェックポイントの責務を区別し、各層で使われる機構(kubeconfig / RBAC / Admission Plugin / Admission Webhook)を整理できる
- Role / RoleBinding を作成し、ServiceAccount に Role を紐付けて最小権限の API アクセス(PoLP・Principle of Least Privilege)を実装できる。
kubectl auth can-i+--asフラグで「特定 SA としてこの操作ができるか」を即座に確認する CKAD 試験速攻パターンを使える。ClusterRole / ClusterRoleBinding との使い分け(Namespace スコープ vs クラスタスコープ)を説明できる - SecurityContext で多段防御を Pod に適用できる。Pod レベル(
spec.securityContext)と Container レベル(spec.containers[].securityContext)の設定の優先順位を理解し、runAsNonRoot/runAsUser/runAsGroup/fsGroup/readOnlyRootFilesystem/allowPrivilegeEscalation/capabilities.drop/seccompProfileの各フィールドの効果と推奨値を説明できる - Admission Controller の役割(ValidatingAdmissionWebhook / MutatingAdmissionWebhook)と CustomResourceDefinition (CRD) の概念を説明できる。組込 Admission Plugin(LimitRanger / ResourceQuota / ServiceAccount / NamespaceLifecycle)の存在を認識し、外部 Webhook と組込 Plugin の関係を整理できる。kubectl で既存 Webhook 設定と CRD を列挙する確認コマンドを使える
- CKAD 試験 D4 ドメイン「Understand authentication, authorization, and admission control」と「Understand SecurityContexts」の 2 Competency に対応できる。RBAC 設計の試験頻出パターン(
kubectl create role/kubectl create rolebindingのワンライナー速攻)と SecurityContext 設計の試験頻出パターン(runAsNonRoot + readOnlyRootFilesystem + capabilities drop の組合せ)を即座に書き起こせる
模擬アプリ進捗(第15回):本回の演習①では fanclub-backend-sa(ep10 で作成済)に pod-reader Role を紐付け、Pod 内から API Server へ最小権限の Pod 一覧取得 API を呼び出す流れを実機体験します。
演習②は SecurityContext の多段防御を確実に成功させるため、fanclub-backend ではなく nginxinc/nginx-unprivileged イメージを使った独立 Pod で全フィールドを適用します。
fanclub-backend を非 root 化するには Dockerfile に USER 1000 を追加して再ビルドが必要になるため、本回は「現状の fanclub-backend に runAsNonRoot: true を強制するとどう失敗するか」を H2「fanclub-backend を non-root 化する場合の課題」で実機観察し、Dockerfile 改修を要する事実を Image レベルの宿題として ep18(HTTPS 公開時にイメージ再ビルド)に持ち越します。
演習③は Admission Webhook と CRD の「現在状態確認」に絞り、外部 Webhook / CRD の本格的な利用は ep18(cert-manager / Gateway API)と第3巻(OPA Gatekeeper / Falco)に譲ります。
第15回完了後の模擬アプリ状態:演習①で作成した pod-reader Role と fanclub-backend-pod-reader RoleBinding は default ns に残ります(ep16 で NetworkPolicy 設計時の SA 認可ベースとして引き続き活用)。
演習②で作成した nginx-secure Pod は H2「やってみよう②」末尾のクリーンアップで kubectl delete pod によって削除します。
fanclub-backend / fanclub-db / node-logger は ep14 完了状態のまま影響を受けず、ep16(NetworkPolicy 基礎)にクリーンな状態で引き継ぎます。
Kubernetes セキュリティの 3 層 — Authn / Authz / Admission
個別の機構の演習に入る前に、本回の中心テーマである「Kubernetes セキュリティの 3 層」を頭の整理として最初に押さえておきます。
kubectl コマンドや Pod の Service Account Token を起点としたあらゆる API リクエストは、API Server に到達した後、以下の 3 つのチェックポイントを順番に通過してから etcd に書き込まれます。
CKAD 試験では「次の機構はどの層に属するか」を問う設問が定番で、3 層の名前と順序を覚えるのが得点の出発点になります。
3 層の役割と実行順序
| 順序 | 層 | 役割 | 機構の例 | 失敗時の HTTP ステータス |
|---|---|---|---|---|
| 1 | Authentication(認証・Authn) | 「リクエストの送信者は誰か」を識別する。kubeconfig の client certificate / Bearer Token / Service Account Token / 外部 IdP(OIDC)等から identity を確定 | X.509 Client Cert / Static Token / SA Token / OIDC / Webhook Authentication | 401 Unauthorized |
| 2 | Authorization(認可・Authz) | 確認された送信者が要求された操作を実行する権限を持つか判定。「user X が resource Y に対して verb Z を実行できるか」を Yes/No で返す | RBAC / Node Authorization / Webhook Authorization / ABAC(非推奨) | 403 Forbidden |
| 3 | Admission Control(受付制御) | リクエスト内容そのものを検証・変更する。「この Pod の resources は LimitRange の max を超えていないか」「runAsNonRoot 違反していないか」「label を自動付与すべきか」等を判断 | 組込 Admission Plugin(LimitRanger / ResourceQuota / ServiceAccount / NamespaceLifecycle 等)+ 外部 Webhook(ValidatingAdmissionWebhook / MutatingAdmissionWebhook) | 403 Forbidden(reject 時) |

3 層の実行順序は Authn → Authz → Admission で固定されています。Authentication で識別できない(401)リクエストは Authz に進まず即座に弾かれ、Authz で権限が無い(403)と判定されたリクエストも Admission に進みません。
Admission Control まで到達したリクエストは内容自体の検証・変更を受け、最終的に Admission を通過すると API Server が etcd にリソースを書き込みます。
第14回で扱った ResourceQuota / LimitRange は 3 層のうち Admission Control(層 3)に属する機構です。
具体的には LimitRanger と ResourceQuota という名前の組込 Admission Plugin が API Server に最初から搭載されており、Pod 作成リクエストが Admission 層を通過する瞬間に「resources が LimitRange max を超えていないか」「Quota Hard を超えていないか」を検証して reject または通過させる動きをしていました。
本回で扱う RBAC は層 2(Authorization)、Admission Controller の概念整理は層 3(Admission Control)と、3 層のうち下 2 層をカバーする回になります。
Authn 層(層 1)は本シリーズ第1巻のスコープ外で、第2巻 CKA の kubeadm クラスタ構築・kubeconfig 生成の文脈で本格的に扱います。
3 層フローを kubectl で擬似的に追う
3 層のフローを実機で追体験するには、kubectl リクエストが「どの層で何が起きたか」を観察するのが分かりやすいです。本回の演習①で kubectl auth can-i コマンドを使うのは、層 2(Authorization)だけを単独で動かして「user / SA が verb を実行できるか」を確認する用途のためです。
Authn は kubeconfig の client certificate で kind の admin として識別されており、Authz が RBAC のルールに従って Yes/No を返します。
Admission は Pod / Service / その他リソースの作成リクエストが Admission Plugin / Webhook を通過する瞬間に動くため、kubectl auth can-i 単体では Admission は走りません。
実際の Pod 作成(kubectl create pod / kubectl apply -f pod.yaml)リクエストでは 3 層すべてが順番に動きます。
第14回の演習②で「resources 未指定の Pod が LimitRange の defaultRequest で自動補完されて作成された」というのは、Admission 層の MutatingAdmissionWebhook 系統(具体的には LimitRanger プラグイン)が Pod の spec を変更してから etcd に書き込んだ結果でした。
同じく ep14 で「count/pods: 3 到達後の Pod 作成が Forbidden で reject された」のは Admission 層の ValidatingAdmissionWebhook 系統(ResourceQuota プラグイン)が拒否した結果です。
「ValidatingAdmissionWebhook = リクエスト内容を検証・reject 可能」「MutatingAdmissionWebhook = リクエスト内容を変更可能」という分類は、第14回で扱った組込 Plugin にも適用される普遍的なパターンになります。
Authentication(層 1)の補足 — 第1巻スコープ外として概要だけ
Authn の主要な認証方式を整理します。本回の演習では kind が自動生成した kubeconfig(client certificate 方式)を経由するため Authn 層は意識しませんが、CKAD 試験では「次の認証方式のうち本番で推奨されるのはどれか」のような選択問題が出題されるため、概要だけ押さえておきます。
| 認証方式 | 概要 | 本シリーズでの扱い |
|---|---|---|
| X.509 Client Certificate | kubeconfig に埋め込まれた client cert で identity を確定。kind / kubeadm のデフォルト | 第5回(kind 利用)から間接的に使用中 |
| Service Account Token | Pod が /var/run/secrets/kubernetes.io/serviceaccount/token から読み取って API Server に送る Bearer Token | ep10 で扱い済・本回演習①で再登場 |
| Bearer Token(Static) | API Server 起動時に静的トークンファイルを指定。本番非推奨 | 触れない(非推奨のため) |
| OIDC(OpenID Connect) | 外部 IdP(Keycloak / Azure AD / Google)と連携。本番のユーザー認証で標準 | 第2巻 CKA + 第3巻 CKS で扱う |
| Webhook Authentication | カスタム認証 endpoint に問い合わせ。OIDC で対応できないレガシー認証等 | 本シリーズ範囲外 |
本シリーズ第1巻の CKAD 範囲では「Pod から API Server に SA Token で認証する」パターンが Authn の主要な実機接点になります。
ep10 で fanclub-backend-sa を作成し automountServiceAccountToken: false で Token mount を意図的に無効化したのは、Pod が侵害されたときに SA Token が攻撃者に渡らないようにする防御策でした。
本回演習①では一時的に automountServiceAccountToken: true に切り替えて SA Token を Pod 内に mount し、curl で API Server を直接叩く流れを実機体験した後、確認が終わったら元の false に戻す(H2-12 ヒヤリハット②で扱う原則の再確認)構成にします。
RBAC の仕組み — Role / RoleBinding と ServiceAccount の紐付け
3 層のうち層 2(Authorization)の中心機構が RBAC(Role-Based Access Control)です。Kubernetes v1.8 以降の標準認可方式で、kind / kubeadm を含むほぼすべての K8s ディストロでデフォルト有効になっています。
本セクションでは RBAC の 4 種類のリソース(Role / RoleBinding / ClusterRole / ClusterRoleBinding)のうち、Namespace スコープの 2 種類(Role / RoleBinding)に焦点を絞り、YAML 構造・主要フィールド・ServiceAccount との紐付け方法を整理します。
クラスタスコープの 2 種類(ClusterRole / ClusterRoleBinding)は次の H2 で扱います。
RBAC の 4 リソースの全体像
| リソース | スコープ | 役割 | 本回での扱い |
|---|---|---|---|
| Role | Namespace | 「Namespace 内の特定リソースに対する verb の集合」を定義(権限の定義側) | 本演習①で pod-reader を作成 |
| RoleBinding | Namespace | Role を Subject(User / Group / ServiceAccount)に紐付ける(権限の付与側) | 本演習①で fanclub-backend-pod-reader を作成 |
| ClusterRole | クラスタ | 「クラスタ全体または全 Namespace 横断のリソースに対する verb の集合」を定義 | 次 H2 で概念のみ説明 |
| ClusterRoleBinding | クラスタ | ClusterRole を Subject に紐付ける | 次 H2 で概念のみ説明 |
記憶のコツは「Role / ClusterRole = 権限の定義(何ができるか)」「RoleBinding / ClusterRoleBinding = 権限の付与(誰に与えるか)」という役割分担です。
Role 単体では権限は誰にも付与されず、RoleBinding を別途作って Subject(User / Group / ServiceAccount)に紐付けて初めて効果が発動します。Role と RoleBinding を 1 セットで作るのが本回の演習①の流れになります。

Role の YAML 構造(全量)
本演習①で作成する pod-reader Role の YAML を全量で示します。これは「default Namespace 内の Pod に対する get / list / watch 権限を持つ Role」の定義で、最小権限の原則(PoLP・Principle of Least Privilege)の好例になります。
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: pod-reader
namespace: default
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list", "watch"]
各フィールドの意味を整理します。
| フィールド | 意味 | 本演習での値 |
|---|---|---|
apiVersion | RBAC API は v1 で安定(v1beta1 は K8s v1.22 で削除済) | rbac.authorization.k8s.io/v1 |
kind | Namespace スコープの権限定義 | Role |
metadata.namespace | Role が所属する Namespace。この Namespace 内のリソースにのみ権限が及ぶ | default |
rules | 権限ルールの配列。複数ルールを並列に定義可能 | 1 ルール(pods の get/list/watch) |
rules[].apiGroups | 対象 API グループの配列。core API(Pod / Service / ConfigMap 等)は空文字 "" | [""](core API) |
rules[].resources | 対象リソースの配列。リソース名は複数形(pods / services / configmaps 等) | ["pods"] |
rules[].verbs | 許可する verb の配列。代表例: get / list / watch / create / update / patch / delete / deletecollection | ["get", "list", "watch"] |
apiGroups: [""](空文字)は core API グループを意味します。
Pod / Service / ConfigMap / Secret / Namespace / ServiceAccount / Node / PV / PVC など、K8s の最も基本的なリソースはすべて core API に属し、apiVersion: v1 という短いバージョン表記がこれを示しています。
一方、Deployment / StatefulSet / DaemonSet は apps API グループ(apiVersion: apps/v1)、Job / CronJob は batch グループ(batch/v1)、Role / RoleBinding は rbac.authorization.k8s.io グループに属します。
Role を書くときに apiGroups の指定を間違えると、対象リソースが見つからずに権限が効かない事故になるため、リソースとグループの対応は暗記しておきたい部分です。
verb 早見表(CKAD 試験頻出)
| verb | 意味 | kubectl コマンドとの対応 |
|---|---|---|
get | 個別リソースの読み取り(名前指定) | kubectl get pod <name> / kubectl describe pod <name> |
list | リソース一覧の読み取り | kubectl get pods(名前なし) |
watch | リソース変更のストリーミング購読 | kubectl get pods --watch / Controller の Informer |
create | 新規リソース作成 | kubectl create / kubectl apply(新規時) |
update | 既存リソースの完全置換 | kubectl replace / kubectl apply(更新時) |
patch | 既存リソースの部分更新 | kubectl patch / kubectl edit |
delete | 個別リソース削除 | kubectl delete pod <name> |
deletecollection | 条件マッチする複数リソース一括削除 | kubectl delete pods --all |
「読み取り専用 Role」を定義したい場合は verbs: ["get", "list", "watch"] の 3 つを与えるのが定石です。
get だけだと一覧取得(kubectl get pods)ができず、list だけだと個別取得や describe ができず、watch がないと Controller が動かない、という穴ができるため、3 つセットで読み取り権限を構成します。CKAD 試験でも "get,list,watch" の 3 連は頻出フレーズになっています。
RoleBinding の YAML 構造(全量)
本演習①で作成する fanclub-backend-pod-reader RoleBinding の YAML を全量で示します。これは「fanclub-backend-sa という ServiceAccount に pod-reader Role を紐付ける」定義です。
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: fanclub-backend-pod-reader
namespace: default
subjects:
- kind: ServiceAccount
name: fanclub-backend-sa
namespace: default
roleRef:
kind: Role
name: pod-reader
apiGroup: rbac.authorization.k8s.io
主要フィールドを整理します。
| フィールド | 意味 | 本演習での値 |
|---|---|---|
metadata.namespace | RoleBinding が所属する Namespace | default |
subjects | 権限を付与する対象の配列(User / Group / ServiceAccount) | 1 件(fanclub-backend-sa) |
subjects[].kind | 対象の種類。ServiceAccount / User / Group のいずれか | ServiceAccount |
subjects[].name | 対象の名前 | fanclub-backend-sa |
subjects[].namespace | ServiceAccount の所属 Namespace(kind: ServiceAccount のみ必須・User/Group では指定不要) | default |
roleRef.kind | 紐付ける Role 種別。Role または ClusterRole | Role |
roleRef.name | 紐付ける Role / ClusterRole の名前 | pod-reader |
roleRef.apiGroup | 固定値(RBAC API グループ) | rbac.authorization.k8s.io |
重要な制約:roleRef は RoleBinding 作成後に変更できない immutable フィールドです。「pod-reader Role を紐付けた RoleBinding を別の Role に切り替えたい」場合は、既存 RoleBinding を kubectl delete で削除してから新規に作成し直す必要があります。
kubectl apply で roleRef を変更しようとすると The RoleBinding is invalid: roleRef: Invalid value: ... cannot change roleRef エラーで弾かれます。
CKAD 試験では「既存 RoleBinding の Role を変更するには?」という設問で「delete + recreate」が正解になる定型パターンです。
RoleBinding の 3 通りの組合せパターン
| パターン | roleRef.kind | subjects[].kind | 用途 |
|---|---|---|---|
| 1 | Role | ServiceAccount | 本演習①の構成。Namespace 内 Pod から Namespace 内リソースにアクセス |
| 2 | Role | User / Group | 運用者個人や運用チームに Namespace 内権限を付与 |
| 3 | ClusterRole | ServiceAccount / User / Group | クラスタワイドに定義した共通 Role を Namespace スコープで限定的に付与(次 H2 で詳述) |
パターン 3 は「ClusterRole を作って RoleBinding で Namespace 内限定で付与」する構成で、複数 Namespace で同じ Role を再利用したい場合のベストプラクティスです。
view / edit / admin という K8s が標準提供する ClusterRole があり、これらを RoleBinding で Namespace 内のユーザーに付与する運用パターンが本番でよく使われます。本演習①ではパターン 1 のシンプル構成に集中しますが、パターン 3 の存在は CKAD 試験で問われることがあるため認識しておきます。
最小権限の原則(PoLP・Principle of Least Privilege)
RBAC 設計の根本原則が「最小権限の原則」です。「アプリケーションや運用者が業務遂行に必要とする最小の API 権限のみを付与し、それ以外の権限は与えない」という考え方で、セキュリティの基本原則として ISO 27001 / NIST CSF / CIS Controls のいずれにも明記されています。Kubernetes RBAC の文脈での実装ガイドラインを整理します。
- verb は必要なものに絞る:読み取り専用で済む SA に
delete/createを与えない。「Pod 一覧を取得して状態監視する」用途ならget / list / watchの 3 verb のみで十分 - resources は必要なものに絞る:Pod アクセスだけ必要なら
podsのみ指定し、"*"(全リソース)は使わない。「Secret も読めるようにしておくと便利」のような便利目的の権限拡大は禁止 - apiGroups はワイルドカードを避ける:
apiGroups: ["*"]は core + apps + batch + rbac + 全 CRD グループを意味し、本番では絶対に使わない。CRD(cert-manager / Gateway API 等)にも権限が及ぶ意図しない権限拡大の原因になる - ClusterRole より Role を優先:Namespace 内で済む権限は Role + RoleBinding で実装。ClusterRole は「クラスタ全体に影響する権限」が本当に必要な場合のみ使う
- ClusterRoleBinding は最小限:cluster-admin 相当の権限を ClusterRoleBinding で付与するのは運用 admin に限る。アプリケーション SA に ClusterRoleBinding を付ける場合は理由を明文化する
本演習①の pod-reader は PoLP の典型例で、「default ns の Pod のみ、読み取り 3 verb のみ」という最小権限の組合せです。
fanclub-backend-sa が将来「Service も一覧取得したい」となれば、別の Role を追加するか pod-reader Role の rules に {apiGroups: [""], resources: ["services"], verbs: ["get", "list", "watch"]} を追加する形で対応します。
「ついでに services も pods Role に追加」のような追加は責務混在を招くため、Role は用途別に分離するのが本番設計のセオリーです。
kubectl create role / kubectl create rolebinding のワンライナー速攻パターン
CKAD 試験本番で時間を節約する重要テクニックが、Role / RoleBinding をワンライナーで作成する速攻パターンです。YAML を書かずにオプションで指定し、必要なら --dry-run=client -o yaml で YAML 化してから kubectl apply に渡す形が定石になります。
| 目的 | コマンド |
|---|---|
| Role 作成(pods の get/list/watch) | kubectl create role pod-reader --verb=get,list,watch --resource=pods -n default |
| Role 作成(複数リソース) | kubectl create role multi-reader --verb=get,list --resource=pods,services -n default |
| RoleBinding 作成(SA に紐付け) | kubectl create rolebinding fanclub-backend-pod-reader --role=pod-reader --serviceaccount=default:fanclub-backend-sa -n default |
| RoleBinding 作成(User に紐付け) | kubectl create rolebinding alice-pod-reader --role=pod-reader --user=alice -n default |
| YAML 化してから編集(dry-run) | kubectl create role pod-reader --verb=get,list,watch --resource=pods -n default --dry-run=client -o yaml > pod-reader-role.yaml |
本演習①では教材的に YAML を全量提示する形を取りますが、CKAD 試験本番では「YAML 全量を書く時間がもったいない」ため、上記ワンライナーを暗記レベルで打鍵できる状態にしておくのが現実的な合格戦略になります。
ClusterRole / ClusterRoleBinding — クラスタスコープの権限
前 H2 で扱った Role / RoleBinding は Namespace スコープの権限機構でしたが、クラスタ全体や全 Namespace 横断のリソースに対しては別の機構が必要です。それが ClusterRole と ClusterRoleBinding です。本 H2 では概念整理のみで実機演習は行わず、CKAD 試験で問われる典型パターンを押さえます。
ClusterRole が必要になる 3 つのケース
| ケース | 説明 | 例 |
|---|---|---|
| 1. クラスタスコープのリソース | Namespace に属さないリソース(Node / PV / StorageClass / Namespace 自身 / CRD / ClusterRole / ClusterRoleBinding 等)への権限 | 「Node を list したい」「Namespace を作成したい」「PV を bind したい」 |
| 2. 全 Namespace 横断のアクセス | Namespace スコープリソースでも複数 Namespace を跨いで操作したい場合 | 「全 Namespace の Pod 一覧を取りたい」(Prometheus のメトリクス収集等) |
| 3. 共通 Role の再利用 | 複数 Namespace で同じ Role を使いたい場合に、ClusterRole を作って各 Namespace で RoleBinding で紐付ける | 「dev / staging / prod の各 ns で同じ app-reader Role を使いたい」 |
ケース 3 の「ClusterRole + RoleBinding」組合せは本番でよく使われるパターンです。
ClusterRole で「Pod / Service / Deployment の読み取り権限」を 1 か所だけ定義し、各 Namespace で RoleBinding を作って同じ ClusterRole を紐付けると、Role を Namespace 数分作る冗長性を排除できます。
RoleBinding 経由の場合、付与される権限は RoleBinding の所属 Namespace 内に限定されるため、ClusterRole の本来のスコープより狭く運用できる点が利点になります。
K8s が標準提供する組込 ClusterRole(よく使う 4 つ)
| ClusterRole 名 | 権限内容 | 典型的な用途 |
|---|---|---|
cluster-admin | 全リソースに対する全 verb 許可(神権限) | クラスタ運用者・初期構築時。本番では極力 ClusterRoleBinding しない |
admin | Namespace 内全リソースの管理権限(ResourceQuota / Namespace 自身の操作は除く) | Namespace 内の「フル権限ユーザー」 |
edit | Namespace 内ワークロード(Pod / Service / Deployment / ConfigMap 等)の作成・更新・削除。Role / RoleBinding 操作は不可 | Namespace 内の「アプリ開発者」 |
view | Namespace 内全リソースの読み取りのみ(Secret は除外) | Namespace 内の「閲覧者」 |
これらは kubectl get clusterroles | grep -E "cluster-admin|^admin|^edit|^view" で実機確認できます(本演習③でも触れます)。
本番では「アプリ開発チームには edit、SRE には admin、運用閲覧者には view」のように組込 ClusterRole を再利用する設計が定着しており、独自に Role / ClusterRole を作るのは「組込で表現できない権限が必要」な場合に限定するのが推奨です。
ClusterRoleBinding と RoleBinding の使い分け
| 組合せ | 権限のスコープ | 用途 |
|---|---|---|
| ClusterRole + ClusterRoleBinding | クラスタ全体 | クラスタ管理者(cluster-admin)/ クラスタワイドの監視 SA(Prometheus 等)/ Node 操作するアプリ SA |
| ClusterRole + RoleBinding | RoleBinding の所属 Namespace 内のみ | 組込 view/edit を Namespace 内ユーザーに付与する一般的なパターン |
| Role + RoleBinding | Role / RoleBinding の所属 Namespace 内のみ | 本演習①のパターン。Namespace 限定の独自 Role 用 |
| Role + ClusterRoleBinding | 無効な組合せ(ClusterRoleBinding は Role を参照不可) | —(試験で誤答候補として出る) |
4 番目の「Role + ClusterRoleBinding」は CKAD 試験で誤答として頻出する組合せです。
ClusterRoleBinding の roleRef.kind は ClusterRole しか受け付けないため、Role を参照しようとすると The RoleRef must reference a ClusterRole in the global namespace エラーになります。3 つの有効な組合せと 1 つの無効な組合せを区別できれば、CKAD の RBAC 関連設問はほぼ確実に得点できます。
権限調査の確認コマンド早見表
| 目的 | コマンド |
|---|---|
| 現在の自分の権限を確認 | kubectl auth can-i <verb> <resource> [-n <ns>] |
| 別 User の権限を確認 | kubectl auth can-i <verb> <resource> --as=<user> [-n <ns>] |
| SA の権限を確認 | kubectl auth can-i <verb> <resource> --as=system:serviceaccount:<ns>:<sa> [-n <ns>] |
| 全権限の列挙 | kubectl auth can-i --list [--as=<user>] [-n <ns>] |
| Role 一覧 | kubectl get roles -n <ns> / kubectl get clusterroles |
| RoleBinding 一覧 | kubectl get rolebindings -n <ns> / kubectl get clusterrolebindings |
| RoleBinding の詳細(Subject と Role の紐付け確認) | kubectl describe rolebinding <name> -n <ns> |
本演習①では kubectl auth can-i を --as=system:serviceaccount:default:fanclub-backend-sa オプション付きで実行し、「fanclub-backend-sa として Pod 一覧取得ができるか / Pod 削除ができるか」を確認します。
--as フラグは管理者権限のユーザー(kind の admin)でしか使えませんが、CKAD 試験本番では admin として試験を受けるため自由に使えます。SA の権限調査の高速コマンドとして暗記しておきましょう。
やってみよう①: fanclub-backend-sa に pod-reader Role を紐付ける
所要時間目安:約 20 分。default Namespace に pod-reader Role を作成し、fanclub-backend-pod-reader RoleBinding で fanclub-backend-sa ServiceAccount に紐付けます。
kubectl auth can-i で許可確認した後、fanclub-backend Pod 内に一時的に SA Token を mount して curl で API Server を叩く流れまで実機体験します。
所要時間の内訳は YAML 作成 + apply 5 分・auth can-i 確認 5 分・automount 切替と curl 検証 8 分・クリーンアップ 2 分です。
Step 1: pod-reader Role の YAML を作成・apply する
目的:default Namespace に Pod の get / list / watch 権限を持つ Role を作成する。本シリーズで初の Role リソース作成になります。
実行コマンド:
$ cat > pod-reader-role.yaml <<'EOF'
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: pod-reader
namespace: default
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list", "watch"]
EOF
$ kubectl apply -f pod-reader-role.yaml
期待される実行結果:
role.rbac.authorization.k8s.io/pod-reader created
Role が作成された時点では、まだ誰にも権限は付与されていません。Role は「権限の定義」だけなので、これだけでは fanclub-backend-sa は Pod を読み取れません。次の Step 2 で RoleBinding を作って権限を実際に付与します。
CKAD 試験では「Role を作っただけで権限を付与した」と勘違いするミスが頻発するため、「Role と RoleBinding はセットで初めて効果が出る」を頭に刻みます。
Step 2: RoleBinding の YAML を作成・apply する
目的:pod-reader Role を fanclub-backend-sa に紐付ける RoleBinding を作成する。
実行コマンド:
$ cat > pod-reader-rolebinding.yaml <<'EOF'
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: fanclub-backend-pod-reader
namespace: default
subjects:
- kind: ServiceAccount
name: fanclub-backend-sa
namespace: default
roleRef:
kind: Role
name: pod-reader
apiGroup: rbac.authorization.k8s.io
EOF
$ kubectl apply -f pod-reader-rolebinding.yaml
期待される実行結果:
rolebinding.rbac.authorization.k8s.io/fanclub-backend-pod-reader created
これで「fanclub-backend-sa は default ns 内の Pod に対する get / list / watch を実行できる」状態になりました。
RoleBinding 作成時に roleRef.name: pod-reader が指定した Role と一致していることを API Server が検証しますが、Role が存在しない状態で RoleBinding を作ろうとしてもエラーにはならず、後から Role を作っても遅延評価で権限が有効化される設計になっています。
逆に「RoleBinding を作ったあとに Role を間違えて削除」すると、その RoleBinding は「dangling reference」として残ったまま権限が無効化されます。本番運用では Role と RoleBinding を 1 つの YAML ファイルにまとめ、同時に apply / delete する運用パターンが推奨されます。
Step 3: RoleBinding の詳細を確認する
目的:作成した RoleBinding が「どの Role を」「どの Subject に」紐付けているかを kubectl describe で確認する。本番運用での RBAC トラブルシュートで最初に打つコマンドのパターンです。
実行コマンド:
$ kubectl describe rolebinding fanclub-backend-pod-reader -n default
期待される実行結果:
Name: fanclub-backend-pod-reader
Labels: <none>
Annotations: <none>
Role:
Kind: Role
Name: pod-reader
Subjects:
Kind Name Namespace
---- ---- ---------
ServiceAccount fanclub-backend-sa default
出力の Role: セクションが「紐付け先の Role 種別と名前」、Subjects: セクションが「権限を付与する対象」を示しています。Kind: ServiceAccount / Name: fanclub-backend-sa / Namespace: default の 3 つが揃っていれば、ep10 で作成した SA に正しく紐付いた状態です。
本番運用で「ある SA に何の権限があるか分からない」となったときは、対象 SA を Subjects に含む全 RoleBinding / ClusterRoleBinding を逆引き検索する必要があります。逆引きの一行コマンドは以下のような形になります。
$ kubectl get rolebindings,clusterrolebindings -A -o json | \
jq -r '.items[] | select(.subjects[]?.name == "fanclub-backend-sa") | "\(.kind)/\(.metadata.name) -> \(.roleRef.kind)/\(.roleRef.name)"'
CKAD 試験で逆引きクエリが問われることは稀ですが、本番運用では頻出のオペレーションなので、jq の使い方も併せて覚えておくと SRE 業務で役立ちます。
Step 4: fanclub-backend-sa の Pod list 権限を kubectl auth can-i で確認する
目的:「fanclub-backend-sa として」Pod 一覧取得ができるかを --as フラグ付きの auth can-i で確認する。RoleBinding が正しく動いている証跡を Yes/No で得るためのコマンドです。
実行コマンド:
$ kubectl auth can-i list pods \
--as=system:serviceaccount:default:fanclub-backend-sa \
-n default
期待される実行結果:
yes
yes が返れば RoleBinding が有効に動いている証拠です。--as フラグの値 system:serviceaccount:<ns>:<sa> は K8s 内部での ServiceAccount の正規表記で、Pod が SA Token で認証された後に Authz 層に渡される際のユーザー名と同じものになります。
auth can-i は API Server の認可ロジックをそのまま実行して結果を返す形式のため、「auth can-i が yes と言うなら本物の API Call も通る」という強い保証が得られます。
同じく get verb と watch verb についても確認しておきます。
実行コマンド:
$ kubectl auth can-i get pods --as=system:serviceaccount:default:fanclub-backend-sa -n default
$ kubectl auth can-i watch pods --as=system:serviceaccount:default:fanclub-backend-sa -n default
期待される実行結果:
yes
yes
3 つの verb いずれも yes が返れば、Role の verbs: ["get", "list", "watch"] が想定通りに効いています。
Step 5: 別 verb で「権限がない」ことを確認する(PoLP の効能)
目的:最小権限の原則が効いている証として、付与していない verb(delete / create)が拒否されることを確認する。auth can-i の no 出力が PoLP の証跡になります。
実行コマンド:
$ kubectl auth can-i delete pods --as=system:serviceaccount:default:fanclub-backend-sa -n default
$ kubectl auth can-i create pods --as=system:serviceaccount:default:fanclub-backend-sa -n default
$ kubectl auth can-i list secrets --as=system:serviceaccount:default:fanclub-backend-sa -n default
$ kubectl auth can-i list pods --as=system:serviceaccount:default:fanclub-backend-sa -n kube-system
期待される実行結果:
no
no
no
no
4 つの確認はそれぞれ別の側面から PoLP を検証しています。
delete pods→no:Role の verbs に delete を含めていないため拒否create pods→no:Role の verbs に create を含めていないため拒否list secrets→no:Role の resources を pods のみに絞っているため、Secret は対象外で拒否list pods -n kube-system→no:Role / RoleBinding は default ns に作成しているため、別 ns(kube-system)のリソースには権限が及ばない
4 つすべて no が返れば、PoLP が正しく機能しています。本番運用で「予期しない権限が付いていないか」をテストするときは、許可確認だけでなく拒否確認も実施するのが定石です。CKAD 試験でも「次のうち、この SA で実行できる/できない操作はどれか」という選択問題が頻出のため、両方向の確認パターンを練習しておきます。
Step 6: Pod 内から SA Token を使った API Call を実機検証する
目的:auth can-i は kubectl 経由の「サーバ側問い合わせ」だが、実際の Pod 内から SA Token で API Server を直接叩いて Pod 一覧を取得できることを確認する。
ep10 で確立した automountServiceAccountToken: false を一時的に true に切り替えて Pod を再作成し、curl で API Call → 確認後に false に戻す手順を踏みます。
まず現在の fanclub-backend Deployment の automountServiceAccountToken 設定を確認します。
実行コマンド:
$ kubectl get deployment fanclub-backend -o jsonpath='{.spec.template.spec.automountServiceAccountToken}'
$ echo
期待される実行結果:
false
ep10 で設定した false が継続しています。これを一時的に true に切り替えて Pod を再作成します。kubectl patch でワンライナーで切替が可能です。
実行コマンド:
$ kubectl patch deployment fanclub-backend \
-p '{"spec":{"template":{"spec":{"automountServiceAccountToken":true}}}}'
$ kubectl rollout status deployment/fanclub-backend
期待される実行結果:
deployment.apps/fanclub-backend patched
deployment "fanclub-backend" successfully rolled out
Rolling Update により新 Pod が起動し、その Pod には SA Token が /var/run/secrets/kubernetes.io/serviceaccount/token に mount されている状態になります。新 Pod 名を取得します。
実行コマンド:
$ POD=$(kubectl get pod -l app=fanclub-backend -o jsonpath='{.items[0].metadata.name}')
$ echo $POD
期待される実行結果:
fanclub-backend-7c8f5d9b6c-x4j2m
注: Pod 名のサフィックスは ReplicaSet hash + Pod hash の組合せで毎回変化します。本回の演習では実機で得られた Pod 名を変数 POD に保存して以降のコマンドで使用します。
Pod 内から SA Token を読み取り、API Server に curl で問い合わせます。Payara Micro イメージには curl が同梱されているため追加インストールは不要です。
実行コマンド:
$ kubectl exec $POD -- sh -c '
TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
CACERT=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt
NS=$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace)
curl -s --cacert $CACERT \
-H "Authorization: Bearer $TOKEN" \
https://kubernetes.default.svc/api/v1/namespaces/$NS/pods \
| head -c 200
echo'
期待される実行結果:
{"kind":"PodList","apiVersion":"v1","metadata":{"resourceVersion":"123456"},"items":[{"metadata":{"name":"fanclub-backend-7c8f5d9b6c-x4j2m","namespace":"default","uid":"
JSON で PodList が返ってくれば、Pod 内から SA Token 経由で「pod-reader Role で許可された Pod 一覧取得 API」が呼び出せた証拠です。出力の冒頭 200 バイトのみ表示する形にして、PodList の構造(kind / apiVersion / metadata / items)が確認できれば成功です。
逆に、許可していない verb(Pod 削除 API = DELETE)を叩くと 403 Forbidden で拒否されます。同じ Token を使った削除リクエストを確認します。
実行コマンド:
$ kubectl exec $POD -- sh -c '
TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
CACERT=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt
curl -s --cacert $CACERT \
-X DELETE \
-H "Authorization: Bearer $TOKEN" \
https://kubernetes.default.svc/api/v1/namespaces/default/pods/fanclub-backend-dummy \
| head -c 300
echo'
期待される実行結果:
{"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"pods \"fanclub-backend-dummy\" is forbidden: User \"system:serviceaccount:default:fanclub-backend-sa\" cannot delete resource \"pods\" in API group \"\" in the namespace \"default\"","reason":"Forbidden","details":{"name":"fanclub-backend-dummy","kind":"pods"},"code":403}
"reason":"Forbidden" / "code":403 / cannot delete resource "pods" のメッセージが PoLP の真打ちです。Token を持っていても、Role の verb に delete が含まれていなければ API Server が 403 で弾く動きが実機で確認できました。
これが「Pod に SA Token が侵害されても、最小権限の Role なら被害を Pod 一覧取得までに留められる」という防御機構の本体になります。
Step 7: automountServiceAccountToken を false に戻す(クリーンアップ)
目的:ep10 で確立した automountServiceAccountToken: false の原則に戻す。Token mount は本番常時 ON にすると侵害時の被害が大きくなるため、確認が終わったら即座に元の状態に戻すのがセキュリティ運用のセオリーです。
実行コマンド:
$ kubectl patch deployment fanclub-backend \
-p '{"spec":{"template":{"spec":{"automountServiceAccountToken":false}}}}'
$ kubectl rollout status deployment/fanclub-backend
期待される実行結果:
deployment.apps/fanclub-backend patched
deployment "fanclub-backend" successfully rolled out
確認のため、新 Pod 内に SA Token が mount されていないことを確認します。
実行コマンド:
$ POD=$(kubectl get pod -l app=fanclub-backend -o jsonpath='{.items[0].metadata.name}')
$ kubectl exec $POD -- ls /var/run/secrets/kubernetes.io/serviceaccount/ 2>&1 | head -3
期待される実行結果:
ls: cannot access '/var/run/secrets/kubernetes.io/serviceaccount/': No such file or directory
command terminated with exit code 2
SA Token のディレクトリ自体が存在しない状態になっており、Token mount が確実に無効化されています。これで演習①は完了です。
pod-reader Role と fanclub-backend-pod-reader RoleBinding は default ns に残った状態で、ep16 の NetworkPolicy 設計や ep17 以降の Helm Chart 化でも引き継いで使用します。
演習①まとめ
- Role / RoleBinding YAML を作成して fanclub-backend-sa に最小権限(Pod の get / list / watch のみ)を付与した
kubectl auth can-i+--as=system:serviceaccount:<ns>:<sa>で許可と拒否の両方を確認した- automountServiceAccountToken を一時的に true に切り替え、Pod 内から curl で API Server に PodList GET / 削除 DELETE を発行し、Role の verb に基づいて 200 OK と 403 Forbidden が分かれることを実機検証した
- 確認後は automountServiceAccountToken を false に戻し、本番運用の原則(Token mount は必要時のみ)を再確立した
SecurityContext の仕組み — Pod / Container レベルの多段防御
RBAC が「API Server への認可」を扱う機構だったのに対し、SecurityContext は「Pod / Container の Linux レベルの権限」を制御する機構です。
コンテナ内で動くプロセスがどの uid で実行されるか、ルートファイルシステムが書き込み可能か、Linux capabilities を持つか、setuid 等で権限昇格できるか、を YAML で宣言的に絞り込めます。
本セクションでは SecurityContext の YAML 構造・主要フィールド・Pod レベルと Container レベルの優先順位・推奨値を整理します。
SecurityContext が解決する 5 つの脅威
| 脅威 | SecurityContext での対策 | 本演習②で扱うフィールド |
|---|---|---|
| 1. コンテナエスケープ(root 実行から host kernel への侵害) | 非 root 実行(uid != 0)に強制 | runAsNonRoot: true + runAsUser: 1000 |
| 2. ルートファイルシステム改ざん(侵害後の永続化バックドア設置) | ルート FS を read-only に強制 | readOnlyRootFilesystem: true |
| 3. 権限昇格(setuid バイナリや sudo 経由の root 取得) | 権限昇格を OS レベルで禁止 | allowPrivilegeEscalation: false |
| 4. Linux capabilities 悪用(NET_ADMIN / SYS_ADMIN 等で危険操作) | すべての capability を drop | capabilities.drop: ["ALL"] |
| 5. syscall 悪用(カーネルの脆弱な syscall 経由の侵害) | seccomp プロファイルで syscall 制限 | seccompProfile.type: RuntimeDefault |
5 つの脅威に対する 5 つの対策を 1 つの Pod YAML に並べると、いわゆる「多段防御」の SecurityContext が完成します。本演習②ではこれら 5 項目をすべて設定した nginx-secure.yaml を作成し、各防御層が実機でどう作用するかを確認します。
Pod レベル securityContext と Container レベル securityContext
SecurityContext は Pod レベル(spec.securityContext)と Container レベル(spec.containers[].securityContext)の 2 階層で設定可能です。両者には設定できるフィールドの違いと優先順位があります。
| フィールド | Pod レベル | Container レベル | 備考 |
|---|---|---|---|
runAsNonRoot | ○ | ○ | Container レベルが優先。両方設定で Container が勝つ |
runAsUser | ○ | ○ | Container レベルが優先 |
runAsGroup | ○ | ○ | Container レベルが優先 |
fsGroup | ○ | × | Pod レベルのみ。volume の所有グループを設定 |
fsGroupChangePolicy | ○ | × | Pod レベルのみ。large volume の chown 最適化 |
supplementalGroups | ○ | × | Pod レベルのみ。追加の gid 群 |
seccompProfile | ○ | ○ | Container レベルが優先 |
seLinuxOptions | ○ | ○ | Container レベルが優先 |
readOnlyRootFilesystem | × | ○ | Container レベルのみ(Pod 全体ではなく Container 単位) |
allowPrivilegeEscalation | × | ○ | Container レベルのみ |
privileged | × | ○ | Container レベルのみ(true は本番禁止) |
capabilities | × | ○ | Container レベルのみ。drop / add のリスト |
procMount | × | ○ | Container レベルのみ |
記憶のコツは「ファイルシステム関連(fsGroup / supplementalGroups)と Pod 全体に効くもの(runAs*)は Pod レベル、Container 個別の挙動に効くもの(readOnly / privilege / capabilities)は Container レベル」というルールです。
本演習②の nginx-secure.yaml は両レベルを使い、Pod レベルに「実行 uid / gid と fsGroup と seccomp」、Container レベルに「ルート FS read-only と権限昇格防止と capabilities drop」を配置するベストプラクティスな構成です。
主要フィールドの効果と推奨値
| フィールド | 効果 | 推奨値 | 非設定時の挙動 |
|---|---|---|---|
runAsNonRoot | uid=0 での実行を Admission 層で reject。コンテナイメージの USER が root の場合は Pod 起動失敗 | true | false(root 実行可) |
runAsUser | コンテナ内のプロセスを指定 uid で実行(Dockerfile の USER を上書き) | 1000(または 65534 = nobody) | イメージの USER を踏襲(多くは root) |
runAsGroup | コンテナ内のプロセスを指定 gid で実行 | 1000 | イメージの GROUP を踏襲 |
fsGroup | Volume の所有グループに gid を設定(Pod 起動時に kubelet が chown) | 1000 | volume の元の所有者のまま |
readOnlyRootFilesystem | コンテナの / を read-only でマウント。書き込み試行は EROFS エラー | true | false(書き込み可) |
allowPrivilegeEscalation | setuid / sudo / fcaps による権限昇格を OS レベルで禁止 | false | true(昇格可) |
capabilities.drop | Linux capabilities を drop(権限剥奪) | ["ALL"] | Docker デフォルト 14 capability 保持 |
capabilities.add | drop した capabilities のうち必要なものだけ add(最小許可) | 必要時のみ追加(例: NET_BIND_SERVICE) | — |
seccompProfile.type | seccomp プロファイル適用。RuntimeDefault は CRI デフォルトのプロファイル | RuntimeDefault | Unconfined(K8s v1.27 以降 PSS で警告対象) |
privileged | 特権コンテナとして host の全 capability を保持 | 絶対に true にしない | false(推奨) |
本演習②の nginx-secure.yaml は上表の 1〜8 行目(capabilities.add を除く)の推奨値をすべて適用する構成です。9 行目の privileged はそもそも YAML に書かない(デフォルト false)形にして、誤って true にする事故を予防します。
Linux capabilities の主要 14 個 + drop ALL の意義
Linux capabilities は、従来「root or 非 root」の二元論だった権限を細粒度に分割したカーネル機能です。Docker / containerd はデフォルトで以下 14 個の capability をコンテナに付与しています。
| capability | 意味 | 悪用された場合の被害 |
|---|---|---|
CAP_CHOWN | ファイル所有者変更 | 機密ファイル所有者書き換え |
CAP_DAC_OVERRIDE | DAC(ファイルパーミッション)バイパス | 権限のないファイル読み書き |
CAP_FOWNER | ファイル所有者でなくてもメタデータ変更 | — |
CAP_FSETID | ファイル変更時の setuid/setgid 保持 | — |
CAP_KILL | 他プロセスへの signal 送信 | — |
CAP_SETGID | プロセス gid 変更 | 権限昇格の足がかり |
CAP_SETUID | プロセス uid 変更 | 権限昇格の足がかり |
CAP_SETPCAP | capability 変更 | — |
CAP_NET_BIND_SERVICE | 1024 未満の port に bind | —(Web Server で port 80/443 bind に必要) |
CAP_NET_RAW | raw socket 利用(ping / arp 等) | ARP spoofing / port scan |
CAP_SYS_CHROOT | chroot() syscall | — |
CAP_MKNOD | デバイスファイル作成 | — |
CAP_AUDIT_WRITE | audit log への書き込み | — |
CAP_SETFCAP | ファイル capability の設定 | — |
これら 14 個のうち、Web Server や API Server の一般的なアプリケーションがランタイムで本当に必要とするものは ほぼゼロです。
Java / Node.js / Python / Go で書かれた API サーバは「ファイルを読み書きする / TCP socket を listen する / signal を扱う」程度で動作しますが、これらは uid 1000 の非 root プロセスでも可能で、特別な capability は不要です。
capabilities.drop: ["ALL"] ですべて drop しても通常動作には影響しないのが原則であり、必要な capability は capabilities.add で個別に追加する「allowlist」方式が本番のベストプラクティスです。
nginx の場合、デフォルトイメージ(nginx:1.27-alpine)は port 80 を listen するため CAP_NET_BIND_SERVICE が必要で、root として起動する設計になっています。
一方、nginxinc/nginx-unprivileged イメージは port 8080 で動作する非 root バリアントで、CAP_NET_BIND_SERVICE も不要なため drop: ["ALL"] で完全に capability を剥奪してもそのまま動きます。本演習②ではこの unprivileged バリアントを採用して多段防御の完全適用を実現します。
readOnlyRootFilesystem と emptyDir の組合せ
readOnlyRootFilesystem: true はルート FS への書き込みをカーネルレベルで禁止する強力な防御ですが、多くのアプリケーションは「ログを書く」「テンポラリファイルを生成する」「pid ファイルを保存する」等で書き込み可能なディレクトリを必要とします。これを両立させるのが emptyDir volume との組合せです。
| アプリ | 書き込みが必要なパス | emptyDir で対応する mountPath |
|---|---|---|
| nginx (unprivileged) | /tmp / /var/cache/nginx / /var/run | 3 つの emptyDir を mount(本演習②) |
| Payara Micro | /tmp / Payara のキャッシュディレクトリ | 2 つの emptyDir(fanclub-backend を non-root 化した将来形) |
| PostgreSQL | /var/lib/postgresql/data / /tmp | PVC(データ)+ emptyDir(一時) |
emptyDir は Pod の lifecycle に紐付くテンポラリ volume で、ep9 で扱った PVC とは異なり Pod 削除時にデータも消えます。
本演習②の nginx-secure では「ルート FS は read-only だが、/tmp と /var/cache/nginx と /var/run の 3 ディレクトリだけは emptyDir で書き込み可能」というハイブリッド構成にします。これがコンテナを read-only で運用するための標準パターンです。
Pod Security Standards (PSS) との関係
K8s v1.25+ では、PodSecurityPolicy (PSP) の後継として Pod Security Standards (PSS) が標準提供されています。
PSS は 3 段階のプロファイル(privileged / baseline / restricted)を定義し、Namespace のラベルで「この Namespace では restricted プロファイル違反の Pod を reject する」という指定が可能になります。
| プロファイル | 厳格度 | 主要な要件 |
|---|---|---|
privileged | 緩い(無制限) | 制約なし。何でも許可 |
baseline | 中程度 | privileged コンテナ禁止 / host namespace 共有禁止 / capabilities ADD 制限 |
restricted | 厳格 | runAsNonRoot: true 必須 / allowPrivilegeEscalation: false 必須 / capabilities.drop ALL 必須 / seccompProfile 必須 / readOnly root 推奨 |
本演習②の nginx-secure.yaml は PSS の restricted プロファイル要件を満たす構成になっています。
第3巻 CKS で Namespace に pod-security.kubernetes.io/enforce: restricted ラベルを付けて PSS Admission を有効化する演習を実施しますが、本回ではフィールド単位の設定にフォーカスし PSS 自体は概念紹介に留めます。
本演習②の SecurityContext 設計が PSS restricted の合格基準であることを認識しておくと、ep18 〜 第3巻でこの土台が活きます。
やってみよう②: nginx Pod に多段防御 SecurityContext を適用する
所要時間目安:約 25 分。nginxinc/nginx-unprivileged:1.27-alpine イメージをベースに、Pod レベル + Container レベル両方の SecurityContext と 3 つの emptyDir volume を組合せた nginx-secure Pod を作成します。
起動成功確認 → uid 確認 → ルート FS への書き込み試行で reject 確認 → emptyDir 経由なら書ける確認 → capabilities がすべて drop されている確認 → クリーンアップの流れを実機検証します。
所要時間の内訳は YAML 作成と apply 5 分・Pod レベル各種確認 10 分・read-only 動作と emptyDir 動作の対比 5 分・capabilities 検証 3 分・削除 2 分です。
Step 1: nginx-secure.yaml を作成する(YAML 全量)
目的:多段防御 SecurityContext と emptyDir 3 個を組合せた nginx Pod YAML を作成する。本演習で扱う SecurityContext の全フィールド推奨値をこの 1 ファイルに集約します。
実行コマンド:
$ cat > nginx-secure.yaml <<'EOF'
apiVersion: v1
kind: Pod
metadata:
name: nginx-secure
namespace: default
labels:
app: nginx-secure
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
seccompProfile:
type: RuntimeDefault
containers:
- name: nginx
image: nginxinc/nginx-unprivileged:1.27-alpine
imagePullPolicy: IfNotPresent
ports:
- containerPort: 8080
securityContext:
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
resources:
requests:
cpu: "50m"
memory: "64Mi"
limits:
cpu: "100m"
memory: "128Mi"
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: {}
EOF
YAML の構造を整理します。
- Pod レベル
spec.securityContext:runAsNonRoot/runAsUser/runAsGroup/fsGroup/seccompProfileの 5 項目。Pod 全体に効くものと FS 関連を配置 - Container レベル
spec.containers[].securityContext:readOnlyRootFilesystem/allowPrivilegeEscalation/capabilities.dropの 3 項目。Container 個別に効くものを配置 - resources:ep14 で確立した requests / limits を 4 値明示(kind の CPU 余裕 450m に収まる小型サイズ)
- volumeMounts と volumes:3 つの emptyDir(
/tmp//var/cache/nginx//var/run)。readOnly ルート FS と共存させるための定石パターン
imagePullPolicy を IfNotPresent にしているのは、ep4 で確立した alma-proxy 経由 docker.io の pull 設定を再利用するためです。kind のローカルキャッシュに同イメージが残っていれば pull せずに起動し、無ければ alma-proxy 経由で取得します。
Step 2: nginx-secure Pod を apply して起動を確認する
目的:YAML を apply して Pod が Running 状態になることを確認する。SecurityContext 違反があると Pod 作成時 / 起動時にエラーが出るため、Running まで到達すれば多段防御の構成が機能している証拠になります。
実行コマンド:
$ kubectl apply -f nginx-secure.yaml
$ kubectl wait --for=condition=Ready pod/nginx-secure --timeout=60s
$ kubectl get pod nginx-secure
期待される実行結果:
pod/nginx-secure created
pod/nginx-secure condition met
NAME READY STATUS RESTARTS AGE
nginx-secure 1/1 Running 0 15s
1/1 Running が確認できれば、SecurityContext の各フィールドが nginx-unprivileged イメージと整合して Pod が正常起動したことになります。RESTARTS: 0 も重要で、起動後に「ルート FS に書き込もうとして失敗 → CrashLoopBackOff」のようなエラーが発生していないことを示しています。
Step 3: Pod に適用された SecurityContext を確認する
目的:kubectl get -o jsonpath で実際に Pod 内に設定された SecurityContext を出力し、YAML で書いた値が API Server に登録されたことを確認する。
実行コマンド:
$ kubectl get pod nginx-secure -o jsonpath='{.spec.securityContext}' | jq
期待される実行結果:
{
"fsGroup": 1000,
"runAsGroup": 1000,
"runAsNonRoot": true,
"runAsUser": 1000,
"seccompProfile": {
"type": "RuntimeDefault"
}
}
Pod レベルの SecurityContext 5 項目が登録されています。続いて Container レベルも確認します。
実行コマンド:
$ kubectl get pod nginx-secure -o jsonpath='{.spec.containers[0].securityContext}' | jq
期待される実行結果:
{
"allowPrivilegeEscalation": false,
"capabilities": {
"drop": [
"ALL"
]
},
"readOnlyRootFilesystem": true
}
Container レベルの 3 項目が登録されています。Pod レベル 5 項目 + Container レベル 3 項目で計 8 項目の多段防御が機能している状態です。jq でフォーマットすることで、ネストした jsonpath の出力が読みやすくなります。
Step 4: コンテナ内の実行 uid を id コマンドで確認する
目的:runAsUser: 1000 / runAsGroup: 1000 が実際にコンテナ内のプロセスに反映されていることを id コマンドで確認する。
実行コマンド:
$ kubectl exec nginx-secure -- id
期待される実行結果:
uid=1000 gid=1000 groups=1000
uid と gid がすべて 1000 になっており、コンテナ内のプロセスが root(uid=0)ではなく uid 1000 として動いていることが確認できました。
出力に (nginx) のような名前が付かず数字の 1000 だけで表示されるのは、SecurityContext の runAsUser: 1000 で強制した uid 1000 がコンテナイメージの /etc/passwd に名前付きで登録されていないためです。
uid と名前のマッピングがなくても実行 uid 自体は確実に 1000 に切り替わっており、非 root 化の目的は達成されています。
同様に、Pod 内で動いている nginx プロセスの実 uid を ps で確認すると、すべて uid 1000 で動いていることが分かります。
実行コマンド:
$ kubectl exec nginx-secure -- ps -ef
期待される実行結果:
PID USER TIME COMMAND
1 1000 0:00 nginx: master process nginx -g daemon off;
28 1000 0:00 nginx: worker process
29 1000 0:00 nginx: worker process
35 1000 0:00 ps -ef
nginx master / worker / ps コマンド自身すべてが uid 1000 で動いており、root プロセスが 1 つも存在しません。USER 列が 1000 という数字表示なのは id コマンドと同じ理由(uid 1000 が /etc/passwd 未登録)です。
この状態であれば、仮にコンテナが侵害されてシェルアクセスを得られても、攻撃者は root 権限を持たないため host の kernel resource にアクセスする難易度が大幅に上がります。
Step 5: ルート FS への書き込み試行で reject 確認
目的:readOnlyRootFilesystem: true が機能していることを、ルート FS への書き込み試行が拒否される実機で確認する。
実行コマンド:
$ kubectl exec nginx-secure -- touch /test.txt
期待される実行結果:
touch: /test.txt: Read-only file system
command terminated with exit code 1
Read-only file system(Linux errno EROFS)のエラーで書き込みが拒否されました。これは Linux kernel レベルの拒否で、コンテナ内のプロセスから見ると「FS が read-only でマウントされている」ため、書き込み試行は syscall レベルで即座に EROFS が返ります。
アプリケーションコードがログを書き込もうとしても、設定ファイルを生成しようとしても、攻撃者が永続化バックドアを設置しようとしても、すべて同じ EROFS で拒否されます。
別のディレクトリ(/etc/nginx や /usr/local/bin)でも同様の拒否が起きます。
実行コマンド:
$ kubectl exec nginx-secure -- sh -c 'echo malicious > /etc/nginx/evil.conf'
期待される実行結果:
sh: can't create /etc/nginx/evil.conf: Read-only file system
command terminated with exit code 1
nginx の設定ファイルディレクトリも read-only として保護されており、ランタイムでの設定改ざんが kernel レベルで防がれています。本番では「Pod を侵害してもアプリの設定ファイルや実行バイナリの改ざんは不可能」という強い保証が、この 1 行(readOnlyRootFilesystem: true)で得られることになります。
Step 6: emptyDir 経由なら書ける確認
目的:ルート FS が read-only でも、emptyDir で mount された /tmp や /var/cache/nginx や /var/run は書き込み可能であることを確認する。
実行コマンド:
$ kubectl exec nginx-secure -- touch /tmp/test.txt
$ kubectl exec nginx-secure -- ls -la /tmp/test.txt
$ kubectl exec nginx-secure -- touch /var/cache/nginx/test-cache.txt
$ kubectl exec nginx-secure -- ls -la /var/cache/nginx/test-cache.txt
期待される実行結果:
-rw-r--r-- 1 1000 1000 0 May 16 07:57 /tmp/test.txt
-rw-r--r-- 1 1000 1000 0 May 16 07:57 /var/cache/nginx/test-cache.txt
touch コマンドがエラーなく完了し、生成されたファイルの所有者が 1000:1000(uid:gid)になっています(uid 1000 が /etc/passwd 未登録のため名前ではなく数字で表示)。
これは Pod レベル securityContext の fsGroup: 1000 が emptyDir に対して chown を実施した結果で、コンテナ内のプロセス(uid 1000)が書き込み可能な権限を持っています。
fsGroup を設定しないと emptyDir の所有者が root のまま残り、uid 1000 のプロセスが書き込めなくなる事故が起きやすいため、SecurityContext で uid 1000 を強制する場合は fsGroup: 1000 もセットで設定するのが定石です。
nginx は /var/cache/nginx に各種キャッシュファイルを書き、/var/run に pid ファイルを書く設計のため、これらのディレクトリが書き込み可能でないと起動できません。3 つの emptyDir で必要な書き込みパスをカバーすることで、ルート FS read-only と nginx の動作要件の両立が実現します。
Step 7: capabilities がすべて drop されていることを確認する
目的:capabilities.drop: ["ALL"] が機能していることを /proc/self/status から確認する。
CapEff(Effective capabilities)/ CapBnd(Bounding capabilities)/ CapPrm(Permitted capabilities)の 3 つがすべて 0 になっていれば、コンテナ内のプロセスは特権操作を一切実行できない状態であることを意味します。
実行コマンド:
$ kubectl exec nginx-secure -- sh -c 'cat /proc/self/status | grep -E "CapEff|CapBnd|CapPrm|CapInh"'
期待される実行結果:
CapInh: 0000000000000000
CapPrm: 0000000000000000
CapEff: 0000000000000000
CapBnd: 0000000000000000
すべての値が 0 = capabilities が完全に drop されている状態です。各行の意味を整理します。
- CapInh(Inheritable):子プロセスに継承される capability。0 なので子プロセスにも特権は引き継がれない
- CapPrm(Permitted):プロセスが取得可能な capability の上限。0 なので一切の特権を取得できない
- CapEff(Effective):現在有効な capability。0 なので現在も特権なし
- CapBnd(Bounding):プロセスツリー全体での capability 上限(最も重要な制約)。0 なので何をしても特権を取得できない
これら 4 行の値はすべて hex で表示され、ビットマスクとして Linux capabilities を表現しています。各 bit が 1 つの capability に対応しており、bit が 1 なら有効、0 なら drop の意味です。
0000000000000000 は全 bit が 0 という意味で、capabilities が完全に drop されていることを示します。
参考までに、後述の fanclub-backend Pod の値 00000000a80425fb をビット展開すると CAP_CHOWN(ファイル所有者変更)/ CAP_NET_BIND_SERVICE(1024 番未満のポート bind)/ CAP_SETUID / CAP_SETGID など Docker / containerd デフォルトの 14 個の capability が含まれます。
本演習②の構成では、コンテナ内で侵害が起きても攻撃者は CAP_NET_ADMIN による NW 操作、CAP_SYS_ADMIN による mount 操作、CAP_SETUID による uid 変更等の特権操作を一切実行できません。
比較として、fanclub-backend Pod(多段防御を適用していない通常の Pod)の同じ値を確認してみます。
実行コマンド:
$ FBPOD=$(kubectl get pod -l app=fanclub-backend -o jsonpath='{.items[0].metadata.name}')
$ kubectl exec $FBPOD -- sh -c 'cat /proc/self/status | grep -E "CapEff|CapBnd|CapPrm|CapInh"'
期待される実行結果:
CapInh: 0000000000000000
CapPrm: 00000000a80425fb
CapEff: 00000000a80425fb
CapBnd: 00000000a80425fb
fanclub-backend では CapPrm / CapEff / CapBnd に 00000000a80425fb という値が立っており、これは Docker / containerd デフォルトの 14 個の capabilities をすべて保持している状態です。攻撃者が侵害すれば、これら 14 個の特権操作を利用できる余地があります。
nginx-secure の 0000000000000000 との対比で、SecurityContext を適用した場合の防御効果が定量的に分かる形になっています。
Step 8: nginx-secure Pod を削除する(クリーンアップ)
目的:本演習で作成した nginx-secure Pod を削除し、default ns のリソース構成を演習開始時の状態に戻す。pod-reader Role / RoleBinding は ep16 で再利用するため残します。
実行コマンド:
$ kubectl delete pod nginx-secure
$ kubectl get pod nginx-secure 2>&1 | head -3
期待される実行結果:
pod "nginx-secure" deleted from default namespace
Error from server (NotFound): pods "nginx-secure" not found
kubectl v1.35 のメッセージフォーマットでは deleted from <ns> namespace という形式で削除元の Namespace が明示されます。続けて kubectl get pod nginx-secure で NotFound が返れば、Pod とその emptyDir volume がすべて削除完了です。
emptyDir は Pod の lifecycle に紐付くため、Pod 削除と同時に /tmp/test.txt や /var/cache/nginx/test-cache.txt も消滅します。
演習②まとめ
- nginx-unprivileged イメージをベースに、Pod レベル + Container レベル両方の SecurityContext と 3 つの emptyDir を組合せた多段防御 Pod を作成した
- uid 1000 での実行・ルート FS read-only・capabilities all drop の 3 防御層が、それぞれ実機で「id 出力」「touch のエラー」「/proc/self/status の 0 値」として観察できることを確認した
- emptyDir 経由なら書き込み可能で、
fsGroup: 1000が emptyDir の所有グループを uid 1000 に自動 chown する仕組みを把握した - fanclub-backend(多段防御未適用)との CapEff 値の対比で、本回の SecurityContext が capabilities 14 個分の防御を加えていることを定量的に確認した
- 演習で作成した nginx-secure Pod は削除済・pod-reader Role / RoleBinding は ep16 で再利用するため残置
fanclub-backend を non-root 化する場合の課題 — Dockerfile レベル改修
演習②では nginx-unprivileged イメージを使って多段防御を成功させましたが、現在の fanclub-backend Pod に同じ SecurityContext を適用するとどうなるかを本セクションで確認します。
ここでは「現状の fanclub-backend:0.1.0 イメージに runAsNonRoot: true を強制した場合の失敗パターン」を実機で観察し、Dockerfile レベルでの改修が必要になる根本理由を整理します。本回の演習②を fanclub-backend ではなく nginx ベースにした理由がここで明確になります。
fanclub-backend:0.1.0 イメージの現状
ep3 で作成した fanclub-backend イメージは、Maven でビルドした Payara Micro + Jakarta EE 11 アプリを実行するもので、Dockerfile には USER 命令が含まれていません。USER 命令がない場合、Docker / containerd は uid 0(root)でプロセスを起動します。
ep10 以降の演習で fanclub-backend Pod 内のプロセスは終始 root として動作しており、本回 Lead 実機観察でも以下が確認されています。
実行コマンド:
$ FBPOD=$(kubectl get pod -l app=fanclub-backend -o jsonpath='{.items[0].metadata.name}')
$ kubectl exec $FBPOD -- id
期待される実行結果:
uid=0(root) gid=0(root) groups=0(root)
uid 0 / gid 0 で稼働しており、Java プロセスも sh プロセスもすべて root として動いています。この状態で SecurityContext の runAsNonRoot: true を強制するとどうなるかを試してみます。
runAsNonRoot: true 強制での失敗パターン
fanclub-backend Deployment に runAsNonRoot: true を patch で追加して動作を確認します(実機検証用・本セクション終了時に元に戻します)。
実行コマンド:
$ kubectl patch deployment fanclub-backend-deployment -n default \
-p '{"spec":{"template":{"spec":{"securityContext":{"runAsNonRoot":true}}}}}'
$ sleep 6
$ kubectl get pod -l app=fanclub-backend -n default
期待される実行結果:
deployment.apps/fanclub-backend-deployment patched
NAME READY STATUS RESTARTS AGE
fanclub-backend-deployment-858545b566-bvwn6 0/1 Init:CreateContainerConfigError 0 6s
fanclub-backend-deployment-86cf676cf7-g7h2z 1/1 Running 0 2d17h
fanclub-backend-deployment-86cf676cf7-gqt2j 1/1 Running 0 2d17h
Rolling Update で新 Pod(ReplicaSet ハッシュ 858545b566)が起動を試みますが、Init:CreateContainerConfigError ステータスで停止しています。ステータスに Init: プレフィックスが付いている点が重要で、これは Init Container の段階で失敗していることを示します。
fanclub-backend Deployment には ep7 で追加した Init Container wait-for-db(busybox イメージ)があり、Pod レベルの runAsNonRoot: true は Init Container にも適用されるため、メインコンテナより先に Init Container が root 実行チェックに引っかかります。kubectl describe pod で詳細を確認します。
実行コマンド:
$ NEWPOD=$(kubectl get pod -l app=fanclub-backend -n default -o jsonpath='{.items[?(@.status.phase!="Running")].metadata.name}' | awk '{print $1}')
$ kubectl describe pod $NEWPOD -n default | grep -A 6 "Events:"
期待される実行結果:
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 6s default-scheduler Successfully assigned default/fanclub-backend-deployment-858545b566-bvwn6 to kind-control-plane
Normal Pulled 5s (x2 over 5s) kubelet spec.initContainers{wait-for-db}: Container image "busybox:1.36" already present on machine and can be accessed by the pod
Warning Failed 5s (x2 over 5s) kubelet spec.initContainers{wait-for-db}: Error: container has runAsNonRoot and image will run as root (pod: "fanclub-backend-deployment-858545b566-bvwn6_default(...)", container: wait-for-db)
エラーメッセージの肝:spec.initContainers{wait-for-db}: Error: container has runAsNonRoot and image will run as root という形式で、「SecurityContext は runAsNonRoot: true を要求しているが、コンテナイメージは root で動くよう構成されている」というミスマッチが kubelet レベルで検出され、Pod の起動が拒否されています。
注目すべきは失敗しているコンテナが wait-for-db(Init Container・busybox イメージ)である点です。
Pod レベルの securityContext は Init Container にも適用されるため、メインの fanclub-backend コンテナの評価に到達する前に Init Container の busybox が root 実行チェックで弾かれます。
これは Admission 層ではなく kubelet の起動前検証で発生するエラーのため、Pod は Pending や CrashLoopBackOff ではなく Init:CreateContainerConfigError という独特の status を取ります。
注: 上記のエラー文言は本シリーズで実機検証された fanclub-backend:0.1.0 image(Init Container は busybox:1.36)に対する実例です。
container has runAsNonRoot and image will run as root のキーワードと container: wait-for-db の表記から、「どのコンテナが」「なぜ」失敗したかを読み取れます。
fanclub-backend を本当に non-root 化するには、メインコンテナだけでなく Init Container の busybox も含めて全コンテナを非 root 対応にする必要があります。
Pod を元に戻す(runAsNonRoot 削除)
確認が終わったので、Deployment から runAsNonRoot 設定を削除して fanclub-backend を Running 状態に戻します。kubectl patch で securityContext を空オブジェクトに上書きする方法を使います。
実行コマンド:
$ kubectl patch deployment fanclub-backend-deployment -n default \
--type=json \
-p='[{"op":"remove","path":"/spec/template/spec/securityContext"}]'
$ kubectl rollout status deployment/fanclub-backend-deployment -n default
期待される実行結果:
deployment.apps/fanclub-backend-deployment patched
deployment "fanclub-backend-deployment" successfully rolled out
securityContext フィールド全体を削除する type=json + op=remove のパッチ方式で、Deployment が元の状態に戻りました。kubectl get pod -l app=fanclub-backend -n default で 2 Pod が Running になっていることを確認しておきます。
解決策 — Dockerfile レベルの改修
fanclub-backend を真に non-root 化するには、Dockerfile を修正してイメージを再ビルドする必要があります。具体的な改修ポイントは以下の 4 つです。
- USER 命令の追加:
USER 1000:1000を Dockerfile の最終ステージに追加。これでイメージのデフォルト実行 uid が 1000 になり、SecurityContext で runAsNonRoot を強制しても起動可能になる - app ディレクトリの所有者変更:
COPY --chown=1000:1000で payara-micro.jar 等のアプリファイルを uid 1000 が読める所有者に設定 - tmp / cache ディレクトリの準備:Payara が書き込みを必要とするディレクトリを emptyDir で mount する前提で、Dockerfile では
mkdir -pしておく - イメージタグを 0.3.0 に更新:non-root 化バリアントとして区別するため新タグでビルド・ep18(HTTPS 公開)で正式採用する流れにする
本回のスコープ判断:fanclub-backend の Dockerfile 改修とイメージ再ビルドは本回スコープ外です。
本回は「SecurityContext の機構を学ぶ」ことが目的で、イメージビルドのフェーズは ep3 で完結しているため、再ビルドを伴う改修は ep18(HTTPS 公開・fanclub-backend:0.3.0 として再ビルド)に集約する設計にしています。
本回で fanclub-backend に SecurityContext を「正しく」適用できないのは、コンテナイメージ側の前提条件(USER 1000 で動くこと)が満たされていないからであり、設計の不備ではなく学習段階の進捗順序の結果です。
ep18 完了時には fanclub-backend:0.3.0 として non-root 化された状態で動き、本回で学んだ多段防御を完全適用できる状態になります。
Admission Controller 概念 — Validating / Mutating Webhook
H2-2 で整理した 3 層のうち、層 3(Admission Control)の機構を本セクションで体系的に整理します。Admission Controller は「リクエスト内容そのものを検証または変更する」役割を持つ Plugin / Webhook の総称で、組込 Plugin と外部 Webhook の 2 系統があります。本回は概念整理に集中し、実演習は H2-11(演習③)で行います。
Admission Controller の 2 系統
| 系統 | 実装場所 | 例 | 本シリーズでの扱い |
|---|---|---|---|
| 組込 Admission Plugin | API Server バイナリ内(kube-apiserver) | LimitRanger / ResourceQuota / ServiceAccount / NamespaceLifecycle / DefaultStorageClass / PodSecurity | ep14(LimitRanger / ResourceQuota)で間接的に使用済 |
| 外部 Admission Webhook | クラスタ内 / 外部の Webhook Service | ValidatingAdmissionWebhook / MutatingAdmissionWebhook(cert-manager / Istio / OPA Gatekeeper / Kyverno / Linkerd 等が登録) | ep18 で cert-manager 経由・第3巻で OPA Gatekeeper / Kyverno 本格利用 |
組込 Plugin は API Server の起動時に --enable-admission-plugins フラグで有効化される C++ / Go コードで、Plugin として API Server のバイナリに baked-in されています。
一方、外部 Webhook は K8s のリソースとして ValidatingWebhookConfiguration / MutatingWebhookConfiguration を作成すると、API Server が外部 HTTPS endpoint(クラスタ内 Service or 外部 URL)に Admission Review リクエストを投げる動きをします。
両者は同じ「Admission 層」の責務を果たしますが、組込は K8s 自身のセットアップ時に存在し、外部 Webhook はオプション的に後から追加できる点が異なります。
Validating Webhook と Mutating Webhook の役割分担
| Webhook 種別 | 役割 | 動作タイミング | 代表例 |
|---|---|---|---|
| Mutating Admission Webhook | リクエスト内容を変更する。Pod の spec に label を自動付与 / sidecar コンテナを自動 inject / default value を補完 等 | Authn / Authz 通過後・Validating より先に動作 | Istio(sidecar 自動 inject)/ LimitRanger(resources 自動補完) |
| Validating Admission Webhook | リクエスト内容を検証し、ポリシー違反なら reject する。変更は不可 | Mutating の後・etcd 書き込み前 | OPA Gatekeeper(CIS ベンチマーク準拠検証)/ ResourceQuota(Quota 違反検出) |
2 つの Webhook は Mutating → Validating の順で動きます。これは「変更を先に適用し、最終形態に対して検証を行う」という設計で、Validating が見るのは Mutating によって変更された後の状態です。
たとえば LimitRanger(Mutating 的に動く組込 Plugin)が resources を自動補完してから ResourceQuota(Validating 的に動く組込 Plugin)が Quota 違反検出するという順序が、Validating が補完後の resources を見て Quota チェックする動きを実現しています。
組込 Admission Plugin の代表例
| Plugin 名 | 役割 | 本シリーズの初出回 |
|---|---|---|
NamespaceLifecycle | 削除中の Namespace でのリソース作成を禁止 | 本回 |
LimitRanger | LimitRange の default / max を Pod 作成時に適用 | ep14 |
ResourceQuota | Quota 違反の Pod / リソース作成を reject | ep14 |
ServiceAccount | Pod に SA Token を mount / default SA を補完 | ep10 |
DefaultStorageClass | PVC に storageClass 未指定時のデフォルト補完 | ep9(local-path がデフォルト) |
PodSecurity | Pod Security Standards (PSS) を Namespace ラベルに基づいて enforce/warn/audit | 第3巻 |
MutatingAdmissionWebhook | 登録された MutatingWebhookConfiguration を呼び出すメタ Plugin | — |
ValidatingAdmissionWebhook | 登録された ValidatingWebhookConfiguration を呼び出すメタ Plugin | — |
本シリーズの第1巻範囲ではすでに 5 つの組込 Plugin(NamespaceLifecycle / LimitRanger / ResourceQuota / ServiceAccount / DefaultStorageClass)の挙動を間接的に観察済です。
これらは API Server のデフォルト構成で有効化されており、ユーザーは特に設定せずとも恩恵を受けています。本回でこれらが「Admission Plugin」というカテゴリーに属することを明確に整理しておくと、第3巻で OPA Gatekeeper / Kyverno による外部 Webhook 拡張を扱うときの土台になります。
外部 Admission Webhook の動作フロー
外部 Webhook がリクエストを処理する流れを整理します。本演習③で kubectl get validatingwebhookconfigurations で確認するリソースが、このフローの登録情報を保持しています。
- (1) ユーザーが
kubectl apply -f pod.yaml等で API Server にリクエストを送信 - (2) Authn 層が kubeconfig の client cert で identity 確定
- (3) Authz 層(RBAC)が verb / resource 権限を判定して通過
- (4) 組込 Mutating Plugin(ServiceAccount / LimitRanger 等)が Pod spec を変更
- (5) 外部 MutatingAdmissionWebhook が
MutatingWebhookConfigurationで登録された URL に AdmissionReview を POST - (6) Webhook が JSON Patch を返して Pod spec をさらに変更(sidecar inject 等)
- (7) 組込 Validating Plugin(ResourceQuota / PodSecurity 等)が変更後の Pod spec を検証
- (8) 外部 ValidatingAdmissionWebhook が
ValidatingWebhookConfigurationで登録された URL に AdmissionReview を POST - (9) Webhook が allowed: true/false を返して許可 or reject 判定
- (10) すべて通過すれば API Server が etcd に Pod を書き込み
(5) と (8) の外部 Webhook 呼び出しでは、API Server が MutatingWebhookConfiguration / ValidatingWebhookConfiguration リソースから clientConfig.service(クラスタ内 Service)や clientConfig.url(外部 URL)を読み取り、HTTPS で AdmissionReview JSON を POST します。
Webhook は数百 ms 以内にレスポンスを返す必要があり、Webhook が落ちると API Server 全体が遅延 / 機能不全になるため、本番では Webhook の HA と failurePolicy(Ignore / Fail)の設計が重要トピックです(第3巻 CKS で詳述)。
本シリーズで登場する外部 Webhook
| Webhook 提供元 | 用途 | 導入回 |
|---|---|---|
| cert-manager | Certificate / Issuer CRD の Validating + ACME Order の Mutating | ep18(本シリーズ) |
| Gateway API | Gateway / HTTPRoute CRD の Validating | ep18(本シリーズ) |
| Traefik | Traefik IngressRoute CRD の Validating | ep18(本シリーズ) |
| OPA Gatekeeper | クラスタワイドのポリシー(CIS / 独自ルール)の Validating | 第3巻 ep4 |
| Kyverno | YAML ベースのポリシー Mutating + Validating | 第3巻 ep4 |
| Falco | ランタイム検知の event Webhook(厳密には Admission ではない) | 第3巻 ep11 |
cert-manager / Gateway API / Traefik は ep18 で導入し、それぞれが ValidatingWebhookConfiguration / MutatingWebhookConfiguration を kind クラスタに登録します。
本演習③で kubectl get validatingwebhookconfigurations を実行するのは、これらの Webhook が「まだ存在しない」現状を把握しておき、ep18 で導入された後にどう変化するかの基準点を残すためです。
CustomResourceDefinition (CRD) 利用概念 — K8s 拡張の入口
CRD(CustomResourceDefinition)は K8s API を拡張するための機構で、ユーザー定義のリソース型を K8s に登録できます。
Gateway / HTTPRoute / Certificate / Issuer / Rollout / FalcoRule など、K8s 本体に組込ではないリソース型はすべて CRD として外部から提供されています。本セクションでは CRD の概念・既存 CRD 例・利用パターンを整理します。
CRD 自身を定義する作業(Operator 開発)は CKAD 範囲外で、本シリーズでも「すでに公開されている CRD を kubectl apply で利用する」立場に集中します。
CRD が解決する問題
K8s 本体には Pod / Service / Deployment / StatefulSet / DaemonSet / ConfigMap / Secret / Job / CronJob / Namespace 等の「組込リソース型」が約 50 種類あります。これらは kubectl api-resources で一覧表示できます。
しかし「Certificate を管理したい」「HTTP Route を定義したい」「PostgreSQL Cluster を宣言的に運用したい」のような業務領域固有のリソース型は組込にはなく、各種ツール(cert-manager / Gateway API / CloudNativePG 等)が CRD として独自リソース型を提供する形になっています。
- kubectl での統一操作:CRD は kubectl の
get/describe/apply/deleteがそのまま使える。独自 CLI を学ぶ必要がない - YAML での宣言的管理:CRD リソースも組込リソース同様 YAML で記述。GitOps(ArgoCD / Flux)と統合可能
- RBAC との統合:CRD リソースに対しても Role / ClusterRole で権限制御可能。
apiGroups: ["cert-manager.io"]/resources: ["certificates"]等で指定 - K8s API のクラスタ拡張:API Server を改造せずに、CRD + Operator パターンでカスタムロジックをクラスタに組み込める
記憶のコツは「CRD は K8s に新しいリソース型を教える機構」「Operator はその新リソース型を動かす Controller の総称」という役割分担です。CRD だけでは何も動かず、Operator(Controller)が CRD の状態を監視して実際の動作(Certificate の発行 / Pod の管理 等)を行います。両者がセットになって 1 つの「拡張機能」を構成します。
本シリーズで登場する CRD 一覧(先取り)
| CRD | API グループ | 役割 | 導入回 |
|---|---|---|---|
Gateway / HTTPRoute / GatewayClass | gateway.networking.k8s.io | Ingress 後継の標準 API(Traefik で実装) | ep18 |
Certificate / Issuer / ClusterIssuer | cert-manager.io | TLS 証明書の宣言的管理 | ep18 |
IngressRoute / TLSStore | traefik.io | Traefik 独自の拡張ルーティング | ep18 |
Rollout / AnalysisRun | argoproj.io | Blue/Green / Canary を Operator で自動化(第2巻 GitOps 範囲) | 第2巻 |
Cluster(PostgreSQL) | postgresql.cnpg.io | CloudNativePG による PostgreSQL Cluster 管理 | 第3巻 |
ConstraintTemplate / Constraint | templates.gatekeeper.sh | OPA Gatekeeper のポリシー定義 | 第3巻 ep4 |
FalcoRule | falco.org | Falco のランタイム検知ルール | 第3巻 ep11 |
ep18 で 3 つの CRD グループ(Gateway API / cert-manager / Traefik)を一気に導入し、第1巻完走時点でクラスタには 10 種類以上の CRD が登録される構成になります。
第2巻・第3巻ではさらに ArgoCD / OPA / Falco / CloudNativePG が追加され、CRD 数は数十〜100 種類規模になります。Kubernetes エンジニアの実務は「組込リソース 50 種類 + CRD 数十種類」を扱う仕事になるため、CRD の存在と仕組みを早期に把握しておくことが重要です。
CRD の確認コマンド
| 目的 | コマンド |
|---|---|
| クラスタ内の全 CRD を一覧 | kubectl get crds |
| CRD の詳細(フィールド構造等) | kubectl describe crd <name> |
| CRD で定義されたリソースの一覧 | kubectl get <crd-resource-name>(例: kubectl get certificates) |
| CRD で定義されたリソース型の API グループ確認 | kubectl api-resources --api-group=<group>(例: --api-group=cert-manager.io) |
| CRD で定義されたフィールド構造の確認 | kubectl explain <resource>.<field>(例: kubectl explain certificate.spec) |
CRD は kubectl の組込コマンドでそのまま操作可能で、独自 CLI を学ぶ必要がないのが K8s 拡張の良いところです。本演習③では kubectl get crds で現状確認を行い、ep18 で Gateway API / cert-manager / Traefik を導入した後にどう変化するかの基準を残します。
やってみよう③: Admission Webhook + CRD の存在確認
所要時間目安:約 15 分。本演習は実機にリソースを作成せず、現状の kind クラスタに存在する Admission 関連 API と CRD を kubectl で確認するだけの「環境理解」演習です。
所要時間の内訳は admission API グループ確認 3 分・Webhook configurations 確認 5 分・CRD 確認 3 分・組込 ClusterRole 確認 4 分です。
Step 1: admissionregistration.k8s.io API グループの存在確認
目的:API Server が Admission Webhook 機能を持っていることを kubectl api-versions で確認する。admissionregistration.k8s.io/v1 が表示されれば、外部 Webhook を登録できる土台が整っていることになります。
実行コマンド:
$ kubectl api-versions | grep admission
期待される実行結果:
admissionregistration.k8s.io/v1
admissionregistration.k8s.io/v1 が GA バージョンで、本番で使うのはこちら。v1beta1 は K8s の旧バージョンで提供されていましたが、kind v0.31.0(K8s v1.35)の実機では v1 のみが表示されます。
外部 Webhook を登録できる土台が整っていることが確認できます。続いて API リソース一覧で Admission 関連を確認します。
実行コマンド:
$ kubectl api-resources --api-group=admissionregistration.k8s.io
期待される実行結果:
NAME SHORTNAMES APIVERSION NAMESPACED KIND
mutatingwebhookconfigurations admissionregistration.k8s.io/v1 false MutatingWebhookConfiguration
validatingadmissionpolicies admissionregistration.k8s.io/v1 false ValidatingAdmissionPolicy
validatingadmissionpolicybindings admissionregistration.k8s.io/v1 false ValidatingAdmissionPolicyBinding
validatingwebhookconfigurations admissionregistration.k8s.io/v1 false ValidatingWebhookConfiguration
4 つのクラスタスコープリソース(NAMESPACED: false)が確認できます。
MutatingWebhookConfiguration:外部 Mutating Webhook の登録用ValidatingWebhookConfiguration:外部 Validating Webhook の登録用ValidatingAdmissionPolicy:K8s v1.30 GA の新しい仕組み。外部 Webhook なしで CEL 式によるポリシー検証を実装可能ValidatingAdmissionPolicyBinding:上記 Policy を Namespace に紐付けるバインディング
後者 2 つ(ValidatingAdmissionPolicy 系)は K8s v1.30 で GA した比較的新しい機構で、外部 Webhook を立てずに CEL(Common Expression Language)で「リクエスト内容をこういう条件で reject する」というポリシーをクラスタ内で完結させられます。
OPA Gatekeeper / Kyverno と競合する位置付けで、第3巻 CKS で詳述します。本回は概念把握にとどめます。
Step 2: 現在のクラスタに登録されている Webhook configurations を確認
目的:kind クラスタに現時点で登録されている ValidatingWebhookConfiguration / MutatingWebhookConfiguration を一覧表示する。ep18 で cert-manager / Gateway API / Traefik を導入した後に、ここに新規 Webhook が追加されることを比較するための基準点を取得します。
実行コマンド:
$ kubectl get validatingwebhookconfigurations
$ kubectl get mutatingwebhookconfigurations
期待される実行結果:
No resources found
No resources found
kind v0.31.0(K8s v1.35)のデフォルト構成では、ValidatingWebhookConfiguration / MutatingWebhookConfiguration はいずれも No resources found で 0 件です。
kindest/node に同梱されている metrics-server や kindnet 等のコンポーネントは外部 Admission Webhook を使わない構成のため、素の kind クラスタには Webhook configurations が一切登録されていません。
重要なのは「ep18 完了時にこのリストが cert-manager 関連 + Gateway API 関連 + Traefik 関連で増える」という現状からの差分を捉えることで、いまは「0 件」が基準点になります。
Step 3: 現在の CRD 一覧を確認
目的:kind クラスタに現時点で登録されている CRD を一覧表示する。基本的に kind v0.31.0 のデフォルト構成では CRD は 0 件で、Gateway API / cert-manager / Traefik はまだ導入されていない素の状態です。
実行コマンド:
$ kubectl get crds
期待される実行結果:
No resources found
No resources found が返れば、現在の kind クラスタには CRD が 1 つも登録されていない状態です。
ep18 で Gateway API CRD(Gateway / HTTPRoute / GatewayClass 等)と cert-manager CRD(Certificate / Issuer / ClusterIssuer 等)と Traefik CRD(IngressRoute / TLSStore 等)を導入すると、ここに 10 〜 20 個程度の CRD が登録される予定です。
CRD は API グループ単位でまとめて確認することもできます。たとえば cert-manager が導入されたか確認したいときは以下のコマンドを使います。
実行コマンド:
$ kubectl api-resources --api-group=cert-manager.io 2>&1 | head -5
$ kubectl api-resources --api-group=gateway.networking.k8s.io 2>&1 | head -5
期待される実行結果:
error: the server doesn't have a resource type "..." in group "cert-manager.io"
error: the server doesn't have a resource type "..." in group "gateway.networking.k8s.io"
「the server doesn’t have a resource type」エラーは「該当 API グループの CRD が登録されていない」状態を示します。ep18 で cert-manager と Gateway API を導入した後は、このコマンドが Certificate / Issuer や Gateway / HTTPRoute 等のリソース型を表示するようになります。
Step 4: 組込 ClusterRole(view / edit / admin / cluster-admin)を確認
目的:H2-4 で言及した K8s 標準提供の 4 つの組込 ClusterRole がクラスタに最初から登録されていることを確認する。CKAD 試験本番でも「組込 ClusterRole を使って権限を付与」する設問が出題されるため、名前と存在を実機で押さえます。
実行コマンド:
$ kubectl get clusterroles | grep -E "^(cluster-admin|admin|edit|view)\s"
期待される実行結果:
admin 4d
cluster-admin 4d
edit 4d
view 4d
4 つの組込 ClusterRole が確認できました。AGE が 4 日(kind クラスタの起動時刻に基づく)になっており、クラスタ起動時に自動作成されていることが分かります。
これらは K8s v1.8 以降で標準化された組込 ClusterRole で、すべての K8s ディストロ(kind / kubeadm / RKE2 / EKS / GKE / AKS 等)で同じ名前で利用可能です。
たとえば view ClusterRole の中身を見てみると、複数の rules で「ほぼ全リソースの get / list / watch」が定義されていることが分かります。
実行コマンド:
$ kubectl describe clusterrole view | head -30
期待される実行結果(抜粋):
Name: view
Labels: kubernetes.io/bootstrapping=rbac-defaults
rbac.authorization.k8s.io/aggregate-to-edit=true
Annotations: rbac.authorization.kubernetes.io/autoupdate: true
PolicyRule:
Resources Non-Resource URLs Resource Names Verbs
--------- ----------------- -------------- -----
bindings [] [] [get list watch]
configmaps [] [] [get list watch]
endpoints [] [] [get list watch]
events [] [] [get list watch]
limitranges [] [] [get list watch]
namespaces [] [] [get list watch]
...
多数のリソースに対して get / list / watch の 3 verb が一括で付与されているのが view ClusterRole の中身です。本演習①で作成した pod-reader Role は「pods のみ」だったのに対し、組込 view は「ほぼ全リソース」を読める分、適用すべき場面が異なります。
「閲覧用のユーザーには組込 view」「特定リソースのみ操作する SA には独自 Role」と使い分けるのが本番の設計です。
演習③まとめ
admissionregistration.k8s.io/v1API グループの存在を確認し、ValidatingWebhookConfiguration / MutatingWebhookConfiguration / ValidatingAdmissionPolicy / ValidatingAdmissionPolicyBinding の 4 リソース型が利用可能であることを把握した- 現在の kind クラスタの Webhook configurations 数を確認し、ep18 で cert-manager / Gateway API / Traefik 導入後に増える差分を捉える基準を確立した
kubectl get crdsで CRD が 0 件であることを確認し、ep18 で 10 〜 20 個の CRD が登録される未来形を理解した- 組込 ClusterRole(cluster-admin / admin / edit / view)の存在を実機で確認し、本演習①の独自 Role と組込 ClusterRole の使い分けを整理した
CKAD 試験頻出パターン + 現場ヒヤリハット 2 件
本セクションでは CKAD 試験本番で出題される RBAC / SecurityContext の頻出パターンを 4 つ整理した後、本番運用で実際に起きやすい事故を 2 件取り上げます。CKAD 試験範囲を一部超える論点も含みますが、本回で扱った機構が「設計を誤ると本番サービスのセキュリティを破綻させる」性質を持つことを認識しておくのが重要です。
試験頻出パターン 4 選
| # | キーワード | 解答パターン | 速攻コマンド |
|---|---|---|---|
| 1 | 「SA に Pod 一覧読み取り権限を付与」 | Role + RoleBinding で SA に pods の get,list,watch verb 付与 | kubectl create role pod-reader --verb=get,list,watch --resource=pods -n <ns> + kubectl create rolebinding pod-reader-binding --role=pod-reader --serviceaccount=<ns>:<sa> -n <ns> |
| 2 | 「全 Namespace 横断で Pod を read-only」 | ClusterRole + ClusterRoleBinding を作成(または組込 view ClusterRole を再利用) | kubectl create clusterrolebinding global-viewer --clusterrole=view --serviceaccount=<ns>:<sa> |
| 3 | 「Pod を非 root で実行」 | Pod spec の securityContext.runAsNonRoot: true + runAsUser: <非0> | YAML で直接記述(速攻コマンドなし) |
| 4 | 「コンテナの権限昇格を防ぐ + capabilities を drop」 | Container spec の securityContext.allowPrivilegeEscalation: false + capabilities.drop: [ALL] | YAML で直接記述(速攻コマンドなし) |
1 番と 2 番は kubectl create role / kubectl create clusterrolebinding のワンライナーで YAML を書かずに即座に作成できる速攻パターンが用意されています。一方、3 番と 4 番は SecurityContext のフィールド設定なので YAML を直接書く形になりますが、内容が短いため暗記して即座に書ける状態を目指します。
SecurityContext 試験対策のミニマル YAML テンプレート
CKAD 試験で「Pod を非 root + 権限昇格防止 + capabilities drop で作成せよ」のような設問が出たときに即座に書ける YAML テンプレートを暗記しておきます。本演習②の nginx-secure.yaml から emptyDir 周りを省いたミニマル版です。
apiVersion: v1
kind: Pod
metadata:
name: secure-pod
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
containers:
- name: app
image: <non-root image>
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
この 10 行程度の YAML が SecurityContext の試験頻出 4 フィールドをカバーします。
試験本番では kubectl run secure-pod --image=<img> --dry-run=client -o yaml > pod.yaml で Pod の素の YAML を生成してから、上記 SecurityContext ブロックを追記する流れで作るのが時間効率的です。
RBAC エラーの読み解き方
| エラーメッセージのキーワード | 原因 | 対処 |
|---|---|---|
User "..." cannot <verb> resource "<res>" in API group "..." in the namespace "<ns>" | 該当 SA / User に該当 verb の権限がない | Role に該当 verb / resource を追加するか、追加の Role + RoleBinding を作る |
The RoleBinding is invalid: roleRef: Invalid value: ... cannot change roleRef | 既存 RoleBinding の roleRef を変更しようとした(roleRef は immutable) | 既存 RoleBinding を delete してから新規作成し直す |
The RoleRef must reference a ClusterRole in the global namespace | ClusterRoleBinding で Role(Namespace スコープ)を参照しようとした | roleRef.kind を ClusterRole に変更するか、RoleBinding に切り替える |
error: container "<name>" must run as non-root user, but is configured to run as user 0 | SecurityContext の runAsNonRoot: true がイメージの USER (root) と矛盾 | イメージの Dockerfile に USER 1000 を追加して再ビルド or runAsUser でイメージの USER を上書き |
Read-only file system | readOnlyRootFilesystem: true 環境でルート FS への書き込みを試行 | 該当ディレクトリを emptyDir で mount するか、readOnlyRootFilesystem: false に変更 |
5 種類のエラーメッセージとそれぞれの原因・対処を覚えておくと、CKAD 試験で「次のエラーの原因として最も適切なのはどれか」型の選択問題に即座に回答できます。本番運用でも RBAC / SecurityContext のトラブルシュート時の最初の手がかりになります。
現場ヒヤリハット 2 件
本回で扱った RBAC / SecurityContext の機構が、本番運用中にどのような事故を引き起こすかを 2 件取り上げます。それぞれ「背景・事故・根本原因・解決策・本番ガードレール」の 5 点セットで整理します。
ヒヤリハット ①: fanclub-backend に runAsNonRoot: true 強制で本番 Pod 全停止
背景:本番運用中の Kubernetes クラスタで「セキュリティ強化施策」として全 Deployment の Pod template に runAsNonRoot: true を一括 patch する PR が出された。
レビュー時には「セキュリティ向上のため」と肯定的に受け止められ、CI のテストも通過したため、staging 環境への適用後に prod 環境にも適用された。staging では Pod が 1 個のみのアプリで動作確認が完了しており、prod でも問題ないと判断された。
事故:prod に patch を apply した数分後、Rolling Update で新 Pod が一斉に CreateContainerConfigError(Init Container を持つ Deployment では Init:CreateContainerConfigError)ステータスに陥り、kubectl describe pod の Events に Error: container has runAsNonRoot and image will run as root が大量に出力された。
既存 Pod は動き続けているが新 Pod が起動できず、Rolling Update が止まったまま maxUnavailable: 0 の保証で旧 Pod のみで稼働。さらに数時間後に Node メンテナンスで Pod 再作成が発生した Deployment では Pod がすべて起動できなくなり、サービスダウンが発生。本番リリースパイプラインも全停止した。
根本原因:「runAsNonRoot: true はコンテナイメージ側が非 root で動くよう構成されている前提で初めて機能する」という認識が抜けていた。
staging 環境のアプリは USER 1000 を Dockerfile で指定したイメージを使っていたため問題が出なかったが、prod の他チームが管理する複数アプリは Dockerfile で USER を指定しておらず root で動く構成だった。
SecurityContext の patch だけでは安全ではなく、イメージ側の改修が必須だったが、レビュー時にチーム横断のイメージ構成確認が抜けていた。
解決策:緊急対応として SecurityContext の patch を revert(kubectl rollout undo + 該当 PR の revert PR を merge)。Deployment が元の状態に戻り、Pod が起動できるようになった。
その後の改善として、全 Deployment のイメージで USER 非 root を確認するための「イメージ監査ジョブ」を CI に追加した(docker inspect <image> --format='{{.Config.User}}' で User フィールドが 0 や空でないことを検証)。
SecurityContext の runAsNonRoot: true は、イメージ監査ジョブが通過したアプリから順次段階適用する形に変更された。
本番ガードレール:
runAsNonRoot: trueを Deployment に追加する PR では、対応するコンテナイメージの Dockerfile にUSER <非0>命令があることを確認する pre-commit hook を設置する- イメージ監査ジョブを CI に組み込み、
docker inspectでConfig.Userが非 root であることを検証する。「USER 未指定 = root」を機械的に検出 - 本番への SecurityContext 強化は段階適用とし、staging で同じイメージ・同じトラフィック条件で動作確認した後に prod に展開する
- クラスタワイドで強制したい場合は、Pod Security Standards (PSS) の
restrictedプロファイルを Namespace ラベルで適用する(pod-security.kubernetes.io/enforce: restricted)。SecurityContext 個別 patch より宣言的 - OPA Gatekeeper / Kyverno で「全 Pod は runAsNonRoot: true が必須」のクラスタワイドポリシーを段階導入(warn → audit → enforce)する
ヒヤリハット ②: automountServiceAccountToken: true 放置で侵害時の権限拡大
背景:本番運用中の Pod のうち、外部からのリクエストを受ける Web フロントエンド Pod が脆弱性スキャンで「攻撃者にシェル取得される可能性のある CVE」が検出された。アプリの修正には数日かかる見込みで、暫定対処として WAF(Web Application Firewall)でブロックしていた。
Pod の SA は default SA を使っており、ServiceAccount Token は Pod の /var/run/secrets/kubernetes.io/serviceaccount/token に自動 mount される設定(automountServiceAccountToken の明示なし = デフォルト true)だった。
事故:WAF をすり抜けた攻撃者が Web フロントエンド Pod でシェル取得に成功。
Pod 内に侵入した攻撃者は、最初に /var/run/secrets/kubernetes.io/serviceaccount/token を読み取り、その Token で API Server を叩いて Pod / Service / ConfigMap / Secret を列挙開始。
default SA には kube-system の各種 SA が持つ「クラスタ内サービス一覧の参照」権限が間接的に付与されており、攻撃者は K8s 内部のサービストポロジー(DB endpoint / Secret 一覧 / 他チームの Service)を把握できる状態になった。
さらに同 Namespace 内に cluster-admin 相当の RoleBinding を持つ別 SA があり、その SA の Secret を読み取って Token を取得したことで cluster-admin への権限昇格が成立。クラスタ全体の Secret / ConfigMap が漏洩した。
根本原因:原因は 3 つの層で発生した。(1) アプリ自体の CVE による侵害許可、(2) SA Token が Pod 内に常に mount されていたことによる初期権限取得、(3) 同 Namespace に cluster-admin 相当の SA が共存しており、Secret 読み取りから Token 奪取への横移動が可能だった点。
automountServiceAccountToken: false を Pod 単位で設定していれば (2) で攻撃連鎖が止まり、被害は CVE 影響範囲のみに留まった。「とりあえずデフォルトのまま」が本番設定として残っていたことが事故の引き金。
解決策:緊急対応として全 Pod の automountServiceAccountToken: false 一括 patch を実施(API 呼び出しが本当に必要な Pod のみ true を残す形にホワイトリスト化)。事後改善として、Pod 作成時に「SA Token が必要かどうか」を明示する設計レビューチェック項目を追加。
Pod 内から K8s API を叩く必要がない(多くの Web アプリ・DB クライアント等)には automount を false で運用するのが原則化された。さらに各 Namespace で「cluster-admin 相当の SA は別 Namespace に分離する」設計に変更。同 Namespace に低権限 SA と高権限 SA が共存する構成を禁止した。
本番ガードレール:
- 全 ServiceAccount の
automountServiceAccountTokenはデフォルトfalseに設定する。SA リソースのレベルで指定し、Pod の override は明示的な justification を要求する - Pod から K8s API を叩く必要がある場合のみ Pod レベルで
automountServiceAccountToken: trueを設定し、対応する Role を SA に最小権限で付与する(本回演習①のパターン) - 本番 Pod に default SA を使わせない。各アプリ専用の SA を作成し、用途を明確化する
- 同 Namespace 内に高権限 SA と低権限 SA を共存させない。cluster-admin 相当の SA は専用 Namespace に隔離
- 侵害想定の threat modeling を定期実施し、「SA Token が漏洩したらどこまで被害が広がるか」を可視化する。OPA Gatekeeper / Kyverno で
automountServiceAccountToken: trueの Pod に label と annotation を強制し、監査ログで追跡可能にする
2 件のヒヤリハットに共通するのは「RBAC / SecurityContext の機構を表面的に適用するだけでは本番のセキュリティが破綻する」という性質です。SecurityContext はイメージ側の前提と組合せて初めて機能し、RBAC は Namespace 設計や SA 設計と組合せて初めて意図通りの権限境界を実現します。
CKAD 試験範囲では「機構の YAML 記述」が中心ですが、本番に出てからは「機構の組合せ設計と段階適用」がメインの仕事になる、というのが本セクションの実務的な肝です。
ep15 完了後の模擬アプリ状態と ep16 への橋渡し
本回で第5部「セキュリティ基礎」の最初の到達点として、RBAC・SecurityContext・Admission Controller 概念・CRD 概念の 4 機構を整理しました。
ep14 の Namespace 分離・ResourceQuota・LimitRange と組合せて、kubernetes クラスタの「ワークロード境界」と「権限境界」が揃った状態です。次の ep16 では NetworkPolicy で「通信境界」を加え、第5部のセキュリティ機構を完成させます。
ep15 完了後のクラスタ状態
| リソース | 状態 |
|---|---|
| kind クラスタ | kind-control-plane Ready(v1.35.0) |
| Namespace 一覧 | default / kube-node-lease / kube-public / kube-system / local-path-storage(合計 5 個・ep14 完了状態と同一) |
Role pod-reader | 新規追加(default ns・本回演習①で作成) |
RoleBinding fanclub-backend-pod-reader | 新規追加(default ns・本回演習①で作成・fanclub-backend-sa に紐付け) |
| fanclub-backend Deployment | replicas: 2 / 3 Probe 設定済 / RollingUpdate / SecurityContext なし(H2-8 で patch 後に元に戻し済) |
| fanclub-backend Service | ClusterIP(継続) |
| fanclub-db StatefulSet | fanclub-db-0 Pod Running(PostgreSQL 18・継続) |
| fanclub-db / fanclub-db-headless Service | 継続 |
| postgres-data-fanclub-db-0 PVC | Bound(継続) |
ConfigMap fanclub-config | 4 キー継続 |
Secret fanclub-secret | 2 キー継続 |
ServiceAccount fanclub-backend-sa | 継続(automountServiceAccountToken: false に復帰済) |
DaemonSet node-logger | 1 Pod Running 継続 |
CronJob fanclub-member-count | Suspend: True 継続 |
| nginx-secure Pod | 削除済(本回演習②で作成 → 末尾で kubectl delete) |
| CRD | 0 件(ep18 で導入予定) |
ep16 への橋渡し
ep16「NetworkPolicy 基礎」では本回で確立した「権限境界」に「通信境界」を加えます。本回の RBAC は「SA / User が API Server に対して何ができるか」を制御する機構でしたが、NetworkPolicy は「Pod が他の Pod / Namespace / 外部 IP に対してどんな通信を行えるか」を制御する機構です。両者は責務が異なり、組合せて使うことで「権限と通信の二重防御」が実現します。
ep16 では fanclub-backend Pod から fanclub-db Pod への DB 通信のみを許可し、他の Pod からの DB アクセスを拒否する NetworkPolicy を実装します。
同時に「default ns 外部の Pod が fanclub-backend に接続するパターン」「fanclub-backend が外部 HTTPS(cert-manager の ACME 等)に接続するパターン」も扱い、ep18 の Gateway API + HTTPS 公開に必要な通信設計の土台を整えます。
本回演習①で確立した pod-reader Role / RoleBinding は、ep16 で fanclub-backend が自分の Namespace 内の Pod 一覧を取得する想定 API Call の権限基盤として継続利用します。
理解度チェック・第15回まとめ・次回予告・シリーズ一覧
理解度チェック(○×形式・9 問)
問 1:Authentication と Authorization は同じ意味で、どちらも「誰がリクエストを送ったか」を識別する役割を持つ。
問 2:Role は Namespace スコープ、ClusterRole はクラスタスコープのリソースである。
問 3:Pod の spec.serviceAccountName で指定できる ServiceAccount は同じ Namespace のものに限られる。
問 4:kubectl auth can-i で --as=system:serviceaccount:default:my-sa フラグを使うと、その SA としての権限を確認できる。
問 5:runAsNonRoot: true を Pod に設定すると、コンテナイメージが root 前提でビルドされていても自動的に非 root で動く。
問 6:readOnlyRootFilesystem: true の Pod は /tmp ディレクトリにも書き込めない。
問 7:capabilities.drop: [ALL] は Linux capabilities をすべて drop する設定で、必要な capability があれば capabilities.add で個別に追加できる。
問 8:MutatingAdmissionWebhook はリクエスト内容を変更できるが、ValidatingAdmissionWebhook はリクエスト内容を変更できない。
問 9:CustomResourceDefinition は kubectl get crds で一覧表示でき、CRD で定義されたリソースも組込リソースと同じく kubectl の get / describe / apply で操作できる。
解答:
| 問 | 解答 | 解説 |
|---|---|---|
| 問 1 | × | Authentication(認証)は「誰か」を識別する層 1、Authorization(認可)は「その人がこの操作を実行できるか」を判定する層 2 で、別の役割。3 層の順序は Authn → Authz → Admission |
| 問 2 | ○ | Role / RoleBinding は Namespace スコープ、ClusterRole / ClusterRoleBinding はクラスタスコープ。両者の組合せは 4 通りあるが、Role + ClusterRoleBinding の組合せは無効 |
| 問 3 | ○ | Pod の serviceAccountName は同じ Namespace の SA のみ指定可能。クロス Namespace の SA 参照はできない。SA はクラスタワイドではなく Namespace スコープのリソース |
| 問 4 | ○ | --as フラグは impersonate の機能で、admin 権限を持つ User が他の User / SA としての権限を確認できる。CKAD 試験本番では admin として受験するため自由に使える |
| 問 5 | × | runAsNonRoot: true はイメージが非 root で動く前提のチェックを行うだけで、自動的に非 root に切り替える機能はない。イメージが root の場合は CreateContainerConfigError で起動失敗する。本回 H2-8 で実機検証した事象 |
| 問 6 | × | readOnlyRootFilesystem: true はルート FS を read-only にするが、/tmp 等を emptyDir で mount すればその mountPath は書き込み可能になる。本回演習② Step 6 で実機検証した事象 |
| 問 7 | ○ | drop: [ALL] で全 capability を drop してから add: ["NET_BIND_SERVICE"] 等で必要なものだけ個別追加する allowlist パターンが本番のベストプラクティス |
| 問 8 | ○ | Mutating は変更可・Validating は検証のみで変更不可。実行順は Mutating → Validating で、Validating は Mutating 後の最終形態を見る |
| 問 9 | ○ | CRD は kubectl の組込コマンドでそのまま操作可能。Gateway / Certificate / Rollout などの拡張リソースも、組込の Pod / Service と同じ kubectl 操作体系で扱える |
第15回まとめ
第15回では以下を実施しました。
- Kubernetes セキュリティの 3 層(Authentication / Authorization / Admission Control)を整理し、各層の役割・実行順序・失敗時の HTTP ステータス・代表的機構を表で対比した。ep14 で扱った LimitRanger / ResourceQuota は層 3(Admission)の組込 Plugin であることを確認し、本回扱う RBAC は層 2、Admission Controller の概念整理は層 3 の役割を持つ位置付けを明確にした
- RBAC の 4 リソース(Role / RoleBinding / ClusterRole / ClusterRoleBinding)の役割分担と 3 通りの有効な組合せパターン・1 通りの無効な組合せ(Role + ClusterRoleBinding)を整理した。Role の YAML 構造(apiGroups / resources / verbs)と RoleBinding の YAML 構造(subjects / roleRef)を全量で示し、最小権限の原則(PoLP)の実装ガイドラインを 5 項目で確立した。
kubectl create role/kubectl create rolebindingのワンライナー速攻パターンを試験対策として整理した - 演習①で fanclub-backend-sa に
pod-readerRole を紐付け、kubectl auth can-i+--as=system:serviceaccount:default:fanclub-backend-saで許可(list/get/watch pods)と拒否(delete/create pods / list secrets / list pods -n kube-system)の両方を実機確認した。automountServiceAccountToken を一時的に true に切り替えて Pod 内から curl で API Server を叩き、PodList 取得は 200 OK・Pod 削除は 403 Forbidden で拒否されることを実機検証し、確認後は automount を false に戻して ep10 の原則を再確立した - 組込 ClusterRole(cluster-admin / admin / edit / view)の役割と用途を整理し、独自 Role と組込 ClusterRole の使い分けを本番設計のセオリーとして確立した。
kubectl describe clusterrole viewで view の中身を確認し、多数リソースに対する get/list/watch の一括付与が組込 ClusterRole の特徴であることを把握した - SecurityContext の 5 つの脅威(コンテナエスケープ / FS 改ざん / 権限昇格 / capabilities 悪用 / syscall 悪用)と対応する 5 フィールド(runAsNonRoot / readOnlyRootFilesystem / allowPrivilegeEscalation / capabilities.drop / seccompProfile)を整理した。Pod レベルと Container レベルの設定可否と優先順位を 13 フィールド分の表で整理し、Pod レベル(FS 関連 + Pod 全体に効くもの)と Container レベル(個別動作に効くもの)の使い分けを確立した。Linux capabilities 14 個の意味と Web Server 系アプリでの不要性を整理し、
drop: [ALL]+ 必要時のみaddする allowlist パターンを本番ベストプラクティスとして示した - 演習②で nginx-unprivileged イメージをベースに Pod レベル + Container レベル両方の SecurityContext と 3 つの emptyDir(/tmp / /var/cache/nginx / /var/run)を組合せた
nginx-securePod を作成した。kubectl exec -- idで uid 1000 確認・touch /test.txtで readOnly のエラー確認・touch /tmp/test.txtで emptyDir 書き込み確認・/proc/self/statusの CapInh/CapPrm/CapEff/CapBnd がすべて0000000000000000で capabilities all drop の確認を実機検証した。fanclub-backend Pod の CapEff(00000000a80425fb・14 capabilities 保持)との対比で、SecurityContext の防御効果を定量化した - H2「fanclub-backend を non-root 化する場合の課題」で、fanclub-backend Deployment に
runAsNonRoot: trueを patch すると、Init Containerwait-for-db(busybox)が先にInit:CreateContainerConfigError+Error: container has runAsNonRoot and image will run as rootエラーで起動失敗することを実機検証した。Pod レベルの securityContext が Init Container にも適用されるため全コンテナの非 root 対応が必要で、Dockerfile レベルの改修(USER 1000 追加・所有者変更・イメージタグ 0.3.0 への更新)が ep18 で必要になる事実を整理した - Admission Controller の 2 系統(組込 Plugin / 外部 Webhook)と、Mutating / Validating の役割分担・実行順(Mutating → Validating)を整理した。組込 Plugin 5 個(NamespaceLifecycle / LimitRanger / ResourceQuota / ServiceAccount / DefaultStorageClass)が本シリーズの ep9 / ep10 / ep14 で間接的に使用済であることを確認した。外部 Webhook の動作フロー 10 ステップを整理し、ep18 で導入する cert-manager / Gateway API / Traefik の Webhook が本回時点では未導入であることを基準点として確認した
- CRD の概念(K8s API 拡張・YAML での宣言的管理・RBAC との統合・Operator との関係)を整理し、本シリーズで登場する 7 種類の CRD(Gateway API / cert-manager / Traefik / Argo Rollouts / CloudNativePG / OPA Gatekeeper / Falco)を一覧化した。演習③で
kubectl get crdsがNo resources foundを返す現状を確認し、ep18 で 10 〜 20 個の CRD が登録される未来形を理解した - CKAD 試験頻出パターン 4 選(SA に Pod 読み取り権限 / 全 Namespace 横断 read-only / 非 root 実行 / 権限昇格防止 + capabilities drop)を解答パターン + 速攻コマンドで整理した。SecurityContext のミニマル YAML テンプレートを暗記用に提示し、RBAC エラーの 5 種類のキーワードと対処を試験対策として確立した
- 現場ヒヤリハットを 2 件扱った。runAsNonRoot 一括 patch で本番イメージが root 前提だったため CreateContainerConfigError で全停止した事例、automountServiceAccountToken: true 放置で侵害時の権限拡大 → cluster-admin への権限昇格が成立した事例を、5 点セット(背景・事故・根本原因・解決策・本番ガードレール)で整理した。SecurityContext はイメージ前提との組合せ・RBAC は SA 設計との組合せが本番運用の肝であることを再確認した
次回予告
第16回 NetworkPolicy 基礎では、本回で確立した RBAC(権限境界)と SecurityContext(実行境界)の上に NetworkPolicy(通信境界)を載せ、第5部「セキュリティ基礎」を完成させます。
fanclub-backend Pod から fanclub-db Pod への DB 通信のみを許可し、他の Pod からの DB アクセスを拒否する NetworkPolicy を実装します。
Ingress / Egress 両方向のルール設計、podSelector / namespaceSelector / ipBlock の使い分け、default deny ポリシーによる「許可リスト方式」の確立を扱います。
kind クラスタの kindnet CNI は NetworkPolicy をデフォルトでサポートしていないため、Calico CNI への切替手順も併せて学習します。
CKAD ドメイン D4 の残り Competency「Demonstrate basic understanding of NetworkPolicies」を本回で完全網羅し、第5部「セキュリティ基礎」の到達点として fanclub-api 環境のセキュリティ機構を完成させます。
シリーズ一覧
第1部:コンテナと Docker
- 第1回 コンテナ技術概念 + Docker 環境準備
- 第2回 Docker 基本操作
- 第3回 Dockerfile + マルチステージビルド + JDK 25 / Payara Micro イメージビルド
- 第4回 コンテナレジストリ + イメージタグ戦略 + Trivy スキャン
第2部:Kubernetes 基礎
第3部:アプリリソース
- 第7回 Pod + Multi-container パターン
- 第8回 Service とネットワーキング
- 第9回 ストレージ(PVC + StatefulSet)+ PostgreSQL DB 追加
- 第10回 ConfigMap + Secret + ServiceAccount 基礎
- 第11回 Job + CronJob + DaemonSet
第4部:ワークロード戦略
- 第12回 Deployment + 3 Probe + Rolling Update + Probe デバッグ実践
- 第13回 Deployment 戦略補完(Blue/Green + Canary + Recreate)
- 第14回 ResourceQuota + LimitRange + Multi-tenant Namespace
第5部:セキュリティ基礎
- 第15回 RBAC + SecurityContext + Admission Controller 概念 + CRD 利用 ← 今ここ
- 第16回 NetworkPolicy 基礎
