diff --git a/kubernetes/apps/paperclip/cluster-view-rbac.yaml b/kubernetes/apps/paperclip/cluster-view-rbac.yaml new file mode 100644 index 00000000..3e68be55 --- /dev/null +++ b/kubernetes/apps/paperclip/cluster-view-rbac.yaml @@ -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 diff --git a/kubernetes/apps/paperclip/kustomization.yaml b/kubernetes/apps/paperclip/kustomization.yaml index f08c0722..52f5e729 100644 --- a/kubernetes/apps/paperclip/kustomization.yaml +++ b/kubernetes/apps/paperclip/kustomization.yaml @@ -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 diff --git a/kubernetes/apps/paperclip/prometheus-read-rbac.yaml b/kubernetes/apps/paperclip/prometheus-read-rbac.yaml index 10d67880..f7766a57 100644 --- a/kubernetes/apps/paperclip/prometheus-read-rbac.yaml +++ b/kubernetes/apps/paperclip/prometheus-read-rbac.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