RBAC — 四つのオブジェクトと一つの罠
一言でいうと
RBACは、「何ができるか」(Role)と「誰がどこでそれをできるか」(Binding)を分離したモデルで、範囲を決めるのは常にBindingのほうです。
なぜ必要なのか
権限を1つのファイルにすべて書くと、2つのことがすぐに破綻します。人が増えるたびに同じ権限のまとまりをコピーしなければならなくなり、どの権限がどこまで及ぶのか、誰も追跡できなくなります。RBACは、この2つを切り離します。権限の内容はRoleとClusterRoleが定義し、その内容を誰にどの範囲で与えるかは、RoleBindingとClusterRoleBindingが決めます。
| オブジェクト | スコープ | やること |
|---|---|---|
| Role | ネームスペース | そのネームスペースのリソースに対するルール |
| ClusterRole | クラスター | クラスタースコープのリソース、または再利用する共通の権限 |
| RoleBinding | ネームスペース | ロールをサブジェクトに、このネームスペースでのみ与える |
| ClusterRoleBinding | クラスター | ロールをサブジェクトに、クラスター全体で与える |
どう動くのか
ルール1つは、3つの要素でできています。apiGroups、resources、verbsです。
rules:
- apiGroups: [""] # 코어 그룹은 빈 문자열
resources: ["pods"]
verbs: ["get", "list", "watch"]
apiGroups: [""]を書き忘れてPodが見えないという問い合わせは、今でも毎週出てきます。コアAPIグループの名前は空文字列で、Deploymentなどはappsグループにあります。
ここで、最も重要な落とし穴が出てきます。ClusterRoleをRoleBindingで紐づけると、その権限は該当のネームスペースでのみ有効です。つまり、スコープを決めるのはロールの種類ではなく、バインディングの種類です。この組み合わせは、実務で非常に役に立ちます。「読み取り専用」のような共通の権限のまとまりをClusterRoleとして一度だけ定義しておき、チームごとに自分のネームスペースでRoleBindingで紐づけて使えばよいのです。同じClusterRoleをClusterRoleBindingで紐づけると、その瞬間にクラスター全体の権限になります。2つのマニフェストの違いは、単語1つだけです。
2つ目に見落としやすいのが、サブリソースです。pods/logとpods/execは、podsとは別のリソース名です。Podを読む権限があるからといってログが読めるわけではなく、ログを読む権限があるからといってシェルに接続できるわけでもありません。ログ収集のボットにpods/logのgetだけを与える設計が可能な理由であり、逆にpods/execを誰にでも開けておけば、コンテナの中で何でもできてしまうということでもあります。
3つ目は確認方法です。権限は書いたものではなく判定されるものなので、目で読まずに問い合わせる必要があります。
kubectl auth can-i list pods -n rbac-lab \
--as=system:serviceaccount:rbac-lab:app-reader
# yes
--asはimpersonationです。サービスアカウントの正式な名前の形式がsystem:serviceaccount:<네임스페이스>:<이름>であることさえわかれば、どんなサブジェクトにもなりすまして問い合わせられます。できることとできないことを一緒に確認して初めて意味があります。yesだけを確認すると、権限が必要以上に広くても気づけません。
現場での姿
1つ目は、ワイルドカードは一度入ると抜けないことです。verbs: ["*"]は便利ですが、あとで新しいリソースができると、誰も知らないうちに権限が自動的に広がります。最小権限は好みの問題ではなく、時間が経っても範囲が大きくならないようにするための仕組みです。
2つ目は、get secretsは事実上、認証情報の閲覧権限であることです。開発者に便宜上与えた編集権限が、本番データベースのパスワードの閲覧権限と同じになってしまうことがよくあります。この話は、次のモジュールへつながります。
3つ目は、定期監査です。cluster-adminが設定されたClusterRoleBindingの一覧、ワイルドカードを使っているロール、secretsを読み取れるロール。この3つの問いは、クラスターごとに定期的に投げかける必要があります。kubectl get clusterrolebindings -o json | jqを数行書けば答えが出ますし、この習慣1つで権限負債が積み上がる速度を大きく下げられます。
権限が効かない理由を突き止める方法
RBACの問題は、推測せずに問い合わせられます。kubectl auth can-iが、APIサーバーに直接判定を問い合わせます。
kubectl auth can-i create pods -n prod
kubectl auth can-i list secrets -n prod --as=system:serviceaccount:prod:app
kubectl auth can-i --list --as=system:serviceaccount:prod:app -n prod
最後の行が特に便利です。そのサブジェクトが持つ権限のすべてを表で表示します。
対象を正確に書く必要があります。サービスアカウントの名前はsystem:serviceaccount:<네임스페이스>:<이름>です。ユーザーアカウントは、証明書のCNやOIDCのクレームに由来します。--asでなりすますには、それ自体がimpersonate権限を必要とします。
ルールは足されるだけです。RBACには拒否がありません。権限が広すぎる場合は、どこかで付与されているはずなので、バインディングを探す必要があります。
kubectl get rolebinding,clusterrolebinding -A -o json | jq -r '
.items[] | select(.subjects[]? .name=="app")
| "\(.kind) \(.metadata.namespace // "-")/\(.metadata.name) -> \(.roleRef.name)"'
よく混同される4つのポイント。
ClusterRoleをRoleBindingで紐づけると、そのネームスペース内でのみ有効です。同じロールを複数のネームスペースで再利用する、正当な方法です。- ノード・PV・ネームスペースのようにネームスペースを持たないリソースには、
ClusterRoleBindingが必要です。 - サブリソースは別に書きます。
pods/log、pods/exec、deployments/scaleは、pods、deploymentsに含まれません。 create権限だけでは、特定の名前を制限できません。resourceNamesは、名前を知っている必要がある動詞(get・update・delete)でのみ動作します。
デフォルトで付与されるものを忘れないでください。認証されたすべてのユーザーは、system:discoveryなどによってAPIの一覧を見ることができ、Podにマウントされたサービスアカウントのトークンは、権限が何もなくてもAPIサーバーに到達できます。不要ならautomountServiceAccountToken: falseでオフにします。
次のラボですること
読み取り専用のRoleを作成してサービスアカウントにバインドし、auth can-iで境界を確認します。ノードのようなクラスタースコープのリソース用のClusterRoleを作成し、同じClusterRoleをRoleBindingで紐づけて、スコープがどのように狭まるかを自分の目で確かめます。サブリソースだけを開くロールを作成し、最後に、クラスターの危険な権限を洗い出す監査レポートをJSONで作成します。