Certified Kubernetes Application Developer (CKAD) #14 ServiceAccount と RBAC (アプリ視点)

#13 ConfigMap と Secret の深掘り でアプリに設定値や秘密値を注入する方法を身につけました。ところが、アプリが単に値を受け取るだけでなく、Pod の中から直接 Kubernetes API を呼び出す 必要がある場合があります。コントローラーパターンで他のリソースを読んだり、自分の名前空間の Pod 一覧を取得したり、ConfigMap を更新したりするアプリがそうです。

このとき「その呼び出しが誰の権限で起きるのか」を決めるのが ServiceAccount (SA)RBAC です。人が kubectl でクラスターを操作するときに使う身元がユーザーアカウントだとすれば、Pod の中のアプリが API を呼び出すときに使う身元が ServiceAccount です。CKAD は運用者試験の CKA ほど RBAC を深くは問いませんが、アプリに適切な SA を付けて最小権限を与える レベルは試験範囲です。今回の記事はそのアプリ視点に集中します。

Pod の中のアプリが API を呼び出す図 #

まず大きな絵をつかみます。アプリが Kubernetes API を呼び出すと、API サーバーは 2 つの段階を経ます。

  1. 認証 (Authentication): このリクエストを送った身元は誰か。Pod の中のアプリは ServiceAccount トークンで自分の身元を証明します。
  2. 認可 (Authorization): その身元はこの動作を行う権限があるか。RBAC (Role-Based Access Control) が「どの身元がどのリソースにどの動作を行えるか」をルールで判定します。

ServiceAccount は 1 段階目の身元を提供し、RBAC は 2 段階目の権限を定義します。両者は RoleBinding でつながります。つまり SA を作り、Role で権限を定義し、RoleBinding でその 2 つを束ねる という流れです。この流れをそのまま手に馴染ませれば試験問題の大半が解けます。Kubernetes の認証・認可のより広い背景は K8s 中級 #7 で一緒に整理しました。

ServiceAccount #

名前空間ごとにある default SA #

ServiceAccount は名前空間に属するリソースです。名前空間を作ると default という名前の ServiceAccount が自動で 1 つ生成されます。Pod spec に serviceAccountName を明示しなければ、Pod は自分の名前空間の default SA で実行されます。

# 現在の名前空間の SA 一覧
k get serviceaccount
k get sa            # 短縮形

# default SA の詳細
k get sa default -o yaml

default SA には基本的に何の権限も付与されていません。そのため default SA で動くアプリが API を呼び出すと、大半は Forbidden で拒否されます。権限が必要なアプリには 専用 SA を別に作って 最小権限だけを与えるのが定石です。

SA の作成 #

専用 SA は命令型で 1 行で作ります。

# app-sa という SA を作成
k create serviceaccount app-sa

# dry-run でマニフェストの骨格だけ取り出す
k create sa app-sa $do > sa.yaml

生成されたマニフェストは kind: ServiceAccount に名前と名前空間だけが入った非常にシンプルな形です。

Pod に SA を付ける #

Pod がどの SA で実行されるかは spec の serviceAccountName で指定します。

apiVersion: v1
kind: Pod
metadata:
  name: api-client
spec:
  serviceAccountName: app-sa
  containers:
    - name: app
      image: bitnami/kubectl
      command: ["sleep", "3600"]

実行中の Deployment の SA を変える必要があるときは、spec を直接編集するか set コマンドを使います。

# Deployment の SA を入れ替える
k set serviceaccount deployment web app-sa

serviceAccountName は Pod 生成時点にのみ適用されるため、既存 Pod の SA を変えるには Pod を作り直す必要があります。Deployment なら上のコマンドが新しい ReplicaSet へのロールアウトを起こし、自動で入れ替わります。

トークンマウント #

自動マウントされる SA トークン #

Pod が起動すると、Kubernetes はその SA のトークンをコンテナ内の決まったパスに自動でマウントします。

/var/run/secrets/kubernetes.io/serviceaccount/
├── token       # SA の JWT トークン
├── ca.crt      # API サーバー検証用の CA 証明書
└── namespace   # Pod が属する名前空間の名前

アプリやクライアントライブラリ (in-cluster config) はこのパスのトークンを読んで API サーバーに認証します。コンテナの中で直接確認してみると、トークンがマウントされているのが見えます。

k exec api-client -- cat /var/run/secrets/kubernetes.io/serviceaccount/token
k exec api-client -- ls /var/run/secrets/kubernetes.io/serviceaccount/

自動マウントを切る #

API を呼び出さないアプリにまでトークンをマウントするのは不要な露出です。最小権限の原則に従い、トークンが要らない Pod は自動マウントを切るのが安全です。automountServiceAccountToken: false で切ります。

apiVersion: v1
kind: Pod
metadata:
  name: no-token
spec:
  automountServiceAccountToken: false
  containers:
    - name: app
      image: nginx

この設定は ServiceAccount 側にも置けます。SA に掛けるとその SA を使うすべての Pod に既定で適用され、Pod spec の設定が SA 設定より優先されます。

apiVersion: v1
kind: ServiceAccount
metadata:
  name: app-sa
automountServiceAccountToken: false

projected token: audience と expiration #

最近の Kubernetes は自動マウントされるトークンを projected volume で注入します。この方式はトークンに 寿命 (expiration)対象 (audience) を指定できるため、無期限に有効なトークンより安全です。自動マウントされる既定のトークンはクラスターがこの形で入れてくれますが、特定の audience や有効期限が必要なら直接 projected volume で明示します。

apiVersion: v1
kind: Pod
metadata:
  name: projected-token
spec:
  serviceAccountName: app-sa
  containers:
    - name: app
      image: nginx
      volumeMounts:
        - name: token
          mountPath: /var/run/secrets/tokens
          readOnly: true
  volumes:
    - name: token
      projected:
        sources:
          - serviceAccountToken:
              path: token
              audience: my-api          # トークンを受け取る対象の識別子
              expirationSeconds: 3600   # 1 時間後に期限切れ

expirationSeconds でトークンの寿命を制限し、audience でそのトークンがどのサービスに向けたものかを固定します。kubelet は期限が近づくとトークンを自動で更新して再マウントします。

RBAC: アプリに権限を与える #

SA を付けるだけでは権限は生まれません。default SA がそうであるように、新しく作った SA も最初は何もできません。権限を与えるには Role で権限を定義 し、RoleBinding でその権限を SA に接続 する必要があります。

Role: 名前空間スコープの権限の束 #

Role は「どのリソース (resources) にどの動作 (verbs) を許可するか」を 1 つの名前空間の中で定義します。動作は getlistwatchcreateupdatepatchdelete のような API 動詞です。

# pod に対する get/list/watch 権限を持つ Role を作成
k create role pod-reader \
  --verb=get,list,watch \
  --resource=pods

# dry-run でマニフェストを確認
k create role pod-reader --verb=get,list,watch --resource=pods $do

生成された Role マニフェストは rules の下に apiGroupsresourcesverbs の 3 つが入ります。core API グループ (pod、service、configmap など) の apiGroups は空文字列 "" だという点がよく間違える箇所です。全体の形は後の全体例で確認します。

RoleBinding: Role を SA に接続 #

RoleBinding は Role が定義した権限を特定の主体 (subject) に付与します。主体はユーザーでも、グループでも、ServiceAccount でもあり得ます。アプリ視点では SA が主体です。

# pod-reader Role を dev 名前空間の app-sa SA に付与
k create rolebinding app-sa-pod-reader \
  --role=pod-reader \
  --serviceaccount=dev:app-sa

--serviceaccount の値は 名前空間:SA 名 の形式です。生成された RoleBinding は roleRef (どの権限を与えるか) と subjects (誰に与えるか) の 2 つの部分から成ります。これで app-sa で動く Pod は dev 名前空間で Pod を取得できます。完成した形は後の全体例で確認します。

権限の検証: auth can-i #

権限が正しく付いたかは kubectl auth can-i で確認します。--as フラグで特定の身元になりすまして検査するのが肝です。SA の身元文字列は system:serviceaccount:名前空間:SA 名 の形式です。

# app-sa が dev で pod を get できるか
k auth can-i get pods \
  --as=system:serviceaccount:dev:app-sa \
  -n dev
# -> yes

# app-sa が pod を delete できるか (Role にない)
k auth can-i delete pods \
  --as=system:serviceaccount:dev:app-sa \
  -n dev
# -> no

yes または no で即座に答えが出るため、RBAC 問題を解いた後に検算する最速の方法です。試験で権限を付与したら、必ずこのコマンドで一度確認する習慣をつけるのが安全です。

ClusterRole と ClusterRoleBinding #

Role と RoleBinding が 1 つの名前空間の中でのみ有効なのに対し、ClusterRoleClusterRoleBinding はクラスター全体スコープです。ノードのように名前空間に属さないリソースや、複数の名前空間にまたがる権限が必要なときに使います。アプリ視点では大半が名前空間スコープで十分なので Role・RoleBinding を優先して使い、ClusterRole の詳しい活用は運用者試験の CKA 領域でより深く扱います。

全体例: SA + Role + RoleBinding + Pod #

ここまでの断片を 1 つに合わせると、専用 SA で動くアプリに Pod 取得権限だけを最小で与える完成したマニフェストになります。

# ServiceAccount: アプリ専用の身元
apiVersion: v1
kind: ServiceAccount
metadata:
  name: app-sa
  namespace: dev
---
# Role: dev 名前空間で pod を読む権限
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: pod-reader
  namespace: dev
rules:
  - apiGroups: [""]
    resources: ["pods"]
    verbs: ["get", "list", "watch"]
---
# RoleBinding: pod-reader 権限を app-sa に付与
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: app-sa-pod-reader
  namespace: dev
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: pod-reader
subjects:
  - kind: ServiceAccount
    name: app-sa
    namespace: dev
---
# Pod: app-sa で実行されるアプリ
apiVersion: v1
kind: Pod
metadata:
  name: api-client
  namespace: dev
spec:
  serviceAccountName: app-sa
  containers:
    - name: app
      image: bitnami/kubectl
      command: ["sleep", "3600"]

適用した後、Pod の中で直接権限を確認してみます。SA トークンが自動マウントされているので、コンテナ内の kubectl はそのトークンで認証します。

k apply -f app-rbac.yaml

# 権限が付いたかの最も信頼度が高い検証: 実際の Pod の中から呼び出す
k exec -n dev api-client -- kubectl get pods
# -> dev 名前空間の Pod 一覧が出力されれば成功

k exec -n dev api-client -- kubectl get secrets
# -> Forbidden。Role に secret 権限がないので拒否される

最後の 2 つのコマンドが示すように、付与した権限の中でのみ動作し、それ以外は拒否されます。これが最小権限の原則が実際に働く姿です。

試験ポイント #

  • SA は名前空間リソース です。明示しなければ Pod は default SA で実行され、default SA には権限がありません。
  • Pod に SA を付けるフィールドは spec.serviceAccountName です。実行中の Deployment は k set serviceaccount deploy <名前> <sa> で入れ替えます。
  • SA トークンは /var/run/secrets/kubernetes.io/serviceaccount/ に自動マウントされます。要らなければ automountServiceAccountToken: false で切ります (Pod spec が SA 設定より優先)。
  • 権限付与は Role (権限定義) + RoleBinding (SA に接続) の 2 段階です。k create rolek create rolebinding --serviceaccount=ns:sa を 1 行で作るのを手に馴染ませると時間が節約できます。
  • 検算は k auth can-i <verb> <resource> --as=system:serviceaccount:<ns>:<sa> -n <ns> です。yes/no で即座に確認できます。
  • ClusterRole・ClusterRoleBinding はクラスタースコープです。アプリ視点では Role・RoleBinding を優先して使います。
  • Role の rules で core API グループ (pod、service、configmap など) の apiGroups は空文字列 "" です。この点をよく間違えます。

まとめ #

この記事で押さえたこと:

  • ServiceAccount は Pod の中のアプリが API を呼び出すときに使う身元です。専用 SA を作って最小権限だけを与えるのが定石です。
  • トークン自動マウントautomountServiceAccountToken で制御し、projected token で audience と有効期限を指定できます。
  • RBAC は Role で権限を定義し、RoleBinding で SA に接続する 2 段階です。
  • 検証k auth can-i ... --as=system:serviceaccount:... で即座に確認します。
  • 権限は常に 必要な分だけ 与えます。これが次の記事のコンテナ権限制限とともに、アプリセキュリティの 2 つの軸を成します。

次へ: SecurityContext と Capabilities #

この記事はアプリが API サーバーに対して 何ができるかを RBAC で制限しました。次の記事は視線を一段内側に移し、コンテナが 自分が動くノードとカーネルに対して 何ができるかを制限します。

#15 SecurityContext と Capabilities では、runAsUserrunAsNonRoot でコンテナを非ルートで実行する方法、fsGroup でボリューム所有権を合わせる方法、readOnlyRootFilesystem でルートファイルシステムを読み取り専用にロックする方法、そして Linux capabilities を add/drop で精密に調整する方法を直接作りながら整理します。

X