新卒インフラエンジニア向け「Kubernetes 実践教科書① CKAD アプリケーション開発編」の第15回です。第14回までで第 4 部「ワークロード戦略」を終え、ここから第 5 部「セキュリティ基礎」に入ります。今回はRBAC(誰が何をできるか)・SecurityContext(コンテナを非 root・読み取り専用で動かす)・Admission Controller の概念・CRD(拡張リソース)を扱い、fanclub-api の Backend をセキュアにします。CKAD ドメイン D4 の総仕上げです。
動作確認バージョン:kind v0.31.0(K8s v1.35.0)/ kubectl v1.35.6 / fanclub-backend:0.3.1 → 0.3.2(非 root 化のため再ビルド)/ busybox:1.37 / Calico v3.32.0(2026-07-02 実機検証時点)
今ここマップ(全 19 回中の現在地)
現在地は第 5 部「セキュリティ基礎」の第 15 回です。動くアプリを「守る」段階に入ります。
- 第 1 部 コンテナと Docker(第 1〜4 回)
- 第 2 部 Kubernetes 基礎(第 5〜6 回)
- 第 3 部 アプリリソース(第 7〜11 回)
- 第 4 部 ワークロード戦略(第 12〜14 回)
- 第 5 部 セキュリティ基礎(第 15〜16 回)← 今ここ
- 第 6 部 パッケージ管理と HTTPS 公開(第 17〜19 回)
この回のゴール
- Role / RoleBinding / ClusterRole を設計・実装し、最小権限の原則で権限を絞れる。
- SecurityContext で非 root 実行・読み取り専用ルートFS・capability 削除を設定できる。
- Admission Controller の役割と、CRD(拡張リソース)の概念・確認方法を説明できる。
RBAC(誰が何をできるか)
RBAC(Role-Based Access Control)は「どの主体(ユーザー / ServiceAccount)が、どのリソースに、どの操作(get / list / create / delete …)をできるか」をルールで定めます。登場人物は 4 つです。
- Role:Namespace スコープの権限の集合(例: default の configmaps を get / list)。
- ClusterRole:クラスタスコープの権限(Namespace をまたぐ・nodes などクラスタ資源も対象)。
- RoleBinding:Role(や ClusterRole)を主体に紐付ける(Namespace 内)。
- ClusterRoleBinding:ClusterRole をクラスタ全体で主体に紐付ける。
大切なのは最小権限の原則(PoLP: Principle of Least Privilege)——必要な権限だけを与え、それ以外は与えないことです。第10回で作った専用 ServiceAccount fanclub-backend に、「default の configmaps を読むだけ」の Role を紐付けてみます。次を fanclub-backend-role.yaml として developer のホームに作成します(一般ユーザー所有・root 不要)。
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: fanclub-backend-reader
namespace: default
rules:
- apiGroups: [""]
resources: ["configmaps"]
verbs: ["get", "list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: fanclub-backend-reader-binding
namespace: default
subjects:
- kind: ServiceAccount
name: fanclub-backend
namespace: default
roleRef:
kind: Role
name: fanclub-backend-reader
apiGroup: rbac.authorization.k8s.io

適用し、その主体になりきって(impersonation)権限を確認します。kubectl auth can-i は「その操作が許可されているか」を yes / no で答えます。実行コマンド:
$ kubectl apply -f fanclub-backend-role.yaml
$ kubectl auth can-i get configmaps \
--as=system:serviceaccount:default:fanclub-backend -n default
$ kubectl auth can-i delete deployments \
--as=system:serviceaccount:default:fanclub-backend -n default
実行結果(例):許可した configmaps の get は yes、与えていない deployments の delete は no です。狙いどおり最小権限になっています。
yes
no
なお fanclub-backend は第10回で automountServiceAccountToken: false にしており、Pod 内から Kubernetes API を呼びません。ここでの Role/RoleBinding は「この SA にもし権限を与えるならこう絞る」という権限モデルと PoLP の実演です(API を使うアプリなら、この権限でトークン経由の操作範囲が決まります)。
今回は Namespace スコープの Role を使いました。クラスタ全体や複数 Namespace に効かせたい権限は ClusterRole にし、ClusterRoleBinding(クラスタ全体)または RoleBinding(特定 Namespace に限定して付与)で紐付けます。作り方は Role/RoleBinding と同じで、違いは metadata.namespace を持たない=スコープだけです。Kubernetes には最初から便利な ClusterRole が組み込まれています。実行コマンド:
$ kubectl get clusterrole view edit admin cluster-admin
実行結果(例):読み取り専用の view、編集可の edit、Namespace 管理の admin、全権の cluster-admin が並びます。自作の Role と同じ仕組みで、スコープが広いだけです。
NAME CREATED AT
view 2026-06-24T22:28:47Z
edit 2026-06-24T22:28:47Z
admin 2026-06-24T22:28:47Z
cluster-admin 2026-06-24T22:28:47Z
自分でも作ってみましょう。Namespace スコープの Role では扱えないクラスタスコープのリソース(例: nodes)を「読むだけ」にする ClusterRole と、それを fanclub-backend SA に紐付ける ClusterRoleBinding です。次を node-reader-clusterrole.yaml として developer のホームに作成します(一般ユーザー所有・root 不要)。
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: node-reader
rules:
- apiGroups: [""]
resources: ["nodes"]
verbs: ["get", "list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: fanclub-backend-node-reader
subjects:
- kind: ServiceAccount
name: fanclub-backend
namespace: default
roleRef:
kind: ClusterRole
name: node-reader
apiGroup: rbac.authorization.k8s.io
Role/RoleBinding との違いは metadata.namespace を持たない点だけです(対象がクラスタ全体になる)。適用して、紐付けの前後で権限が変わることを確認します。実行コマンド:
$ kubectl auth can-i list nodes \
--as=system:serviceaccount:default:fanclub-backend
$ kubectl apply -f node-reader-clusterrole.yaml
$ kubectl auth can-i list nodes \
--as=system:serviceaccount:default:fanclub-backend
実行結果(例):紐付け前は no、適用後は yes に変わります(nodes はクラスタスコープのため resource 'nodes' is not namespace scoped という情報表示が併せて出ますが、動作に問題はありません)。
no
yes
確認できたらすぐに削除して最小権限へ戻します。今回の Backend にノード情報の閲覧権限は不要で、使わない権限は残さないのが PoLP の実践です。実行コマンド:
$ kubectl delete -f node-reader-clusterrole.yaml
$ kubectl auth can-i list nodes \
--as=system:serviceaccount:default:fanclub-backend
実行結果(例):削除後は再び no に戻り、余計な権限が残っていないことを確認できます。
no
SecurityContext(非 root・読み取り専用で動かす)
SecurityContext は、コンテナ(や Pod)をどのくらい制限して動かすかを指定します。侵入されても被害を最小化する「守りの設定」です。主なものは次のとおりです。
runAsNonRoot: true/runAsUser:root で動かさない。root で動くイメージなら起動を拒否する。readOnlyRootFilesystem: true:ルートファイルシステムを読み取り専用にする。書き込みが必要な場所だけ Volume(emptyDir 等)で開ける。capabilities.drop: ["ALL"]:Linux ケーパビリティをすべて剥奪(必要なものだけ add)。allowPrivilegeEscalation: false:setuid等での権限昇格を禁止。
runAsNonRoot: true を効かせるには、まずイメージ自体を非 root にする必要があります。第3回のイメージは「root で動く最小構成」で、非 root 化はセキュリティを扱う今回に先送りしていました(第3回の予告どおりです)。ここで Dockerfile の実行ステージに非 root ユーザーを追加します(~/fanclub-api/Dockerfile・developer 所有)。
# ===== 実行ステージ(非 root 化を追加)=====
FROM eclipse-temurin:25-jre AS runtime
WORKDIR /opt/payara
ADD https://repo.maven.apache.org/maven2/fish/payara/extras/payara-micro/7.2026.4/payara-micro-7.2026.4.jar payara-micro.jar
COPY --from=build /app/target/fanclub-api.war app.war
COPY entrypoint.sh entrypoint.sh
RUN useradd --uid 10001 --no-create-home --shell /usr/sbin/nologin appuser \
&& mkdir -p /opt/payara/tmp \
&& chmod +x /opt/payara/entrypoint.sh \
&& chown -R 10001:0 /opt/payara \
&& chmod -R g=u /opt/payara
USER 10001
EXPOSE 8080
ENV JAVA_OPTS="-XX:MaxRAMPercentage=75.0 -Djava.io.tmpdir=/opt/payara/tmp"
ENTRYPOINT ["/opt/payara/entrypoint.sh"]
useradd --uid 10001:非 root の実行ユーザーを作る。chown -R 10001:0 /opt/payara+chmod -R g=u:作業ディレクトリを uid 10001(グループ 0)で読み書きできるようにする。USER 10001:以降このユーザーで実行する(root で動かさない)。
ビルドして push します。非 root 化はイメージの実変更なので、タグを 0.3.1 → 0.3.2 に上げます(第3回で予告した v1.0.0 は、第13回以降のコンテンツ駆動タグ方針に合わせ 0.3.2 とします)。実行コマンド:
$ cd ~/fanclub-api
$ docker build -t k8s-registry:5000/fanclub-backend:0.3.2 .
$ docker push k8s-registry:5000/fanclub-backend:0.3.2
次に、この非 root イメージへ SecurityContext を重ねます。1 点だけ readOnlyRootFilesystem: true の注意です。Payara は起動時に一時ファイルや展開先を書き込みますが、本アプリは書き込み先をすべて /opt/payara/tmp に集約してあります(entrypoint.sh の --rootDir と、先ほどの JAVA_OPTS の java.io.tmpdir)。そこでルートFSを読み取り専用にしても、/opt/payara/tmp に emptyDir を当てるだけで動きます。
第12回から使ってきた backend-deploy.yaml(developer のホーム・一般ユーザー所有)に SecurityContext と /opt/payara/tmp の emptyDir を足した完成形が次です(init / sidecar / 3 Probe / envFrom / SA / ローリング更新戦略はそのまま、イメージだけ 0.3.2 に更新)。同じファイルを上書きして kubectl apply します(クラスタへ送るだけなので root 権限は不要)。
apiVersion: apps/v1
kind: Deployment
metadata:
name: fanclub-backend
labels:
app: fanclub-backend
spec:
replicas: 1
selector:
matchLabels:
app: fanclub-backend
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
template:
metadata:
labels:
app: fanclub-backend
spec:
serviceAccountName: fanclub-backend
automountServiceAccountToken: false
initContainers:
- name: wait-for-db
image: busybox:1.37
command:
- sh
- -c
- 'until nslookup fanclub-db.default.svc.cluster.local >/dev/null 2>&1; do echo "waiting for fanclub-db..."; sleep 2; done; echo "fanclub-db resolved"'
- name: log-shipper
image: busybox:1.37
restartPolicy: Always
command:
- sh
- -c
- 'echo "sidecar started"; tail -F /var/log/app/app.log 2>/dev/null || sleep infinity'
volumeMounts:
- name: applog
mountPath: /var/log/app
containers:
- name: backend
image: k8s-registry:5000/fanclub-backend:0.3.2
ports:
- containerPort: 8080
envFrom:
- configMapRef:
name: fanclub-config
- secretRef:
name: fanclub-db-secret
securityContext:
runAsNonRoot: true
runAsUser: 10001
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
startupProbe:
httpGet:
path: /health/started
port: 8080
failureThreshold: 30
periodSeconds: 10
livenessProbe:
httpGet:
path: /health/live
port: 8080
periodSeconds: 10
failureThreshold: 3
readinessProbe:
httpGet:
path: /health/ready
port: 8080
periodSeconds: 10
failureThreshold: 3
resources:
requests:
memory: "512Mi"
cpu: "250m"
limits:
memory: "768Mi"
cpu: "1000m"
volumeMounts:
- name: applog
mountPath: /var/log/app
- name: payara-tmp
mountPath: /opt/payara/tmp
volumes:
- name: applog
emptyDir: {}
- name: payara-tmp
emptyDir: {}
SecurityContext は backend コンテナにだけ付けています。Pod レベルで runAsNonRoot: true にすると、root で動く busybox の init / sidecar が「root では起動できない」と弾かれてしまうためです(本番でそれらも固めるなら各コンテナに runAsUser を指定します)。適用して確認します。実行コマンド:
$ kubectl apply -f backend-deploy.yaml
$ kubectl rollout status deployment/fanclub-backend
$ POD=$(kubectl get pod -l app=fanclub-backend -o name | head -1)
$ kubectl exec "$POD" -c backend -- id -u
$ kubectl run c --rm -i --restart=Never --image=curlimages/curl:latest -- \
curl -s http://fanclub-backend:8080/health/ready
実行結果(例):uid は 10001(非 root)、readiness は UP。読み取り専用ルートFSでも、書き込み先を /opt/payara/tmp に逃がしているので Payara は正常に起動します。
10001
{"status":"UP","checks":[{"name":"fanclub-api-ready","status":"UP","data":{}}]}
読み取り専用が効いているかを確かめます。ルートFS配下(/opt/payara)へ書こうとすると拒否され、Volume を当てた /opt/payara/tmp には書けます。実行コマンド:
$ kubectl exec "$POD" -c backend -- sh -c 'touch /opt/payara/testfile'
$ kubectl exec "$POD" -c backend -- sh -c 'touch /opt/payara/tmp/ok && echo written'
実行結果(例):ルートFSは拒否、Volume は成功します。
touch: cannot touch '/opt/payara/testfile': Read-only file system
command terminated with exit code 1
written

Admission Controller(概念)
リソースを作る/更新するリクエストは、認証・認可(RBAC)を通ったあと、保存される前に Admission Controller を通過します。ここで検証(ポリシー違反を拒否)や変更(既定値の注入)ができます。第14回の LimitRange が「未指定 Pod に既定値を注入」していたのも Admission の一種です。Webhook で自作・拡張する仕組みが 2 つあります。
- ValidatingAdmissionWebhook:リクエストを検証し、条件を満たさなければ拒否する(変更はしない)。
- MutatingAdmissionWebhook:リクエストを書き換える(サイドカー自動注入・既定値付与など)。
「非 root でないコンテナは拒否」「特定レジストリのイメージだけ許可」といったポリシーを組織全体で強制したいときは、OPA Gatekeeper や Kyverno といったポリシーエンジンを使います(宣言的にルールを書けます)。本巻では概念紹介にとどめ、本格的な実装は第3巻(CKS)で扱います。
CRD(拡張リソース)
CRD(CustomResourceDefinition)は、Kubernetes に新しい種類のリソースを追加する仕組みです。CRD を登録すると、kubectl get / apply で標準リソースと同じように独自リソースを扱えるようになります。このクラスタには既に CRD が登録されています——第5回で入れた Calico(CNI)が、自身の設定を CRD として持っているのです。確認します。実行コマンド:
$ kubectl get crds
実行結果(例):crd.projectcalico.org グループの CRD が並びます(全部で 20 個超)。これらは Calico が導入時に登録したものです。
NAME CREATED AT
bgpconfigurations.crd.projectcalico.org 2026-06-24T22:28:50Z
bgpfilters.crd.projectcalico.org 2026-06-24T22:28:50Z
bgppeers.crd.projectcalico.org 2026-06-24T22:28:50Z
blockaffinities.crd.projectcalico.org 2026-06-24T22:28:50Z
...
第18回で導入する Gateway API の Gateway / HTTPRoute も、実体は CRD です(標準の kind ではなく、CRD で追加された拡張リソース)。「CRD = Kubernetes の語彙を増やす仕組み」と押さえておけば十分です。
やってみよう
- Role(configmaps を get / list)+ RoleBinding を
fanclub-backendSA に適用し、kubectl auth can-i ... --as=system:serviceaccount:default:fanclub-backendで configmaps=yes / deployments delete=no を確認する。 - ClusterRole(
nodesを get / list)+ ClusterRoleBinding を同じ SA に適用してcan-i list nodesが no→yes に変わることを確認し、確認後は削除して no に戻す(PoLP)。 backend-deploy.yamlの backend コンテナにsecurityContext(runAsNonRoot / runAsUser 10001 / readOnlyRootFilesystem / cap drop / 昇格禁止)と/opt/payara/tmpの emptyDir を足して適用し、id -u=10001・readiness UP を確認する。kubectl exec ... -- touch /opt/payara/testfileがRead-only file systemで失敗し、/opt/payara/tmpには書けることを確認する。kubectl get crdsで Calico の CRD が登録済みであることを観察する。
理解度チェック
次の各文が正しいか(○)誤りか(×)を判断してください。解答は下にまとめています。
- Role は Namespace スコープ、ClusterRole はクラスタスコープの権限を定義する。
- RoleBinding は Role(や ClusterRole)を ServiceAccount などの主体に紐付ける。
- 最小権限の原則(PoLP)は、必要な権限だけを与える考え方である。
kubectl auth can-i --as=<主体>で、その主体が操作できるかを確認できる。readOnlyRootFilesystem: trueにすると、マウントした書き込み用 Volume にも一切書けなくなる。runAsNonRoot: trueの指定では、root で動くイメージのコンテナは起動を拒否される。- CRD は Kubernetes に新しい種類のリソースを追加する仕組みで、
kubectl get crdsで確認できる。 - ValidatingAdmissionWebhook はリクエストを書き換えて既定値を注入する。
解答
- 1. ○:Role=Namespace 内、ClusterRole=クラスタ全体。
- 2. ○:主体(SA / ユーザー)と Role を結びつける。
- 3. ○:必要な権限だけを与える(PoLP)。
- 4. ○:impersonation で許可可否を yes / no 確認できる。
- 5. ×:ルートFSは読み取り専用だが、マウントした Volume には書ける(だから
/opt/payara/tmpに emptyDir を当てた)。 - 6. ○:
runAsNonRootは root イメージの起動を拒否する。 - 7. ○:CRD で拡張リソースを追加、
kubectl get crdsで一覧確認。 - 8. ×:書き換えは Mutating。Validating は検証(拒否)のみで変更しない。
まとめ
本記事では、RBAC(Role / RoleBinding で最小権限を設計し auth can-i で検証、クラスタスコープは ClusterRole / ClusterRoleBinding で付与し不要になれば削除)、SecurityContext(非 root・読み取り専用ルートFS・capability 剥奪・昇格禁止で Backend を多層防御。読み取り専用ルートFSの書き込み先は /opt/payara/tmp の emptyDir に逃がす)、Admission Controller(検証/変更・OPA/Kyverno は第3巻)、CRD(Calico のライブ例で確認)を学びました。イメージは非 root 化して 0.3.2 に更新し(第3回で予告した対応)、Pod spec と RBAC リソースでセキュアにできました。これで CKAD ドメイン D4 の主要項目が揃いました。
次回予告
次回(第16回)は第 5 部の締め、NetworkPolicy です。既定では素通しの Pod 間通信を default-deny(全拒否)から始め、必要な通信だけを段階的に許可して、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 基礎
