リクエスト一つがapiserverを通る道
一言でいうと
apiserverに入ってきたリクエストは、認証 → 認可 → アドミッション → 検証 → 保存の順に流れます。 各段階が何を判断し、何は判断しないのかがわかれば、セキュリティに関する質問の大半は解けます。
なぜ必要なのか
「このリクエストはなぜ拒否されたのか」に答えるには、どの段階で引っかかったのかを知る必要があります。 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フラグで指定します。
- Node: kubeletが自分のノードに関係するものだけを読めるように制限
- RBAC: ロールベース。実質的にこれが本体
- Webhook / ABAC: 外部への委任 / ファイルベース(レガシー)
- AlwaysAllow: すべて許可。本番環境では致命的です
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つの方向の身元を持ちます。
- 外へ出る方向: apiserverに接続するときに使うクライアント証明書。
system:node:<노드명>(プレースホルダーはノード名です)という ユーザー名とsystem:nodesグループを持ち、Node認可モジュールがこの身元で範囲を制限します。 - 入ってくる方向: 誰かがkubelet API(10250)を呼び出すとき。ここでは
authentication.anonymous.enabledとauthorization.modeが決定的です。authorization.mode: AlwaysAllowなら認証さえ通ればなんでも でき、Webhookならapiserverに問い合わせます。Webhookが正解です。
コントローラーマネージャーとトークンの発行
コントローラーマネージャーは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で証明し、
匿名ユーザーが今このクラスターで何をできるのかを観察して記録します。