KubernetesLVII · AuthenticationAuthentication
Anonymous access — the request that has no identity
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
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:
| Misconfiguration | Effect |
|---|---|
--anonymous-auth=true on the API server | Unauthenticated requests reach RBAC |
--anonymous-auth=true on the kubelet | Unauthenticated requests reach kubelet endpoints |
kubelet --authorization-mode=AlwaysAllow | Anonymous = full kubelet access |
ClusterRoleBinding for system:unauthenticated | Anonymous 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 discovery —
kubectl cluster-infohits/versionand/healthz, which are anonymous in some distributions. This is fine because the endpoints return only static data. system:anonymousRoleBinding — a small number of built-in Roles allowsystem:authenticated(any authenticated user) to perform read-only operations likeselfsubjectreviews. 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
- Anonymous access inherited from a Helm chart.
The chart sets
--anonymous-auth=trueto support a legacy dashboard. The chart is upgraded, but the flag stays. The fix is to override the flag in the chart values. - AlwaysAllow on kubelet for “easier debugging.”
An operator sets
AlwaysAllowto makekubectl execfrom the host network work without an SA. The cluster is fully open to host-network attackers. system:unauthenticatedbound toview. A team grants the built-inviewClusterRole tosystem:unauthenticatedto allow a dashboard to read pods. Now any anonymous user can read every Pod in the cluster.- 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
Q1. What is the worst-case Kubernetes authentication misconfiguration?
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.
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.
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.