Certified Kubernetes Security Specialist (CKS) #5: ServiceAccount トークン管理、API アクセス制限、クラスターアップグレード

#4 RBAC 最小権限の深掘り で誰が何をできるかを RBAC で狭めたなら、この記事はその権限を実際に持ち歩く ServiceAccount トークン を管理する番です。Pod の中に自動で挿し込まれるトークン 1 枚が奪われると、RBAC でいくら権限を狭めても、その狭めた権限の分はそのまま攻撃者の手に渡ります。だから Cluster Hardening ドメインは 不要なトークンを最初からマウントしないこと から始まります。

この記事では ServiceAccount トークンのマウントを切る方法、bound トークンの期限と audience、anonymous 認証の遮断と kubelet API の保護、そしてセキュリティパッチのためのクラスターアップグレードまで、Cluster Hardening の残り半分を整理します。

ServiceAccount トークンとは何か #

すべての Pod は Kubernetes API に自分を証明する身元が必要です。その身元が ServiceAccount (SA) であり、SA の資格情報がすなわち トークン です。トークンは JWT 形式の文字列で、kube-apiserver がこれを検証して「このリクエストは default ネームスペースの build-bot SA が送ったもの」だと判断したあと、その SA に紐づく RBAC 権限でリクエストを許可または拒否します。

問題は Kubernetes が デフォルトで すべての Pod にそのネームスペースの default SA トークンを自動でマウントする点です。Pod の中から見ると、次のパスにトークンが入っています。

/var/run/secrets/kubernetes.io/serviceaccount/token
/var/run/secrets/kubernetes.io/serviceaccount/ca.crt
/var/run/secrets/kubernetes.io/serviceaccount/namespace

ほとんどのアプリケーション Pod は Kubernetes API を直接呼ぶことがありません。それでもトークンがマウントされていると、その Pod が侵害されたときに攻撃者がこのトークンをそのまま持って API サーバーへアクセスします。つまり 使いもしないトークンが攻撃表面を広げる ことになります。

automountServiceAccountToken でマウントを遮断 #

コンテナが侵害されると、ファイルシステムに平文で置かれたトークンを読んで API サーバーを呼び出せて、その SA に付与された権限の分だけクラスターを操作できます。RBAC で権限を狭めるのが 1 次防御なら、最初からトークンを挿さないこと がより根本的な遮断です。マウントを切る設定が automountServiceAccountToken: false で、2 か所に置けます。

Pod レベルで切る #

特定の Pod だけマウントを切るには Pod spec に直接置きます。

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

この Pod の中では /var/run/secrets/kubernetes.io/serviceaccount/ パスが空です。API を使わないアプリケーションなら、こうしておくのが安全です。

ServiceAccount レベルで切る #

同じ SA を使うすべての Pod に一括で切るには ServiceAccount オブジェクトに置きます。

apiVersion: v1
kind: ServiceAccount
metadata:
  name: restricted-sa
  namespace: default
automountServiceAccountToken: false

この SA を使う Pod は、別途指定しなくてもトークンがマウントされません。ただし Pod レベルの設定が SA レベルの設定を上書きします。 つまり SA で false にしても特定の Pod で true にすると、その Pod にはトークンがマウントされます。逆に SA で true (デフォルト) でも Pod で false にすると、その Pod にはマウントされません。

設定の場所結果
Podfalse常にマウントされない (SA 設定を無視)
Podtrue常にマウントされる (SA 設定を無視)
Pod 未指定 + SAfalseマウントされない
Pod 未指定 + SAデフォルトマウントされる

試験では「この Pod に ServiceAccount トークンがマウントされないようにせよ」または「この SA を使うワークロードにトークン自動マウントを切れ」という作業が定番です。どのレベルに置けというのか文章を正確に読み、優先順位を思い出して正しい場所に設定するのが要です。

専用 SA を作って明示的に接続 #

default SA をそのまま使う代わりに、ワークロードごとに専用 SA を作り、必要な最小権限だけを RBAC で付与するのが推奨パターンです。API を呼ばなければならない Pod ならトークンをオンにしつつ専用 SA で隔離し、呼ばない Pod なら切ります。

apiVersion: v1
kind: ServiceAccount
metadata:
  name: build-bot
  namespace: ci
automountServiceAccountToken: true
---
apiVersion: v1
kind: Pod
metadata:
  name: builder
  namespace: ci
spec:
  serviceAccountName: build-bot
  containers:
    - name: builder
      image: gcr.io/kaniko-project/executor:latest

serviceAccountName を指定しないと default SA が付くので、権限が必要な Pod ほど専用 SA を明示する習慣がよいです。

bound ServiceAccount トークン #

トークンの種類も知っておく必要があります。Kubernetes 1.24 以前は ServiceAccount を作ると対応する Secret が自動で生成 され、その中に期限のない永続トークンが入っていました。これが legacy トークンで、期限がないので一度漏れると無期限に有効に見え、セキュリティの観点でよくありませんでした。

projected トークン (bound token) #

1.24 からは Pod が起動するときに kubelet が projected volume でトークンを発行します。このトークンが bound ServiceAccount トークンで、次の特性を持ちます。

  • 期限 (expiration) があります。デフォルトで 1 時間単位で kubelet が自動でトークンを更新して再マウントします。
  • audience が紐づきます。トークンがどの受信者 (API サーバーなど) のためのものか明示され、見当違いの場所で再利用されません。
  • その Pod の寿命に bound されます。Pod が消えるとトークンも無効になります。

つまりトークンが奪われても、短い期限と audience の制約のおかげで被害範囲と時間が大きく減ります。直接 Pod spec に projected トークンを明示することもできます。

apiVersion: v1
kind: Pod
metadata:
  name: api-client
spec:
  serviceAccountName: build-bot
  containers:
    - name: app
      image: nginx:1.27
      volumeMounts:
        - name: token
          mountPath: /var/run/secrets/tokens
          readOnly: true
  volumes:
    - name: token
      projected:
        sources:
          - serviceAccountToken:
              path: token
              expirationSeconds: 3600
              audience: vault

expirationSeconds で期限をさらに短く縮め、audience でトークンを特定の受信者だけに有効に紐づけられます。

legacy Secret トークンとの違い #

区分legacy Secret トークンbound (projected) トークン
発行の場所SA に紐づく Secret オブジェクトPod の projected volume
期限なし (永続)あり (デフォルト 1 時間、自動更新)
audienceなし指定可能
寿命SA/Secret が生きている間Pod の寿命に紐づく
推奨可否非推奨 (特殊目的のみ)デフォルト推奨

依然として期限のないトークンが必要な場合 (外部システム連携など) には次のように Secret を明示的に作れますが、CKS の観点では 本当に必要なときだけ 使い、普段は bound トークンを使うのが正解です。

apiVersion: v1
kind: Secret
metadata:
  name: build-bot-token
  namespace: ci
  annotations:
    kubernetes.io/service-account.name: build-bot
type: kubernetes.io/service-account-token

こうして作った Secret には期限のないトークンが満たされるので、漏れると危険が大きいです。試験で「期限のないトークンを作れ」という作業が出たらこの形を思い出しつつ、セキュリティのベストプラクティスとしては推奨されない点も一緒に覚えておきます。

API アクセス制限 #

トークンを狭めたなら、次は API サーバー自体へ入ってくる入口を狭める番です。Cluster Hardening のもう 1 つの軸が 認証されていないアクセスと過度な露出を防ぐこと です。

anonymous 認証を切る #

kube-apiserver はデフォルトで匿名リクエストを system:anonymous ユーザーとして受け入れます。RBAC がよく組まれていれば匿名ユーザーができることはほとんどありませんが、攻撃表面を減らす意味で 匿名認証そのものを切ること が推奨されます。API サーバーのマニフェストに次のフラグを置きます。

# /etc/kubernetes/manifests/kube-apiserver.yaml
spec:
  containers:
    - command:
        - kube-apiserver
        - --anonymous-auth=false
        # ... 残りのフラグ

/etc/kubernetes/manifests/ の static Pod マニフェストを修正すると kubelet が API サーバー Pod を自動で再起動します。修正直後にしばらく API サーバーが落ちるので、インデントと引用符を正確に合わせることが重要です。1 文字でも間違えると API サーバーが再び起動しません。

ただし --anonymous-auth=false にすると一部のヘルスチェックパス (/healthz/livez/readyz) に影響が及ぶことがあるので、試験では作業が要求する範囲の中だけで切り、影響範囲を確認するのが安全です。

RBAC で入口を狭める #

#4 で扱った RBAC が API アクセス制限の本体です。認証を通過したリクエストでも、認可の段階で権限がなければ拒否されます。次を確認します。

  • system:anonymoussystem:unauthenticated グループに不要な RoleBinding/ClusterRoleBinding が紐づいていないか点検します。
  • 広範な cluster-admin が複数の SA に付いていないか減らします。
  • ワイルドカード (*) の verb と resource を具体的な権限に狭めます。

kubelet API の保護 #

API サーバーと同じくらい重要なのが各ノードの kubelet API です。kubelet はノードでコンテナを実際に動かすコンポーネントで、その API が開いていると Pod 一覧の照会やコンテナ内コマンドの実行まで可能になり、非常に危険です。CKS でよく点検する項目が次の 2 つです。

# /var/lib/kubelet/config.yaml
readOnlyPort: 0
authentication:
  anonymous:
    enabled: false
  webhook:
    enabled: true
authorization:
  mode: Webhook
  • readOnlyPort: 0 で認証なしに開いていた 10255 読み取り専用ポートを閉じます。このポートが開いていると、誰でもノードの Pod 情報とメトリクスを認証なしに読めます。
  • anonymous.enabled: false で kubelet に対する匿名リクエストを遮断します。
  • authorization.mode: Webhook で kubelet API リクエストを API サーバーの認可に委任し、認証されたリクエストでも権限を再確認させます。

設定を変えたあとは systemctl restart kubelet で kubelet を再起動して反映します。この 3 項目は CIS benchmark (#3 の kube-bench) でも点検する項目なので、まとめて覚えておくとよいです。

クラスターアップグレードでセキュリティパッチを適用 #

最後の軸は クラスターを最新バージョンに保つこと です。Kubernetes には定期的に CVE (既知の脆弱性) が報告され、パッチは新しいマイナー/パッチバージョンで配布されます。古いバージョンを使うとすでに公開された脆弱性にそのまま露出するので、アップグレード自体がセキュリティ作業 です。

なぜアップグレードがセキュリティなのか #

  • コンポーネント (kube-apiserver、kubelet、etcd など) の既知の脆弱性がパッチされます。
  • セキュリティ機能 (例: bound トークン、PSA) の改善が新バージョンで入ります。
  • サポート終了 (EOL) バージョンはもうセキュリティパッチを受けられないので、サポートウィンドウ内のバージョンを保つ必要があります。

kubeadm アップグレード手順の要約 #

実際のアップグレード手順は CKA の領域なので、ここでは要点だけ押さえます。詳しい手順は CKA #6 クラスターのアップグレード で扱います。

# 1) control plane ノードで kubeadm をアップグレード
apt-get update && apt-get install -y kubeadm=1.31.x-*
kubeadm upgrade plan
kubeadm upgrade apply v1.31.x

# 2) 該当ノードを drain して kubelet/kubectl をアップグレード
kubectl drain <node> --ignore-daemonsets
apt-get install -y kubelet=1.31.x-* kubectl=1.31.x-*
systemctl daemon-reload && systemctl restart kubelet
kubectl uncordon <node>

# 3) ワーカーノードは kubeadm upgrade node のあと同じように kubelet を更新

要点は 一度に 1 マイナーバージョンずつ 上げる点、そして control plane を先に上げてからワーカーを上げる順序です。CKS の観点では、アップグレードの 動機 がセキュリティパッチの適用と CVE 対応だという文脈を理解すれば十分です。

試験ポイント #

  • automountServiceAccountToken: false がこのドメインの 1 番手の定番です。Pod レベルと SA レベルの 2 か所に置けて、Pod レベルが SA レベルを上書きする という優先順位を覚えておきます。
  • API を使わない Pod はトークンを切り、使う Pod は専用 SA + 最小 RBAC で隔離します。default SA には権限を付けません。
  • bound (projected) トークンは 期限・audience・Pod 寿命 bound が特徴です。legacy Secret トークンは期限がないので非推奨で、本当に必要なときだけ kubernetes.io/service-account-token Secret で作ります。
  • API サーバーは --anonymous-auth=false、kubelet は readOnlyPort: 0anonymous.enabled: falseauthorization.mode: Webhook で入口を狭めます。
  • static Pod マニフェスト (/etc/kubernetes/manifests/) や kubelet config を修正したあとは再起動・反映を確認します。インデントエラーでコンポーネントが起動しない事故がよくあります。
  • クラスターアップグレードは CVE 対応のためのセキュリティ作業です。一度に 1 マイナーバージョン、control plane を先に、ワーカーをあとにする順序を覚えます。

まとめ #

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

  • ServiceAccount トークンは Pod の身元 であり、デフォルトですべての Pod に自動マウントされて攻撃表面を広げます。
  • 使わないトークンは automountServiceAccountToken: false で遮断します。Pod レベルが SA レベルより優先 します。
  • bound トークンは期限・audience・Pod bound で奪取被害を減らします。legacy Secret トークンは期限がないので特殊目的だけに使います。
  • API アクセスは anonymous 認証の遮断、RBAC の最小化、kubelet API の保護 (readOnlyPort: 0 ほか) で狭めます。
  • クラスターアップグレードは CVE パッチを適用するセキュリティ作業です。control plane を先に、1 マイナーバージョンずつ上げます。

CKAD の観点で ServiceAccount と Pod 設定の基本をもっと見たいなら CKAD #14 ServiceAccount とセキュリティコンテキスト を、アップグレード手順の全ステップは CKA #6 クラスターのアップグレード を一緒に見るとよいです。

次へ — AppArmor プロファイル #

Cluster Hardening まで終えたので、ここからノードの Linux カーネルレベルに降ります。コンテナがホストで何をできるのかをオペレーティングシステムの次元で閉じ込めるドメインが System Hardening です。

#6 AppArmor プロファイル (System Hardening) では、AppArmor がファイルアクセスと capability をどう制限するのか、プロファイルを作成してノードにロードする方法、そしてそのプロファイルを Pod に付けてコンテナの行動を閉じ込めるパターンを直接作ってみながら整理します。

X