TT Lab
はじめる
学ぶ 学習パス コース

KCSA — Kubernetesセキュリティアソシエイト

リクエスト一つがapiserverを通る道

TT Labで続きを見る

一言でいうと

apiserverに入ってきたリクエストは、認証 → 認可 → アドミッション → 検証 → 保存の順に流れます。 各段階が何を判断し、何は判断しないのかがわかれば、セキュリティに関する質問の大半は解けます。

1つのリクエストがapiserverを通過する6段階: 認証、認可、Mutatingアドミッション、スキーマ検証、Validatingアドミッション、etcd保存。401は1段階目の失敗、403は2段階目の失敗であり、アドミッションは先に直してから検査しないと辻褄が合わないため、Mutatingが先です

なぜ必要なのか

「このリクエストはなぜ拒否されたのか」に答えるには、どの段階で引っかかったのかを知る必要があります。 401は認証の失敗、403は認可の失敗、アドミッションWebhookによる拒否はまた別のメッセージです。 段階を知らないと、RBACを直すべきときに証明書をいじることになります。

どう動くのか

1段階目: 認証(Authentication)—「あなたは誰ですか」

apiserverは複数の認証器(authenticator)を順番に試し、1つでも成功すればその身元で 処理を進めます。すべて失敗すると、リクエスト元はsystem:anonymous(グループsystem:unauthenticated)になります。

方式 使われる場所
X.509クライアント証明書 kubelet、コントローラー、管理者のkubeconfig。CNがユーザー名、Oがグループ
ServiceAccountトークン(JWT) Pod内からのAPI呼び出し
OIDCトークン 人間のユーザーを社内のIdPと連携
Webhookトークン認証 外部の認証システムへの委任

重要な事実が1つあります。Kubernetesには、Userオブジェクトがありません。ユーザーとは認証器が作り出した 文字列にすぎず、そのため「ユーザーを削除」することはできません。証明書を失効させるか、IdP側で切り離す必要があります。

匿名認証(--anonymous-auth=true)は、認証の失敗を拒否にせず、匿名の身元を与えます。 匿名に何を許可するかは認可の段階が決めるため、匿名アクセスそのものより、匿名に付けられた権限が 本当の問題です。デフォルトのクラスターでは、匿名は/healthzや/versionのような公開情報しか見られません。

system:mastersグループは特別です。このグループのメンバーはすべてのRBACチェックを迂回し、 RoleBindingやClusterRoleBindingを削除しても権限を取り消せません。 証明書にO=system:mastersが埋め込まれると、その証明書が有効な間は、事実上取り消せない最高権限になります。

2段階目: 認可(Authorization)—「それをしてもよいですか」

認可モジュールも順番に評価され、1つでも許可すれば通過します。--authorization-modeフラグで指定します。

RBACには許可しかなく、拒否がありません。どこにも許可がなければ拒否です。 そのため「この人から権限を奪うRole」のようなものは作れず、バインディングをなくす必要があります。

3段階目: アドミッション(Admission)—「この内容はポリシーに合っていますか」

認可を通過したリクエストでも、内容がルールに合っているかを見ます。順序が重要です。 Mutatingが先、Validatingが後です。先に直してから検査しないと辻褄が合いません。

ここに住む代表的なプラグインがPodSecurity(PSA)です。そして外部のポリシーエンジン (OPA Gatekeeper、Kyverno)もWebhookでこの段階に割り込みます。

アドミッションWebhookには運用上の落とし穴があります。failurePolicyがFailだと、Webhookが落ちたときに すべてのPodの作成が拒否され、Ignoreだとポリシーが丸ごと迂回されます。著者のブログがまとめた判断は 明快です。「本番環境ではFailを維持しつつ、GatekeeperのPodのリソースとreplicasを十分に 確保します。」可用性の問題を、ポリシーを弱めて解決するのではなく、エンジニアリングで解決せよという意味です。

4段階目: etcdへの保存と保存時の暗号化

apiserverはオブジェクトをシリアライズ(デフォルトはprotobuf)したあと、transformerを経由してetcdに書き込みます。 transformerがidentityなら平文、aescbc/aesgcm/kmsなら暗号文です。

apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
  - resources: [secrets]
    providers:
      - aescbc:
          keys: [{name: key1, secret: <base64>}]
      - identity: {}

providers配列の順序がすべてです。1番目が書き込みに使われ、すべてのプロバイダーが読み取りに使われます。 そのためidentityを先頭に置くと平文保存に戻り、末尾に置くと「新しく書くものは暗号化、 以前の平文も読める」状態になります。そして設定を有効にしても、既存のSecretは書き直されるまで平文のまま 残ります。kubectl get secrets -A -o json | kubectl replace -f -のような全件の書き直しが必要です。

KMS v2プロバイダーはさらに一歩進んでいます。DEKはローカルで作ってキャッシュし、リモートのKMSはKEKだけを 管理します(エンベロープ暗号化)。キーのローテーションにapiserverの再起動は必要ありません。

5段階目: kubeletの認証/認可

kubeletは2つの方向の身元を持ちます。

コントローラーマネージャーとトークンの発行

コントローラーマネージャーはServiceAccountコントローラーとトークンコントローラーを動かし、--service-account-private-key-file で受け取ったキーでSAトークンに署名します。このキーを手に入れると、任意のSAになりすますトークンを自分で発行できます。 apiserverは対応する公開鍵で検証するだけだからです。

v1.24からは、SAを作っても永続的なトークンSecretが自動生成されず、TokenRequest APIに基づく 有効期限付きのトークンがデフォルトです(kubectl create token <sa> --duration=3600s)。セキュリティ上の大きな改善です。 ここから--service-account-lookup=falseが危険な理由も見えてきます。この値がfalseだと、 すでに削除されたSAのトークンも、署名さえ有効なら通り続けます。

現場での姿

著者のホームラボ拡張の記録に、この仕組みの急所がそのまま出てきます。コントロールプレーンを追加するには CA秘密鍵が必要です。新しいノードも証明書を発行する主体にならなければならないからです。 kubeadmはこのファイルをscpで運ばせるのではなく、kubeadm init phase upload-certs --upload-certsで 暗号化してクラスター内のSecretとしてアップロードし、64桁の16進数のcertificate-keyをその暗号文の鍵として渡します。

そしてこのSecretは2時間後に自動で削除されます。クラスターの信頼の根であるCA秘密鍵が、 暗号化された状態であってもクラスター内に存在する時間を最小にするための設計です。 「機密性の高い資料の露出ウィンドウ(window)を縮める」という原則が製品の機能として実装された好例です。

もう1つあります。そのホームラボでは、コントロールプレーンを2台に増やさず、いきなり3台まで増やしました。 理由は明確です。etcdのクォーラムは過半数で、メンバー2つの過半数は2なので、メンバー2つは1つよりも 障害確率がむしろ高くなります。どちらか一方が落ちれば書き込み不能になるからです。 可用性の設計で「多いほど良い」が間違いになる代表的な例です。

次のラボですること

次のラボでは、apiserverの危険なフラグの一覧を自分で書き、EncryptionConfigurationのYAMLを プロバイダーの順序まで正しく書き、kubeletの点検項目をまとめます。そして2つのネームスペースに ServiceAccountとRoleを設定し、分離が実際に機能していることをkubectl auth can-iで証明し、 匿名ユーザーが今このクラスターで何をできるのかを観察して記録します。