Skip to main content
cd ../config-traps
risk/register/systempublic-info-viewer-one-binding-two-trust-levels.html
Kubernetes RBAChigh severityKubernetes

system:public-info-viewer: One Binding, Two Trust Levels

Severity
high
Reviewed
28 Jul 2026
Remediation
~20 minutes
Overview

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.

03

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 yaml
kubectl patch clusterrolebinding system:public-info-viewer --type=json -p='[{"op":"remove","path":"/subjects/1"}]'
system:unauthenticated
04

Verify, 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.

After remediation

Further reading stays below the corrective workflow and is selected by platform, category and shared technical keywords.