system:public-info-viewer: One Binding, Two Trust Levels
A single kubeadm-installed ClusterRoleBinding lists both system:authenticated and system:unauthenticated as subjects against one shared ClusterRole, so any rule added for monitoring or discovery tooling becomes reachable without a bearer token or client certificate. RBAC returns allow, and audit logs show ordinary anonymous traffic rather than a policy violation worth escalating.
At a glance
- Unsafe setting
- system:public-info-viewer ClusterRoleBinding lists both system:authenticated and system:unauthenticated as subjects against one shared ClusterRole.
- Failure trigger
- A rule added to the bound ClusterRole for observability or discovery tooling becomes reachable by unauthenticated requests via the system:unauthenticated subject.
- Blast radius
- Anonymous requests to the API server gain whatever permissions were added to the shared ClusterRole, with no distinct audit signal separating them from routine anonymous health-check traffic.
- Recommended control
- Split the default binding, remove the system:unauthenticated subject, and gate any new nonResourceURLs rules behind a dedicated authenticated-only ClusterRole.
The Trap
Extending the ClusterRole behind the default system:public-info-viewer ClusterRoleBinding, not realising that binding’s subject list already includes Group system:unauthenticated alongside Group system:authenticated.
The Default State
kubeadm-bootstrapped control planes ship a ClusterRoleBinding named system:public-info-viewer that binds the ClusterRole of the same name to two group subjects at once: system:authenticated and system:unauthenticated. Out of the box that ClusterRole only permits GET on a handful of nonResourceURLs: /healthz, /version, /livez, /readyz and their subpaths. Platform teams later run kubectl edit clusterrole system:public-info-viewer, or apply a Helm chart that patches it, to expose additional discovery endpoints for monitoring agents, assuming the word “authenticated” in the binding’s name means the change is scoped to logged-in identities only. It is not; the rule set attaches to the ClusterRole, and the ClusterRole is bound to both groups simultaneously.
The Blast Radius
Once a new rule lands on that ClusterRole, granting GET on nonResourceURLs such as /metrics, /debug/pprof/*, or a custom aggregated discovery path, the endpoint becomes reachable by curl https://<apiserver>:6443/metrics with no Authorization header and no client certificate at all. With --anonymous-auth left at its default of true on most kubeadm and managed control planes, the unauthenticated request is authenticated internally as user system:anonymous, placed in group system:unauthenticated, and matches the shared binding cleanly. The authorization decision is a legitimate allow, so there is no denied-request audit entry to alert on; the request appears in logs as routine anonymous traffic hitting a health endpoint. Exposed data typically includes controller-manager and scheduler metrics containing node names and internal IPs, build version strings useful for CVE targeting, or profiling output from pprof revealing goroutine stacks and memory layout.
The Lead Mechanic Fix
Run kubectl get clusterrolebinding system:public-info-viewer -o yaml before touching the associated ClusterRole. Strip the anonymous subject rather than extend the shared role: kubectl patch clusterrolebinding system:public-info-viewer --type=json -p='[{"op":"remove","path":"/subjects/1"}]' after confirming the correct array index for the system:unauthenticated entry. For any additional discovery or metrics rule, create a dedicated ClusterRole and ClusterRoleBinding scoped only to Group system:authenticated or a named service account, never append to the default binding. Where anonymous health probes are still required externally, terminate them at a load balancer or kubelet-local healthz check rather than the API server, and set --anonymous-auth=false on kube-apiserver where feasible. Reconcile drift periodically with kubectl auth reconcile -f locked-rbac.yaml --remove-extra-permissions.
Apply the safer control
Before you change production
Confirm the affected scope, export the current configuration, and test the replacement control in a non-production environment first.
Fix commands and configuration
kubectl get clusterrolebinding system:public-info-viewer -o yamlkubectl patch clusterrolebinding system:public-info-viewer --type=json -p='[{"op":"remove","path":"/subjects/1"}]'system:unauthenticatedVerify, roll back or escalate
Verify
Re-run the detection test and a controlled negative-path test. Confirm the unsafe behaviour is blocked while approved traffic still succeeds.
Rollback
Restore the exported configuration if the new control blocks required production traffic, then narrow the policy before redeployment.
Escalate
Escalate when the blast radius is uncertain, the control cannot be tested safely, or remediation requires an outage or security exception.