本記事には広告(アフィリエイトリンク)が含まれます。

K8s RBAC SecurityContext Admission CRD【CKAD第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 Deploymentreplicas: 2 / 3 Probe 設定済 / RollingUpdate maxSurge:1 maxUnavailable:0ep12 完了状態を継続
fanclub-db StatefulSetfanclub-db-0 Pod Running(PostgreSQL 18)ep9 から継続
node-logger DaemonSet1 Pod Runningep11 から継続
ConfigMap / Secretfanclub-config / fanclub-secret 継続ep10 から継続
ServiceAccountfanclub-backend-sa(automountServiceAccountToken: false)+ defaultep10 から継続
Role / RoleBinding0 件(default ns・本回で初導入)Lead 実機観察
CRD0 件(Gateway API CRD は ep18 で導入予定)Lead 実機観察
Allocated CPU1550m / 2000m(77 %)Lead 実機観察
fanclub-backend Pod 実行 uiduid=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 ステータス
1Authentication(認証・Authn)「リクエストの送信者は誰か」を識別する。kubeconfig の client certificate / Bearer Token / Service Account Token / 外部 IdP(OIDC)等から identity を確定X.509 Client Cert / Static Token / SA Token / OIDC / Webhook Authentication401 Unauthorized
2Authorization(認可・Authz)確認された送信者が要求された操作を実行する権限を持つか判定。「user X が resource Y に対して verb Z を実行できるか」を Yes/No で返すRBAC / Node Authorization / Webhook Authorization / ABAC(非推奨)403 Forbidden
3Admission Control(受付制御)リクエスト内容そのものを検証・変更する。「この Pod の resources は LimitRange の max を超えていないか」「runAsNonRoot 違反していないか」「label を自動付与すべきか」等を判断組込 Admission Plugin(LimitRanger / ResourceQuota / ServiceAccount / NamespaceLifecycle 等)+ 外部 Webhook(ValidatingAdmissionWebhook / MutatingAdmissionWebhook)403 Forbidden(reject 時)
kubectl リクエストが Authentication(認証・401)・Authorization(認可・403)・Admission Control(受付制御・403)の 3 層を上から順に通過し、最後に etcd へ保存される縦フロー図。層 1 Authentication は第1巻スコープ外として淡色・点線で示し、層 2 RBAC と層 3 Admission を本回の主役として強調している

3 層の実行順序は Authn → Authz → Admission で固定されています。Authentication で識別できない(401)リクエストは Authz に進まず即座に弾かれ、Authz で権限が無い(403)と判定されたリクエストも Admission に進みません。

Admission Control まで到達したリクエストは内容自体の検証・変更を受け、最終的に Admission を通過すると API Server が etcd にリソースを書き込みます。

第14回で扱った ResourceQuota / LimitRange は 3 層のうち Admission Control(層 3)に属する機構です。

具体的には LimitRangerResourceQuota という名前の組込 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 Certificatekubeconfig に埋め込まれた client cert で identity を確定。kind / kubeadm のデフォルト第5回(kind 利用)から間接的に使用中
Service Account TokenPod が /var/run/secrets/kubernetes.io/serviceaccount/token から読み取って API Server に送る Bearer Tokenep10 で扱い済・本回演習①で再登場
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 リソースの全体像

リソーススコープ役割本回での扱い
RoleNamespace「Namespace 内の特定リソースに対する verb の集合」を定義(権限の定義側)本演習①で pod-reader を作成
RoleBindingNamespaceRole を 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 セットで作るのが本回の演習①の流れになります。

RBAC の紐付け図。中央ハブの RoleBinding が subjects で ServiceAccount(fanclub-backend-sa)を、roleRef で Role(pod-reader)を参照する縦の階層構造。Role には pods への get / list / watch を許可する rules サブパネルが入れ子で示され、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"]

各フィールドの意味を整理します。

フィールド意味本演習での値
apiVersionRBAC API は v1 で安定(v1beta1 は K8s v1.22 で削除済)rbac.authorization.k8s.io/v1
kindNamespace スコープの権限定義Role
metadata.namespaceRole が所属する 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.namespaceRoleBinding が所属する Namespacedefault
subjects権限を付与する対象の配列(User / Group / ServiceAccount)1 件(fanclub-backend-sa)
subjects[].kind対象の種類。ServiceAccount / User / Group のいずれかServiceAccount
subjects[].name対象の名前fanclub-backend-sa
subjects[].namespaceServiceAccount の所属 Namespace(kind: ServiceAccount のみ必須・User/Group では指定不要)default
roleRef.kind紐付ける Role 種別。Role または ClusterRoleRole
roleRef.name紐付ける Role / ClusterRole の名前pod-reader
roleRef.apiGroup固定値(RBAC API グループ)rbac.authorization.k8s.io

重要な制約roleRefRoleBinding 作成後に変更できない 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.kindsubjects[].kind用途
1RoleServiceAccount本演習①の構成。Namespace 内 Pod から Namespace 内リソースにアクセス
2RoleUser / Group運用者個人や運用チームに Namespace 内権限を付与
3ClusterRoleServiceAccount / 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 しない
adminNamespace 内全リソースの管理権限(ResourceQuota / Namespace 自身の操作は除く)Namespace 内の「フル権限ユーザー」
editNamespace 内ワークロード(Pod / Service / Deployment / ConfigMap 等)の作成・更新・削除。Role / RoleBinding 操作は不可Namespace 内の「アプリ開発者」
viewNamespace 内全リソースの読み取りのみ(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 + RoleBindingRoleBinding の所属 Namespace 内のみ組込 view/edit を Namespace 内ユーザーに付与する一般的なパターン
Role + RoleBindingRole / RoleBinding の所属 Namespace 内のみ本演習①のパターン。Namespace 限定の独自 Role 用
Role + ClusterRoleBinding無効な組合せ(ClusterRoleBinding は Role を参照不可)—(試験で誤答候補として出る)

4 番目の「Role + ClusterRoleBinding」は CKAD 試験で誤答として頻出する組合せです。

ClusterRoleBinding の roleRef.kindClusterRole しか受け付けないため、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 podsno:Role の verbs に delete を含めていないため拒否
  • create podsno:Role の verbs に create を含めていないため拒否
  • list secretsno:Role の resources を pods のみに絞っているため、Secret は対象外で拒否
  • list pods -n kube-systemno: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 を dropcapabilities.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 レベル備考
runAsNonRootContainer レベルが優先。両方設定で Container が勝つ
runAsUserContainer レベルが優先
runAsGroupContainer レベルが優先
fsGroup×Pod レベルのみ。volume の所有グループを設定
fsGroupChangePolicy×Pod レベルのみ。large volume の chown 最適化
supplementalGroups×Pod レベルのみ。追加の gid 群
seccompProfileContainer レベルが優先
seLinuxOptionsContainer レベルが優先
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」を配置するベストプラクティスな構成です。

主要フィールドの効果と推奨値

フィールド効果推奨値非設定時の挙動
runAsNonRootuid=0 での実行を Admission 層で reject。コンテナイメージの USER が root の場合は Pod 起動失敗truefalse(root 実行可)
runAsUserコンテナ内のプロセスを指定 uid で実行(Dockerfile の USER を上書き)1000(または 65534 = nobody)イメージの USER を踏襲(多くは root)
runAsGroupコンテナ内のプロセスを指定 gid で実行1000イメージの GROUP を踏襲
fsGroupVolume の所有グループに gid を設定(Pod 起動時に kubelet が chown)1000volume の元の所有者のまま
readOnlyRootFilesystemコンテナの / を read-only でマウント。書き込み試行は EROFS エラーtruefalse(書き込み可)
allowPrivilegeEscalationsetuid / sudo / fcaps による権限昇格を OS レベルで禁止falsetrue(昇格可)
capabilities.dropLinux capabilities を drop(権限剥奪)["ALL"]Docker デフォルト 14 capability 保持
capabilities.adddrop した capabilities のうち必要なものだけ add(最小許可)必要時のみ追加(例: NET_BIND_SERVICE
seccompProfile.typeseccomp プロファイル適用。RuntimeDefault は CRI デフォルトのプロファイルRuntimeDefaultUnconfined(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_OVERRIDEDAC(ファイルパーミッション)バイパス権限のないファイル読み書き
CAP_FOWNERファイル所有者でなくてもメタデータ変更
CAP_FSETIDファイル変更時の setuid/setgid 保持
CAP_KILL他プロセスへの signal 送信
CAP_SETGIDプロセス gid 変更権限昇格の足がかり
CAP_SETUIDプロセス uid 変更権限昇格の足がかり
CAP_SETPCAPcapability 変更
CAP_NET_BIND_SERVICE1024 未満の port に bind—(Web Server で port 80/443 bind に必要)
CAP_NET_RAWraw socket 利用(ping / arp 等)ARP spoofing / port scan
CAP_SYS_CHROOTchroot() syscall
CAP_MKNODデバイスファイル作成
CAP_AUDIT_WRITEaudit 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/run3 つの emptyDir を mount(本演習②)
Payara Micro/tmp / Payara のキャッシュディレクトリ2 つの emptyDir(fanclub-backend を non-root 化した将来形)
PostgreSQL/var/lib/postgresql/data / /tmpPVC(データ)+ 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.securityContextrunAsNonRoot / runAsUser / runAsGroup / fsGroup / seccompProfile の 5 項目。Pod 全体に効くものと FS 関連を配置
  • Container レベル spec.containers[].securityContextreadOnlyRootFilesystem / 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 は PendingCrashLoopBackOff ではなく 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 PluginAPI Server バイナリ内(kube-apiserver)LimitRanger / ResourceQuota / ServiceAccount / NamespaceLifecycle / DefaultStorageClass / PodSecurityep14(LimitRanger / ResourceQuota)で間接的に使用済
外部 Admission Webhookクラスタ内 / 外部の Webhook ServiceValidatingAdmissionWebhook / 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 でのリソース作成を禁止本回
LimitRangerLimitRange の default / max を Pod 作成時に適用ep14
ResourceQuotaQuota 違反の Pod / リソース作成を rejectep14
ServiceAccountPod に SA Token を mount / default SA を補完ep10
DefaultStorageClassPVC に storageClass 未指定時のデフォルト補完ep9(local-path がデフォルト)
PodSecurityPod 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 一覧(先取り)

CRDAPI グループ役割導入回
Gateway / HTTPRoute / GatewayClassgateway.networking.k8s.ioIngress 後継の標準 API(Traefik で実装)ep18
Certificate / Issuer / ClusterIssuercert-manager.ioTLS 証明書の宣言的管理ep18
IngressRoute / TLSStoretraefik.ioTraefik 独自の拡張ルーティングep18
Rollout / AnalysisRunargoproj.ioBlue/Green / Canary を Operator で自動化(第2巻 GitOps 範囲)第2巻
Cluster(PostgreSQL)postgresql.cnpg.ioCloudNativePG による PostgreSQL Cluster 管理第3巻
ConstraintTemplate / Constrainttemplates.gatekeeper.shOPA Gatekeeper のポリシー定義第3巻 ep4
FalcoRulefalco.orgFalco のランタイム検知ルール第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/v1 API グループの存在を確認し、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 に podsget,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 namespaceClusterRoleBinding で Role(Namespace スコープ)を参照しようとしたroleRef.kind を ClusterRole に変更するか、RoleBinding に切り替える
error: container "<name>" must run as non-root user, but is configured to run as user 0SecurityContext の runAsNonRoot: true がイメージの USER (root) と矛盾イメージの Dockerfile に USER 1000 を追加して再ビルド or runAsUser でイメージの USER を上書き
Read-only file systemreadOnlyRootFilesystem: 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 inspectConfig.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 Deploymentreplicas: 2 / 3 Probe 設定済 / RollingUpdate / SecurityContext なし(H2-8 で patch 後に元に戻し済)
fanclub-backend ServiceClusterIP(継続)
fanclub-db StatefulSetfanclub-db-0 Pod Running(PostgreSQL 18・継続)
fanclub-db / fanclub-db-headless Service継続
postgres-data-fanclub-db-0 PVCBound(継続)
ConfigMap fanclub-config4 キー継続
Secret fanclub-secret2 キー継続
ServiceAccount fanclub-backend-sa継続(automountServiceAccountToken: false に復帰済)
DaemonSet node-logger1 Pod Running 継続
CronJob fanclub-member-countSuspend: True 継続
nginx-secure Pod削除済(本回演習②で作成 → 末尾で kubectl delete)
CRD0 件(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 のものに限られる。

問 4kubectl auth can-i--as=system:serviceaccount:default:my-sa フラグを使うと、その SA としての権限を確認できる。

問 5runAsNonRoot: true を Pod に設定すると、コンテナイメージが root 前提でビルドされていても自動的に非 root で動く。

問 6readOnlyRootFilesystem: true の Pod は /tmp ディレクトリにも書き込めない。

問 7capabilities.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
問 2Role / RoleBinding は Namespace スコープ、ClusterRole / ClusterRoleBinding はクラスタスコープ。両者の組合せは 4 通りあるが、Role + ClusterRoleBinding の組合せは無効
問 3Pod の 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 で実機検証した事象
問 7drop: [ALL] で全 capability を drop してから add: ["NET_BIND_SERVICE"] 等で必要なものだけ個別追加する allowlist パターンが本番のベストプラクティス
問 8Mutating は変更可・Validating は検証のみで変更不可。実行順は Mutating → Validating で、Validating は Mutating 後の最終形態を見る
問 9CRD は 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-reader Role を紐付け、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-secure Pod を作成した。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 Container wait-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 crdsNo 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

第2部:Kubernetes 基礎

第3部:アプリリソース

第4部:ワークロード戦略

第5部:セキュリティ基礎

第6部:パッケージ管理 + HTTPS 公開

広告
kubernetes
スポンサーリンク