認証と認可は別の層だ
一言でいうと
apiserverに入ってきたリクエストは、認証(あなたは誰か) → 認可(その人はこれをしてよいか) → アドミッション(その内容はルールに合っているか)の順に通過します。RBACは2番目の層1つであり、1番目の層は、証明書とトークンの世界です。この2つを混ぜると、診断がもつれます。
なぜ必要なのか
Kubernetesにはユーザーオブジェクトがありません。kubectl get usersはありません。なぜなら、apiserverは身元の発行者ではなく、検証者だからです。身元の出どころは外にあります。
- クライアント証明書: CNがユーザー名、Oがグループになります。
kubeadmが作ってくれるadmin.confが、この方式です。 - ServiceAccountトークン: Pod内のワークロードの身元です。
system:serviceaccount:<네임스페이스>:<이름>という名前(プレースホルダーはネームスペースと名前です)と、system:serviceaccountsグループを持ちます。 - OIDC、Webhook、ブートストラップトークンなどです。
そのため、「そのユーザーを削除してください」というリクエストは成り立ちません。証明書を失効させるか、バインディングを削除するしかありません。
認可側は、4種類のオブジェクトだけです。
| ネームスペースの範囲 | クラスターの範囲 | |
|---|---|---|
| 権限の定義 | Role | ClusterRole |
| 権限の付与 | RoleBinding | ClusterRoleBinding |
ここに、紛らわしい組み合わせがちょうど1つあります。RoleBindingがClusterRoleを参照することは許可されており、その場合、ClusterRoleのルールは、そのRoleBindingがあるネームスペースの中でのみ適用されます。view、editのような組み込みのClusterRoleを、ネームスペースごとに再利用する標準的な方法です。逆に、ClusterRoleBindingを使うとクラスター全体に広がるので、ネームスペースの境界を守ろうとして、ここで間違えることが多くあります。
RBACは累積するだけです。拒否ルールがありません。どのバインディングであれ、一度許可すれば許可です。そのため、「なぜこのアカウントがこれをできるのか」を追跡するときは、すべてのバインディングを洗い出す必要があり、kubectl auth can-i --as=が、その近道です。このコマンドは、SubjectAccessReviewを作成してapiserverに尋ねるだけで、実際のリクエストは送りません。副作用なしに、安全に確認できます。
どう動くのか
Aggregated ClusterRoleは、ルールを直接書かないClusterRoleです。aggregationRuleにラベルセレクターを書くと、コントローラーが、そのラベルの付いたほかのClusterRoleたちを見つけて、rulesを埋めます。CRDで新しいリソースを追加したときに、既存のview/editのロールに、ラベル1つで載せられる拡張ポイントです。ここに手でrulesを書くと、コントローラーが上書きします。
automountServiceAccountTokenは、PodにAPIトークンを入れるかどうかを決めます。デフォルトでは入れます。そうすると、APIをまったく使わないnginxのPodにもトークンが入り、コンテナが侵害されると、そのトークンがそのままクラスターへのアクセス権になります。ServiceAccountやPodのスペックでfalseにすると、kube-api-access-*の投影ボリュームそのものが注入されません。
現場での姿
事例1: CA秘密鍵の露出期間を2時間に絞ります。ホームラボを3ノードから7ノードに増やすとき、ワーカーの参加とコントロールプレーンの参加の違いが、認証の構造をそのまま見せてくれました。ワーカーは、トークン1つで終わりです。
kubeadm token create --print-join-command
コントロールプレーンは違います。新しいノードも証明書を発行する主体になる必要があるので、CA秘密鍵が必要です。kubeadmは、この機微なファイルをscpで運ばせず、kubeadm init phase upload-certs --upload-certsで、CA鍵の束を暗号化してクラスター内のSecretとしてアップロードします。参加コマンドに渡す64桁のcertificate-keyが、その暗号文を解く対称鍵です。
そしてこのSecretは、2時間後に自動削除されます。暗号化されているとはいえ、クラスターの信頼の根が中に入っているので、露出期間を最小化しようとする設計です。期限が切れたら、upload-certsを再実行すればよく、既存のクラスターには影響がありません。認証の核心は、このように「信頼の根を、どれだけ短く露出するか」です。
事例2: 証明書のSANがそのままアクセス権になります。同じクラスターで、コントロールプレーンノードを3つに増やしたのに、最初のノードが落ちると、誰もつなげませんでした。apiserverの証明書のSANに、残りの2ノードのIPがなかったからです。RBACをどれだけうまく組んでも、TLS検証を通過できなければ、認可の層にすら到達できません。逆に、この性質を逆手に取り、落ちたノードのIPを生きているノードのインターフェースに載せると、証明書の再発行もkubeconfigの修正もなく、すべてのクライアントがそのまま再接続します。クライアントが検証するのはノードではなく、接続したアドレスがSANにあるかだからです。
事例3: 権限ではなく設定が問題のときもあります。GPU Operatorを導入しているときに、Podがすべて、Initで止まったことがあります。kubectl get runtimeclassにはnvidiaが問題なくありましたが、ノードのcontainerdの設定には、nvidiaが1文字もありませんでした。ラベルはKubernetesのオブジェクトとして存在し、実体はノードになければならないのに、後者がなかったのです。オブジェクトがあることと、実際に動作することは別の命題です。認可の問題を診断するときにも、同じ習慣が必要です。can-iがyesを返したなら、その次は、証明書、ネットワーク、アドミッションWebhookの順に、下っていく必要があります。
次のラボですること
ServiceAccountを作成し、Role/RoleBindingで読み取り権限だけを与え、kubectl auth can-i --as=で、できることとできないことの両方を確認します。そのあと、ClusterRole/ClusterRoleBindingでクラスタースコープのリソースを開き、Aggregated ClusterRoleを構成し、最後に、トークンがそもそも注入されないPodを作成します。