skip to content

In Kubernetes, what are the user system:anonymous and the group system:unauthenticated, when does a request end up with them, and why is binding a ClusterRole to system:unauthenticated dangerous?

level: middleimportance: should knowfreq 32%

answer

  1. all authenticators abstain + anonymous-auth=true → system:anonymous
  2. group system:unauthenticated; default grant = system:public-info-viewer (healthz/livez/readyz/version)
  3. 403 not 401 = anonymous auth on, reached authorization
  4. never bind roles to system:unauthenticated or system:authenticated
  5. system:authenticated includes every ServiceAccount

basics

~20 s

When every authenticator abstains and anonymous access is enabled, the API server treats the request as user system:anonymous in group system:unauthenticated instead of returning 401. Any RBAC binding to that group applies to anybody who can reach the API server with no credentials at all — so binding a powerful ClusterRole to it exposes the cluster to unauthenticated callers.

solid answer

~50 s

kube-apiserver runs with `--anonymous-auth=true` by default. A request carrying no recognised credential is not rejected; it proceeds as user `system:anonymous` in group `system:unauthenticated`, and authorization then decides. In practice it is denied, because the only default grant is the `system:public-info-viewer` ClusterRole, bound to both `system:unauthenticated` and `system:authenticated`, allowing `/healthz`, `/livez`, `/readyz` and `/version`. That exists so load balancers can probe the API server without credentials. The danger is a binding — usually created while debugging — that attaches a real ClusterRole to `system:unauthenticated`. Anyone with network reachability to the API server then holds those permissions with no credential whatsoever, which for `cluster-admin` is total cluster compromise. The sibling trap is `system:authenticated`: it includes **every ServiceAccount in the cluster**, so binding to it turns any compromised pod into a holder of that role. Mitigations: never bind roles to either group, audit existing bindings, keep the API server off the public internet, and on newer clusters restrict anonymous access to specific health paths rather than disabling it wholesale.

code

bash · 16 lines
bash
# Any ClusterRoleBinding naming the two synthetic groups
kubectl get clusterrolebindings -o json \
  | jq -r '.items[]
      | select(.subjects != null)
      | select(any(.subjects[];
          .name == "system:unauthenticated"
          or .name == "system:authenticated"
          or .name == "system:anonymous"))
      | "\(.metadata.name)\t-> \(.roleRef.name)"'
# Expected output: only system:public-info-viewer

# What can an unauthenticated caller actually do?
kubectl auth can-i --list --as=system:anonymous --as-group=system:unauthenticated

# Confirm anonymous access is limited: 403 (not data) is the healthy answer
curl -k https://$APISERVER/api/v1/namespaces/kube-system/secrets

go deeper

for a junior

Know that a request with no credentials can be treated as system:anonymous in group system:unauthenticated, and that you must never grant permissions to that group.

for a middle

Explain the default system:public-info-viewer grant for health endpoints, that 403 rather than 401 indicates anonymous auth is enabled, and why system:authenticated is equally risky.

for a senior

Show how you would audit existing bindings, why RBAC's additive model means the binding must be deleted, and the path-restricted anonymous configuration as the modern fix, plus the kubelet's equivalent exposure.

for a principal

Treat it as an exposure-surface decision: API-server network reachability, guardrails preventing such bindings from being created, audit-log detection of authorized anonymous requests, and a fleet-wide policy on anonymous configuration.

## How a request becomes anonymous Authentication is a chain of authenticators, each of which either identifies the caller or abstains. When all of them abstain — no client certificate the API server trusts, no bearer token, no recognised proxy header — there are two possible behaviours: - With `--anonymous-auth=false`, the API server returns **401 Unauthorized**. - With `--anonymous-auth=true` (the default), the request continues with the synthetic identity **`system:anonymous`**, in the group **`system:unauthenticated`**. The key point is that anonymous requests are not rejected at authentication; they are handed to authorization like any other identity. Whether they do anything depends entirely on RBAC. An important subtlety: a request presenting a **bad** credential — an expired token, a certificate from an untrusted CA — is normally a 401, not an anonymous request. Anonymity is for the absence of credentials, not for invalid ones. ## Why anonymous access exists at all Infrastructure needs to probe the API server before it has credentials. Load balancers hit `/healthz`, `/livez` and `/readyz`; clients check `/version` for compatibility; kubeadm-style bootstrapping fetches public cluster info. Kubernetes ships this as a tightly scoped default: the ClusterRole **`system:public-info-viewer`** grants `get` on exactly those non-resource paths, and the ClusterRoleBinding `system:public-info-viewer` attaches it to both `system:unauthenticated` and `system:authenticated`. So an out-of-the-box cluster answers unauthenticated health probes and denies everything else with 403 — note, **403 not 401**, which is a useful fingerprint: it tells you anonymous auth is enabled and the request reached authorization. ## The failure mode The damage comes from RBAC bindings that reference these synthetic groups. Two recur constantly: **Binding to `system:unauthenticated`.** Almost always created while debugging ("nothing works, let me widen this and narrow it later"), then forgotten. The result is that every permission in that role is available to anyone who can open a TCP connection to the API server, with no credential at all. With `cluster-admin` that is immediate, complete cluster takeover — read every Secret, create a privileged pod, take the nodes. **Binding to `system:authenticated`.** Superficially safer, and far more common, but that group contains **every ServiceAccount in the cluster** as well as every human. So granting it `secret` read means every pod's token — including a compromised third-party sidecar in a sandbox namespace — can read Secrets cluster-wide. It converts any container compromise into a cluster-wide one. `system:public-info-viewer` is essentially the only binding to it that belongs. Whether this is exploitable from outside also depends on network exposure: an API server reachable from the internet turns an anonymous binding into a remote, unauthenticated compromise, while a private endpoint limits it to the network. Both should be fixed; the network position only determines urgency. ## Finding and fixing it List every binding that references the two groups and justify each one; the only expected result is `system:public-info-viewer`. Because RBAC is additive and never denies, you cannot neutralise a bad binding with another rule — you must delete or narrow it. Setting `--anonymous-auth=false` is the blunt fix, and it breaks unauthenticated health probing, which is why many managed platforms leave anonymous auth on. Newer Kubernetes versions offer a better option: **anonymous authentication configuration** that restricts anonymous requests to an explicit list of paths (typically `/healthz`, `/livez`, `/readyz`). Anonymous access then works for probes and is structurally impossible everywhere else, so a stray binding to `system:unauthenticated` cannot be exploited beyond those endpoints. Complementary controls: keep the API server endpoint private or firewalled; alert on creation of ClusterRoleBindings naming either group; and check the audit log for authorized requests from `system:anonymous`, which should be nothing but health endpoints. ## The related kubelet exposure The same reasoning applies to the kubelet's own API. A kubelet running with `--anonymous-auth=true` and authorization mode `AlwaysAllow` — an old default and still seen in hand-rolled clusters — lets anyone who reaches port 10250 list pods and exec into containers on that node without credentials. Kubelets should use webhook authentication and authorization delegated to the API server, with anonymous auth off.

  • Why is binding a Role to system:authenticated nearly as dangerous as binding it to system:unauthenticated?
    Every ServiceAccount in the cluster is a member of system:authenticated, alongside every human user. A grant to that group therefore applies to every pod that mounts a ServiceAccount token, including third-party sidecars and workloads in untrusted namespaces. Any single container compromise then yields the whole permission set, turning a contained incident into a cluster-wide one.
  • Should you simply set --anonymous-auth=false everywhere?
    It removes the exposure but also breaks unauthenticated health and version probing, which load balancers and bootstrap tooling rely on, so many managed platforms will not allow it. The better modern option is the authentication configuration that permits anonymous requests only on an explicit path list such as /healthz, /livez and /readyz; probes keep working while any binding to system:unauthenticated becomes unexploitable beyond those endpoints.

saying these in an interview costs you the question

  • Assuming a credential-less request always gets 401 — with anonymous auth on it proceeds as system:anonymous and is usually denied at authorization with 403.
  • Treating system:authenticated as 'real users only', forgetting it includes every ServiceAccount.
  • Trying to neutralise a dangerous binding with a deny rule; RBAC is additive and has no deny.
  • Believing anonymous access is harmless because the API server is behind a firewall — it is still exploitable from anything on that network, including a pod.
  • Ignoring the kubelet's own anonymous-auth setting, which can expose exec on port 10250.

context