feat(paperclip): bind the built-in view ClusterRole to the paperclip SA (REN-111)
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful

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:
atlas-bot
2026-09-26 00:37:09 +00:00
co-authored by Claude Opus 5
parent 3d6c9940d6
commit 230756c35e
3 changed files with 120 additions and 8 deletions
@@ -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