目次
16 章

RBAC / ServiceAccount 深掘り

第14章 RBAC の基本の上に、運用クラスタで出会う深さをもう一層重ねます。ClusterRole をラベルで束ねる Aggregated ClusterRole、別の主体の権限で呼び出す Impersonation、ServiceAccount トークンが永続 Secret から有効期限・audience・rotation を備えた projected token へ変わった流れ、そして EKS の IRSA・GKE の Workload Identity で K8s ServiceAccount をクラウド IAM に対応づけるモデルまでを整理します。

第14章 RBAC / NetworkPolicy / ResourceQuota で RBAC の4つのオブジェクト (RoleClusterRoleRoleBindingClusterRoleBinding) と権限を受ける3つの主体 (User、Group、ServiceAccount) を扱いました。「最小権限」という原則と、標準 ClusterRole を RoleBinding でネームスペースに限定して使うパターンまでがその章の締めくくりでした。本章ではその上にもう一層入る4つのテーマを整理します — Aggregated ClusterRole (権限の束をラベルで拡張)、Impersonation (別の主体の権限で一時的に呼び出す)、ServiceAccount トークンの lifecycle の変化 (K8s 1.22 の projected token のデフォルト化と 1.24 の legacy secret 自動生成の停止)、外部 IAM との接続 (EKS の IRSA、GKE の Workload Identity)。

本章の終わりには K8s の ServiceAccount がクラスタ内の RBAC 権限とクラスタ外のクラウド IAM 権限を同時に持つ運用の標準形 が手に入ります。第29章 シークレット運用 の「パスワード0」パターンの核心的な土台が本章の IRSA / Workload Identity です。

Aggregated ClusterRole — ラベルで束ねられる権限の束 #

運用クラスタで標準 ClusterRole を直接いじらずに権限を拡張したいときがあります。例えば K8s があらかじめ作っておく view ClusterRole はすべての標準リソースの read 権限を束ねたオブジェクトです。自分のチームが CRD (CustomResourceDefinition) で定義した新しいリソース種類を追加したとき、「view 権限を持つ人はこの新しいリソースも自動的に読めるべき」だとするとき、view ClusterRole を直接修正せずにこの動作を表現できるべきです。

この空きを埋めるオブジェクトが Aggregated ClusterRole です。モデルは単純です — 1つの ClusterRole が自分の権限リストを直接書く代わりに、aggregationRule でラベルセレクタを書いておくと K8s がそのセレクタに合う他の ClusterRole たちの rules を集めて束ねてくれます。

aggregated-view.yaml — 標準 view ClusterRole の形 (単純化)
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: view
aggregationRule:
  clusterRoleSelectors:
    - matchLabels:
        rbac.authorization.k8s.io/aggregate-to-view: "true"
rules: []  # 空 — コントローラが自動で埋める

rules が空である理由が核心です。K8s の RBAC コントローラがクラスタのすべての ClusterRole を舐めて、ラベルが rbac.authorization.k8s.io/aggregate-to-view: "true" のものたちの rules を集めて上の ClusterRole の rules フィールドに埋めてくれます。新しい権限を追加したいときはそのラベルが付いた新しい ClusterRole を1つ作ればよいです。

my-crd-view.yaml — 新しい CRD の view 権限を追加
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: my-crd-view
  labels:
    rbac.authorization.k8s.io/aggregate-to-view: "true"
rules:
  - apiGroups: ["myteam.example.com"]
    resources: ["widgets"]
    verbs: ["get", "list", "watch"]

このマニフェスト1枚を適用するとその瞬間から標準 view ClusterRole に widgets リソースの read 権限が自動的に束ねられます。view を RoleBinding で受けていたすべてのユーザーがコード1行も変えずに新しいリソースの read 権限を持つことになります。

標準ラベルは3つです — aggregate-to-viewaggregate-to-editaggregate-to-admin。K8s があらかじめ作っておく view / edit / admin ClusterRole がそれぞれのラベルで権限を集めます。第18章 CRD と Operator パターン で扱う CRD を導入した運用チームがユーザー定義 RBAC を最もきれいに拡張する道がこのモデルです。権限の source of truth が散らばりますが、標準 ClusterRole の意味を自然に拡張できるという利点が大きいです。

Impersonation — 別の主体の権限で呼び出す #

運用中には「このユーザーはどんな動作ができるか」を事前に確認したいときがあります。人間のユーザーに新しい権限を付与する前にその権限でどんな動作が可能かを検証したり、外部から入ってきた権限の問題を再現してみたり、ServiceAccount の権限範囲を確認したりする場合です。

K8s の Impersonation 機能はこの要求を満たしてくれます。呼び出し元が自分の資格で API を呼びながら「この呼び出しはユーザー X として振る舞ってくれ」とヘッダーを一緒に送ると、API サーバーが権限検査をその X の権限で実行します。呼び出し元自身の権限ではなく X の権限が適用されるので、呼び出し元には先に impersonate 権限がなければなりません。

kubectl --as オプションで別のユーザーになりすます
kubectl --as=alice@example.com get pods -n payments
kubectl --as=system:serviceaccount:default:my-sa get secrets

--as オプションが呼び出し元側の impersonation の表現です。上の1行目は「alice@example.com ユーザーの権限で payments ネームスペースの Pod を照会」する呼び出しで、2行目は「default ネームスペースの my-sa ServiceAccount の権限で Secret を照会」する呼び出しです。第14章 §「kubectl auth can-i」で一度押さえたオプションの深さを本章で広げます。

この呼び出しが通るには呼び出し元の RBAC に次の権限がなければなりません。

impersonator-clusterrole.yaml — impersonate 権限を付与
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: user-impersonator
rules:
  - apiGroups: [""]
    resources: ["users", "groups", "serviceaccounts"]
    verbs: ["impersonate"]

impersonate verb が核心です。この権限を持つユーザーは自分の権限を越えて別のユーザーの権限で呼び出しを送れるので、事実上クラスタの権限モデル全体を迂回できる権限です。impersonate 権限自体が非常にセンシティブなので、運用環境では SRE またはセキュリティチームの少数の人員にのみ付与します。

権限点検の標準ツール — kubectl auth can-i #

impersonation の日常的な活用は普通 kubectl auth can-i コマンドと連動します。

権限点検 — alice が secret を作れるか
kubectl auth can-i create secrets --as=alice@example.com -n payments
出力
yes

この呼び出しは実際に Secret を作りはしません。API サーバーの権限決定ロジックだけを回して yes / no を返します。新しい RoleBinding を適用する前にその意図が合うように展開されるか確認するとき、または権限の問題が入ってきたときどこが詰まるかを押さえるときに最初に呼ぶツールです。デバッグツリーの完成された流れは 第27章 kubectl デバッグパターン で整理します。

ServiceAccount トークン — legacy secret から projected token へ #

第14章 で ServiceAccount のトークンが Pod の中の /var/run/secrets/kubernetes.io/serviceaccount/token に自動的にマウントされると押さえました。この流れは二度に分かれて変わりました。K8s 1.22 では projected token がデフォルトになり、K8s 1.24 では legacy secret の自動生成が停止されました。運用クラスタでよく突き当たる変化なので順番に押さえておきます。

旧モデル — 自動生成される Secret オブジェクトの永続トークン #

K8s 1.21 までのデフォルト動作は次の通りでした。ServiceAccount を作ると K8s が自動的に同じ名前の Secret オブジェクトを作り、その Secret の中に ServiceAccount の JWT トークンを入れておきました。このトークンは 有効期限のない永続トークン でした。Pod がその ServiceAccount を使うと kubelet がその Secret をマウントしてトークンをコンテナの中に入れてくれる方式です。第6章 ConfigMap と Secret の Secret type の表で kubernetes.io/service-account-token として短く押さえたあのモデルです。

このモデルの問題は2つでした。

  • 有効期限がない — トークンが一度外部に漏れるとその ServiceAccount を消すかトークンを回転するまで永久に有効です。
  • Pod とトークンが分離される — Secret の中のトークンは Pod の生涯周期と無関係です。トークンの audience (誰のためのトークンか) 情報もありません。

新モデル — Projected Token (Bound ServiceAccount Token) #

K8s 1.22 から projected token がデフォルト動作になりました。ServiceAccount を作っても legacy Secret オブジェクトが自動的にできることはありません。代わりに Pod が起動するとき kubelet が その Pod 専用の短期 JWT トークンを発行して Pod のファイルシステムに直接マウントします。 このトークンの特徴は3つです。

  • 有効期限がある — デフォルト1時間、オプションでより短く / より長く調整可能です。kubelet が有効期限の前に自動的に新しいトークンを発行して再マウントします (rotate)。
  • audience が明示される — このトークンがどの API サーバーの呼び出しにのみ有効かがトークンの中に書かれています。
  • Pod に縛られる — トークンが Pod の UID と縛られているので、Pod が消えるとそのトークンももはや有効ではありません。
Pod マニフェストの projected token (自動で入る)
spec:
  containers:
    - name: app
      volumeMounts:
        - name: kube-api-access
          mountPath: /var/run/secrets/kubernetes.io/serviceaccount
          readOnly: true
  volumes:
    - name: kube-api-access
      projected:
        sources:
          - serviceAccountToken:
              path: token
              expirationSeconds: 3607
              audience: ""
          - configMap:
              name: kube-root-ca.crt
              items:
                - key: ca.crt
                  path: ca.crt
          - downwardAPI:
              items:
                - path: namespace
                  fieldRef:
                    fieldPath: metadata.namespace

kubelet が Pod を作るときこの projected ボリュームを自動的に追加します。マニフェストに直接書かなくてもすべての Pod に入るデフォルト動作です。コンテナの中のパス (/var/run/secrets/...) は旧モデルと同じなので、コンテナの中のコードはトークンモデルの変化を気にする必要がありません。

旧方式が必要なとき — 明示的な Secret #

CI パイプラインの外部ツールが K8s API を呼び出すとき、または IDE の K8s プラグインが一度受け取って使い続けるトークンが必要なときのように 有効期限のないトークン が必要な場合が残っています。このような場合は今や明示的に Secret を作らなければなりません。

legacy-token.yaml
apiVersion: v1
kind: Secret
metadata:
  name: my-sa-token
  annotations:
    kubernetes.io/service-account.name: my-sa
type: kubernetes.io/service-account-token

type と annotation が核心です。この Secret を作ると K8s がその中に my-sa の永続トークンを埋めてくれます。有効期限のないトークンなので外部に露出したときの影響が大きいです — 運用ではできる限り projected token を使い、本当に必要な場合にのみ明示的な Secret を作りトークン回転ポリシーを一緒に固めておくのが安全です。回転ポリシーの本格的な運用パターンは 第29章 シークレット運用 で扱います。

外部 IAM との接続 — IRSA と Workload Identity #

ここまでの話は K8s API 自体に対する権限でした。しかし運用クラスタのワークロードは K8s API 以外にもクラウドの他のサービスを呼びます — S3 バケット、RDS データベース、KMS の鍵、Secrets Manager の秘密。これらの呼び出しは K8s の RBAC の外の領域、すなわち クラウドの IAM で権限が評価されます。

伝統的な方法はクラウド資格情報 (access key + secret key) を K8s Secret に入れて Pod にマウントすることでした。この方法の問題は明白です — 資格情報が Secret として一度入ってくると有効期限がなく、回転が難しく、一度漏れるとその影響が大きいです。

EKS の IRSA (IAM Roles for Service Accounts) と GKE の Workload Identity はこの問題を同じ方式で解きます — K8s の ServiceAccount をクラウドの IAM Role と接続し、projected token を IAM の一時資格情報と交換 します。資格情報を静的に保管せず、呼び出し時点で短期トークンを発行してもらって使うモデルです。

IRSA の流れ — EKS の ServiceAccount + IAM Role #

IRSA のセットアップは3段階です。

IRSA セットアップ — 単純化した流れ
1. EKSクラスタにOIDC providerを有効化
   → EKSのServiceAccount JWTトークンをAWS IAMが信頼するよう設定
2. AWS IAM Roleを作りtrust policyに上のOIDC provider + 特定のServiceAccountを書く
   → "ns/my-appのSA = app-saが発行したトークンだけがこのRoleを取得できる"
3. K8s ServiceAccountにRole ARN annotationを付着
   → eks.amazonaws.com/role-arn: arn:aws:iam::ACCOUNT:role/my-app-role

このセットアップが終わったクラスタで Pod が AWS API を呼ぶと次の流れが自動的に回ります。

Podの中のAWS SDKがS3呼び出し時
1. Podにマウントされたprojected tokenを読む
2. AWS STSのAssumeRoleWithWebIdentity APIにそのトークンを送る
3. STSがトークンをEKS OIDC providerで検証
4. trust policyに書かれたServiceAccountと一致するか確認
5. 一致すればIAM Roleの一時資格情報(15分~12時間)を返す
6. AWS SDKがその資格情報でS3呼び出し

このすべての段階が AWS SDK の中で自動的に起こるので、アプリケーションコードは資格情報を直接扱いません。コードはただ boto3.client('s3') を呼べば終わりで、SDK 内部の資格情報チェーンが上の流れを辿ります。

ServiceAccount + Pod マニフェスト (IRSA 適用)
apiVersion: v1
kind: ServiceAccount
metadata:
  name: app-sa
  namespace: my-app
  annotations:
    eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/my-app-s3-role
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: app
  namespace: my-app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: app
  template:
    metadata:
      labels:
        app: app
    spec:
      serviceAccountName: app-sa  # ← 上で作ったSAを使用
      containers:
        - name: app
          image: my-app:latest

ServiceAccount 1個と IAM Role 1個が 1:1 で対応し、その ServiceAccount を使うすべての Pod がその IAM Role の権限を持ちます。静的資格情報を K8s Secret に保管する必要が消えます。EKS 上の IRSA の実戦セットアップ (Terraform で OIDC provider・IAM Role を作るなど) は 第21章 EKS クラスタセットアップ で本格的に扱い、RDS IAM auth と連携した「DB パスワード0」パターンは 第23章 DB 連携 — RDS・External Secrets で整理します。

GKE Workload Identity — 同じモデル、別の名前 #

GKE の Workload Identity も本質的に同じモデルです。K8s ServiceAccount を GCP IAM の Service Account と対応づけ、projected token を GCP STS と交換して一時資格情報を受け取る流れです。

GKE Workload Identity — ServiceAccount annotation
apiVersion: v1
kind: ServiceAccount
metadata:
  name: app-sa
  namespace: my-app
  annotations:
    iam.gke.io/gcp-service-account: my-app-sa@PROJECT.iam.gserviceaccount.com

annotation のキーと値の形式だけが違います。AKS も Azure Workload Identity で同じモデルを導入し、OIDC + STS ベースのトークン交換という同一の構造の上にクラウド事業者別のアダプタが載っている形です。

導入時に固めておく運用原則 #

IRSA と Workload Identity はセキュリティ面ではほぼ標準に近いですが、セットアップ段階の運用原則をいくつか押さえておきます。

  • ServiceAccount 1個 = クラウド IAM Role 1個の 1:1 マッピング — 1つの SA にあまりに多くの権限を寄せず、ワークロード単位で ServiceAccount を分け、各 SA に最小権限の IAM Role を付けます。
  • trust policy に namespace と SA name の両方を明示 — namespace だけを明示するとその namespace の別の SA が同じ Role を取得でき、隔離が崩れます。
  • トークン audience の検証 — STS がトークンを検証するとき audience (sts.amazonaws.com など) を確認します。ServiceAccount の projected token の audience を STS が期待する値と合わせなければなりません。
  • 資格情報の lifetime — IAM Role の一時資格情報は STS 呼び出し時点で発行され普通1時間有効です。AWS SDK が自動的に更新するのでアプリケーション側の処理は必要ありません。

練習問題 #

  1. 自分のクラスタに標準 view ClusterRole の aggregationRule がどう書かれているかを確認します (kubectl get clusterrole view -o yaml)。rules が空で aggregationRule だけがある形を §「Aggregated ClusterRole」のモデルと突き合わせて一段落で整理し、新しい CRD の read 権限を view に束ねるマニフェスト1枚を直接書いてみます。
  2. kubectl auth can-i create pods --as=system:serviceaccount:default:default -n default で default ServiceAccount が Pod を作れるか確認します。次に 第14章pod-reader RoleBinding を思い出し、その SA の立場で kubectl auth can-i list pods --as=system:serviceaccount:dev:pod-reader -n dev が yes と応答する流れを §「Impersonation」のモデルで再現します。impersonate 権限のないユーザーが同じコマンドを試したときどんなエラーが出るかも記録します。
  3. EKS 環境で ServiceAccount 1個に IRSA annotation を付け、その SA を使う Pod の中の AWS SDK が資格情報をどう得るかを §「IRSA の流れ」の6段階で直接シミュレーションしてみます。trust policy に namespace だけ書いて SA name を書かないとどんな隔離問題が生じるか、第29章 シークレット運用 の「パスワード0」パターンが本章の IRSA の上にどう載るかを一段落で整理します。

一行まとめ: Aggregated ClusterRole で標準権限の束をラベルで拡張し、Impersonation で別の主体の権限の意図を kubectl auth can-i で検証する。ServiceAccount トークンは K8s 1.22 から有効期限・audience・rotation を備えた projected token がデフォルトで、1.24 から legacy Secret の自動生成が停止された。IRSA / Workload Identity は K8s ServiceAccount をクラウド IAM Role と 1:1 マッピングして静的資格情報をクラスタの外へ追い出したモデルで、OIDC + STS トークン交換という共通構造の上にクラウド事業者別のアダプタが載る。

次の章 #

本章まで RBAC と ServiceAccount の権限モデルの深さを整理しました。次の章のテーマは視点をもう一段移します — マニフェストが etcd に保存される直前に検査・変形する段階 です。RBAC が「このユーザーがこの動作をできるか」を問うなら、次の章の admission は「このマニフェストがクラスタのポリシーに合うか」を問います。

第17章 Admission Controller では ValidatingAdmissionWebhook・MutatingAdmissionWebhook の K8s 標準モデルと、その上にポリシーエンジンを載せた2つのツール — OPA Gatekeeper (Rego 言語ベース) と Kyverno (K8s ネイティブ YAML ポリシー) の比較、そして「limits のないコンテナを拒否」「特定ラベルを強制」のような運用ポリシーのマニフェストパターンまでを一連の流れで辿ります。第14章 §「よくある罠 — 広すぎる ClusterRole」で押さえた「cluster-admin の束を禁止」のようなポリシーを admission 段階で強制する道が本格的に開きます。

X