Skip to main content
RunBook Academy

KubernetesLVII · AuthenticationAuthentication

Anonymous access — the request that has no identity

Advanced⏱ ~13 minkubectl

What you'll learn

  • Explain how an unauthenticated request becomes `system:anonymous`
  • Identify the production failure modes of leaving anonymous auth enabled
  • Audit the cluster for anonymous access and verify the controls
  • Recognise the audit log signals that anonymous requests leave

Prerequisites

Verified against Kubernetes 1.34.x · kubeadm 1.34.x · kubectl 1.34.x · etcd 3.6.x · CoreDNS 1.11.x · containerd 1.7.x / 2.x · 2026-08-16

Not yet marked complete on this device.

When the API server or kubelet receives a request that carries no credentials, it does not reject the request outright. It maps the request to a special identity — system:anonymous, with group system:unauthenticated — and continues through the authorisation chain. The request is then either rejected by RBAC (because system:unauthenticated is not bound to anything) or, in the worst case, allowed (because of AlwaysAllow or a misconfigured ClusterRoleBinding). This lesson covers how anonymous access works, the production failure modes, and the audit.

How anonymous access works

Every Kubernetes request is either authenticated or anonymous. The authentication chain returns either a UserInfo (authenticated) or, if no authenticator matched, the request continues as system:anonymous.

flowchart LR
    R[Request] --> TLS[TLS termination]
    TLS --> A{Authenticators}
    A -->|match| UI[UserInfo]
    A -->|no match| AN[system:anonymous]
    UI --> AUTH[RBAC]
    AN --> AUTH
    AUTH -->|allow| OK[200 OK]
    AUTH -->|deny| ER[403 Forbidden]

The --anonymous-auth=true flag (default in legacy charts, false in 1.34 defaults) controls whether unauthenticated requests are mapped to anonymous or rejected outright. With the flag enabled, the request proceeds to authorisation; with it disabled, the request is rejected at the authentication layer.

The production failure modes

Four misconfigurations turn anonymous access into a Critical incident:

MisconfigurationEffect
--anonymous-auth=true on the API serverUnauthenticated requests reach RBAC
--anonymous-auth=true on the kubeletUnauthenticated requests reach kubelet endpoints
kubelet --authorization-mode=AlwaysAllowAnonymous = full kubelet access
ClusterRoleBinding for system:unauthenticatedAnonymous RBAC allow

Each one is a CIS Benchmark FAIL. Together they are the worst-case scenario: anyone who can reach the API server or kubelet port has full cluster access, with no credentials, no audit trail beyond the system:anonymous username.

# Verify the API server flag
kubectl -n kube-system get pod kube-apiserver-... -o yaml | grep anonymous-auth
# Should show: --anonymous-auth=false

# Verify the kubelet flag on every node
ssh node-01 'cat /var/lib/kubelet/config.yaml | grep anonymous-auth'
# Should show: anonymousAuth: false

Where anonymous is still used

Two places anonymous access is intended:

  • Cluster info discoverykubectl cluster-info hits /version and /healthz, which are anonymous in some distributions. This is fine because the endpoints return only static data.
  • system:anonymous RoleBinding — a small number of built-in Roles allow system:authenticated (any authenticated user) to perform read-only operations like selfsubjectreviews. These are anonymous in the sense that the user has no specific identity beyond “authenticated.”

The defensive operator audits every system:unauthenticated binding and ensures the default system:anonymous group has no RoleBindings beyond what is required for cluster bootstrap.

Auditing anonymous access

The audit log records every anonymous request with user.username: "system:anonymous" and user.groups: ["system:unauthenticated"]. A well-tuned SIEM rule alerts on any anonymous request that is not /healthz, /readyz, /version, or /livez:

# Pseudo-rule for the audit webhook
- name: anonymous-access
  match:
    - verb: any
      user.username: system:anonymous
      level: RequestResponse
  exclude:
    - requestURI: /healthz
    - requestURI: /readyz
    - requestURI: /version
    - requestURI: /livez
  alert: high

A cluster that has any anonymous requests beyond the health endpoints has a misconfiguration.

Verifying the controls

# 1. API server flag
kubectl -n kube-system get pod -l component=kube-apiserver \
  -o jsonpath='{.items[*].spec.containers[*].args}' | tr ' ' '\n' | grep anonymous

# 2. Kubelet flag (every node)
for node in $(kubectl get nodes -o name); do
  echo "=== $node ==="
  kubectl debug node/$node -it --image=alpine -- \
    cat /var/lib/kubelet/config.yaml 2>/dev/null | grep -E "anonymous|authorization"
done

# 3. Anonymous-role bindings
kubectl get rolebindings,clusterrolebindings -A -o json | \
  jq '.items[] | select(.subjects[]?.name == "system:anonymous" or .subjects[]?.name == "system:unauthenticated")'

# 4. Anonymous can-i
kubectl auth can-i get pods --as=system:anonymous
# Should return: no

Production failure modes

  1. Anonymous access inherited from a Helm chart. The chart sets --anonymous-auth=true to support a legacy dashboard. The chart is upgraded, but the flag stays. The fix is to override the flag in the chart values.
  2. AlwaysAllow on kubelet for “easier debugging.” An operator sets AlwaysAllow to make kubectl exec from the host network work without an SA. The cluster is fully open to host-network attackers.
  3. system:unauthenticated bound to view. A team grants the built-in view ClusterRole to system:unauthenticated to allow a dashboard to read pods. Now any anonymous user can read every Pod in the cluster.
  4. No audit log rule for anonymous. Anonymous access is invisible because no rule alerts on it. The fix is to add the rule above and verify it fires.

Cross-course references

  • The Observability course covers the audit log rules that detect anonymous access.
  • The Linux course covers the host network that the attacker uses to reach the kubelet port.

Quiz

Knowledge check · 4 questions

  1. Q1. What is the worst-case Kubernetes authentication misconfiguration?

  2. Q2. Kubernetes 1.34 ships with `--anonymous-auth=true` by default for the API server and kubelet; an operator must explicitly set it to false.

  3. Q3. An attacker on the host network of node-01 runs `curl -k https://10.0.1.5:10250/exec/default/myapp/mainsh -d 'command=id'`. The kubelet returns `uid=0(root) gid=0(root) groups=0(root)`. What misconfiguration(s) allowed this?

    The kubelet is configured with `--anonymous-auth=true` (inherited from a Helm chart default) and `--authorization-mode=AlwaysAllow` (set to make debugging easier). The Pod `myapp` runs as root. There is no NetworkPolicy on the kubelet port.

  4. Q4. Name three checks you would run to verify a cluster has no anonymous access beyond the health endpoints.

Passing score: 75%. Answers are checked in this browser.

Production discipline

Anonymous access is the structural weakness of every Kubernetes authentication layer. The CIS Benchmark flags --anonymous-auth=true and AlwaysAllow as Critical; a cluster that has either is unmonitored, uncontrolled, and fully exploitable from any attacker with network reach. The discipline is to verify every API server flag, every kubelet flag, every RoleBinding that references system:anonymous or system:unauthenticated, and the audit log for any anonymous request beyond the health endpoints. A cluster whose anonymous access is disabled and whose anonymous role bindings are empty has the first control of zero trust in place.