feat(paperclip): bind the built-in view ClusterRole to the paperclip SA (REN-111)
Implements the owner's decision recorded on REN-52 (2026-09-25, option B), which was never shipped: one ClusterRoleBinding, roleRef the built-in `view` ClusterRole, subject the ServiceAccount REN-36 created. No new ServiceAccount, no new ClusterRole, no write verb, no Secrets. Kept in its own file rather than appended to prometheus-read-rbac.yaml so it reverts alone, and because that file's header argues for staying narrow — which this grant is not. Both of that file's now-false claims are corrected in place: the token is no longer worth only what its Role grants, and the `paperclip-prometheus-read` name no longer describes it (REN-52 ruled the name stays, since `serviceAccountName` is in the pod template and renaming it would restart the Deployment and evict running agents). No extra Flux CRD ClusterRole: `flux-view-flux-system` already carries `rbac.authorization.k8s.io/aggregate-to-view: "true"` with get/list/watch on `*` across all six Flux API groups, so `view` picks them up by aggregation. REN-52 pre-approved adding one only if that were absent. Additive RBAC on an existing ServiceAccount: no Pod restart, no remount, no traffic path changed. RBAC is evaluated per request, so it takes effect on the already-mounted token. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
3d6c9940d6
commit
230756c35e
@@ -0,0 +1,97 @@
|
||||
---
|
||||
# Cluster-wide READ for the agents in the paperclip pod (REN-111, decided on
|
||||
# REN-52 2026-09-25 by the owner, option B).
|
||||
#
|
||||
# WHAT THIS IS
|
||||
#
|
||||
# One ClusterRoleBinding. It binds the Kubernetes built-in `view` ClusterRole to
|
||||
# the ServiceAccount that REN-36 created in `../paperclip/prometheus-read-rbac.yaml`.
|
||||
# No new ServiceAccount, no new ClusterRole, no write verb anywhere. `view` is
|
||||
# maintained upstream and holds get/list/watch only; revocation is deleting this
|
||||
# one object.
|
||||
#
|
||||
# WHY IT IS A SEPARATE FILE
|
||||
#
|
||||
# `prometheus-read-rbac.yaml` is deliberately narrow and its header argues for
|
||||
# staying that way. This grant is the opposite in character — cluster-wide,
|
||||
# every namespace — so it is kept in its own file and its own kustomization
|
||||
# entry. Reverting it is deleting this file and one line from
|
||||
# `kustomization.yaml`; nothing in the REN-36 path is touched, and the token
|
||||
# falls back to `get services/proxy` on two named Services.
|
||||
#
|
||||
# WHY THE OWNER WANTED IT
|
||||
#
|
||||
# REN-108 part B had to validate a CiliumNetworkPolicy allowlist whose selector
|
||||
# was `app.kubernetes.io/name: prometheus-blackbox-exporter`, and could not:
|
||||
# kube-state-metrics here does not export `kube_pod_labels`, and
|
||||
# blackbox-exporter is not a scrape target so Prometheus has no
|
||||
# `__meta_kubernetes_pod_label_*` for it either. A CNP selector that matches no
|
||||
# pod allows nothing and reports no error, so the proposal shipped unverified.
|
||||
# `list pods` closes that, and the same gap recurs on every netpol or
|
||||
# scrape-selector change.
|
||||
#
|
||||
# WHAT IT COSTS — ACCEPTED ON REN-52, NOT DISCOVERED HERE
|
||||
#
|
||||
# * EVERY process in the paperclip pod can read the mounted token, so this is
|
||||
# a grant to every agent, not to one. REN-36 was approved on the premise
|
||||
# that "the token is only worth what this Role grants"; this file changes
|
||||
# that arithmetic, and the owner priced it knowingly.
|
||||
# * `view` reads ConfigMaps in EVERY namespace. This repo keeps application
|
||||
# configuration in ConfigMaps. Chosen over Hermes' narrower alternative.
|
||||
# * `view` grants `pods/log`, which surfaces anything an application prints,
|
||||
# including credentials it prints by mistake.
|
||||
#
|
||||
# WHAT IT DOES NOT GRANT
|
||||
#
|
||||
# * Secrets. `view` excludes them. "Secrets, metadata only" was the original
|
||||
# ask and is NOT expressible in Kubernetes RBAC — the authorizer has no
|
||||
# field-level verb — so that part of the ask is dead. Do not re-add Secrets
|
||||
# here to approximate it.
|
||||
# * Any write verb, on anything.
|
||||
# * ClusterRoles and RoleBindings: `view` excludes the whole
|
||||
# `rbac.authorization.k8s.io` group, on purpose, so a holder cannot read the
|
||||
# shape of its own cage or find a Role to escalate through.
|
||||
#
|
||||
# FLUX CRDs — COVERED BY AGGREGATION, NO EXTRA ClusterRole NEEDED
|
||||
#
|
||||
# REN-52 pre-approved adding a read-only ClusterRole for the Flux CRDs *if*
|
||||
# aggregation did not already cover them. It does, so none is added:
|
||||
# `flux-view-flux-system` carries `rbac.authorization.k8s.io/aggregate-to-view:
|
||||
# "true"` and lists get/list/watch on `*` across all six API groups —
|
||||
# `../../clusters/homelab/flux-system/gotk-components.yaml:232-254`. The
|
||||
# aggregation controller merges those rules into `view` itself, so this one
|
||||
# binding picks up kustomizations, helmreleases, gitrepositories, imagepolicies,
|
||||
# imagerepositories and imageupdateautomations. Read from git, not from a live
|
||||
# ClusterRole — `view` cannot read ClusterRoles, so it is unverifiable from
|
||||
# inside the pod. The post-merge SelfSubjectAccessReview on REN-111 proves
|
||||
# whether aggregation actually fired in the cluster; if `list kustomizations`
|
||||
# comes back `allowed: false` there, the pre-approved ClusterRole is the fix.
|
||||
#
|
||||
# PRODUCTION
|
||||
#
|
||||
# Adding a ClusterRoleBinding mutates no workload, restarts no Pod and moves no
|
||||
# traffic. RBAC is evaluated per request, so the grant takes effect on the
|
||||
# already-mounted token with no remount and no rollout. That sequencing was the
|
||||
# point of REN-52 putting this after REN-36: the paperclip Deployment restart
|
||||
# was paid once, by REN-36, and is already paid.
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: paperclip-cluster-view
|
||||
labels:
|
||||
app.kubernetes.io/name: paperclip
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
# Built-in, shipped by kube-controller-manager's default RBAC bootstrap. Not
|
||||
# defined in this repo and must not be: it is auto-reconciled, so a local copy
|
||||
# would be overwritten on every apiserver start.
|
||||
name: view
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: paperclip-prometheus-read
|
||||
# A ClusterRoleBinding has no namespace of its own, so this is the only
|
||||
# place the subject's namespace can be stated. Omit it and the binding
|
||||
# resolves to no subject and fails closed, looking identical to a missing
|
||||
# grant.
|
||||
namespace: paperclip
|
||||
@@ -8,6 +8,7 @@ resources:
|
||||
- scheduled-backup.yaml
|
||||
- pvc.yaml
|
||||
- prometheus-read-rbac.yaml
|
||||
- cluster-view-rbac.yaml
|
||||
- paperclip.yaml
|
||||
- gate-service.yaml
|
||||
- ingress.yaml
|
||||
|
||||
@@ -23,10 +23,20 @@
|
||||
# ONE VERB, NAMED SERVICES ONLY — ON PURPOSE
|
||||
#
|
||||
# Every process in the paperclip pod can read the mounted ServiceAccount token,
|
||||
# so the token is only worth what this Role grants. It grants `get` on the two
|
||||
# Services named below and nothing else. Widening it is a new card and a new
|
||||
# review, not an edit here — the Alertmanager name was added that way (REN-95,
|
||||
# for read-only alert-delivery checks; parent REN-94).
|
||||
# so the token is worth the sum of everything bound to this ServiceAccount. This
|
||||
# Role's own share of that is `get` on the two Services named below and nothing
|
||||
# else. Widening it is a new card and a new review, not an edit here — the
|
||||
# Alertmanager name was added that way (REN-95, for read-only alert-delivery
|
||||
# checks; parent REN-94).
|
||||
#
|
||||
# THIS IS NO LONGER THE ONLY GRANT ON THIS ServiceAccount
|
||||
#
|
||||
# `cluster-view-rbac.yaml` in this directory binds the built-in `view`
|
||||
# ClusterRole to the same ServiceAccount, cluster-wide (REN-111, owner-approved
|
||||
# on REN-52). So reading this file alone now understates what the token can do:
|
||||
# it also carries get/list/watch on most namespaced resources in every
|
||||
# namespace, including ConfigMaps and `pods/log`. Still no Secrets and still no
|
||||
# write verb. Read both files.
|
||||
#
|
||||
# `resourceNames` is matched verbatim by the authorizer, not as a selector:
|
||||
# `svc`, `svc:9090` and `http:svc:9090` are three different strings. Only the
|
||||
@@ -46,10 +56,14 @@ metadata:
|
||||
# second grant, the name is a lie and the review should say so. Whether the
|
||||
# token is mounted is decided on the pod (`paperclip.yaml`), not here.
|
||||
#
|
||||
# REN-95 stretched the name: it now also proxies to Alertmanager. Kept as-is
|
||||
# deliberately — `serviceAccountName` is in the pod template, so renaming it
|
||||
# would restart the paperclip Deployment and evict running agents. Read the name
|
||||
# as "read-only kube-prometheus-stack access", and read the Role for the truth.
|
||||
# REN-95 stretched the name: it now also proxies to Alertmanager. REN-111 broke
|
||||
# it outright — this account now also holds cluster-wide `view`, which is not
|
||||
# Prometheus and not read of one thing. Kept as-is deliberately, and REN-52
|
||||
# ruled it stays: `serviceAccountName` is in the pod template, so renaming it
|
||||
# would restart the paperclip Deployment and evict running agents, which is a
|
||||
# larger production cost than a misleading name. Read the name as "the paperclip
|
||||
# pod's read-only cluster identity", and read this file plus
|
||||
# `cluster-view-rbac.yaml` for the truth.
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
|
||||
Reference in New Issue
Block a user