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

RBACとSecurityContext|CKAD第15回

広告

新卒インフラエンジニア向け「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 つです。

  • RoleNamespace スコープの権限の集合(例: 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
RBAC の 4 要素の関係図。ServiceAccount(主体)を RoleBinding が Role に紐付け、Role が対象リソース(configmaps)への操作(get/list)を許可する流れ。ClusterRole/ClusterRoleBinding はクラスタスコープ版であることも示す。
図1:RBAC の 4 要素(ServiceAccount ← RoleBinding → Role → リソース/操作)

適用し、その主体になりきって(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: falsesetuid 等での権限昇格を禁止。

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/payarachmod -R g=u:作業ディレクトリを uid 10001(グループ 0)で読み書きできるようにする。
  • USER 10001:以降このユーザーで実行する(root で動かさない)。

ビルドして push します。非 root 化はイメージの実変更なので、タグを 0.3.10.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_OPTSjava.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
SecurityContext の防御レイヤー図。非 root 実行(runAsNonRoot/runAsUser 10001)、読み取り専用ルートFS(readOnlyRootFilesystem・書込先は /opt/payara/tmp の emptyDir のみ)、capability 全剥奪(drop ALL)、権限昇格禁止(allowPrivilegeEscalation false)が backend コンテナを多層で守る様子。
図2:SecurityContext の防御レイヤー(非 root / 読み取り専用 / capability 剥奪 / 昇格禁止)

Admission Controller(概念)

リソースを作る/更新するリクエストは、認証・認可(RBAC)を通ったあと、保存される前に Admission Controller を通過します。ここで検証(ポリシー違反を拒否)や変更(既定値の注入)ができます。第14回の LimitRange が「未指定 Pod に既定値を注入」していたのも Admission の一種です。Webhook で自作・拡張する仕組みが 2 つあります。

  • ValidatingAdmissionWebhook:リクエストを検証し、条件を満たさなければ拒否する(変更はしない)。
  • MutatingAdmissionWebhook:リクエストを書き換える(サイドカー自動注入・既定値付与など)。

「非 root でないコンテナは拒否」「特定レジストリのイメージだけ許可」といったポリシーを組織全体で強制したいときは、OPA GatekeeperKyverno といったポリシーエンジンを使います(宣言的にルールを書けます)。本巻では概念紹介にとどめ、本格的な実装は第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 APIGateway / HTTPRoute も、実体は CRD です(標準の kind ではなく、CRD で追加された拡張リソース)。「CRD = Kubernetes の語彙を増やす仕組み」と押さえておけば十分です。

やってみよう

  1. Role(configmaps を get / list)+ RoleBinding を fanclub-backend SA に適用し、kubectl auth can-i ... --as=system:serviceaccount:default:fanclub-backend で configmaps=yes / deployments delete=no を確認する。
  2. ClusterRole(nodes を get / list)+ ClusterRoleBinding を同じ SA に適用して can-i list nodes が no→yes に変わることを確認し、確認後は削除して no に戻す(PoLP)。
  3. backend-deploy.yaml の backend コンテナに securityContext(runAsNonRoot / runAsUser 10001 / readOnlyRootFilesystem / cap drop / 昇格禁止)と /opt/payara/tmp の emptyDir を足して適用し、id -u=10001・readiness UP を確認する。
  4. kubectl exec ... -- touch /opt/payara/testfileRead-only file system で失敗し、/opt/payara/tmp には書けることを確認する。
  5. kubectl get crds で Calico の CRD が登録済みであることを観察する。

理解度チェック

次の各文が正しいか(○)誤りか(×)を判断してください。解答は下にまとめています。

  1. Role は Namespace スコープ、ClusterRole はクラスタスコープの権限を定義する。
  2. RoleBinding は Role(や ClusterRole)を ServiceAccount などの主体に紐付ける。
  3. 最小権限の原則(PoLP)は、必要な権限だけを与える考え方である。
  4. kubectl auth can-i --as=<主体> で、その主体が操作できるかを確認できる。
  5. readOnlyRootFilesystem: true にすると、マウントした書き込み用 Volume にも一切書けなくなる。
  6. runAsNonRoot: true の指定では、root で動くイメージのコンテナは起動を拒否される。
  7. CRD は Kubernetes に新しい種類のリソースを追加する仕組みで、kubectl get crds で確認できる。
  8. 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

第2部:Kubernetes 基礎

第3部:アプリリソース

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

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

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

広告
kubernetes
スポンサーリンク