Authentication and the Path Into the API Server

K
Kai··5 min read

Part X handled network traffic. Part XI moves to who gets to do what on the cluster, and the starting point is the path into the API server. Every kubectl get, every controller call to the API, is an HTTPS request, and the API server processes it through three stages in order:

   request (kubectl, controller, pod...)
        │
        ▼
   ┌─────────────────┐   who are you?      401 if every authenticator rejects
   │ Authentication  │ ──────────────────►  (attaches username + groups to the request)
   └─────────────────┘
        │
        ▼
   ┌─────────────────┐   what may you do?   403 if no permission
   │ Authorization   │ ──────────────────►  (Node, RBAC — Article 52)
   └─────────────────┘
        │
        ▼
   ┌─────────────────┐   is the request valid / does it need editing?
   │ Admission       │ ──────────────────►  (NodeRestriction, PSA — Article 54)
   └─────────────────┘
        │
        ▼   write to etcd / return data

This article stops at the first stage: the API server figuring out who you are. This stage only attaches a username and groups to the request and passes it on — it doesn't decide what you're allowed to do, that's authorization's job in the next article.

Two kinds of identity

Kubernetes distinguishes two kinds of caller. A normal user — like you via kubectl — is not an object in the cluster; there's no kind: User to kubectl get. Their identity lives outside the cluster, in a client certificate or token, and the API server only trusts the CA that signed it. A ServiceAccount is the opposite: an object in the cluster, belonging to a namespace, meant for processes running inside the cluster (pods, controllers) calling the API.

The cluster's API server authentication flags (set in Article 7) show how it accepts identities:

ssh controller-0 'grep -oE "\-\-(client-ca-file|service-account-key-file|anonymous-auth)[^ ]*" \
  /etc/systemd/system/kube-apiserver.service'
--client-ca-file=/var/lib/kubernetes/ca.pem
--service-account-key-file=/var/lib/kubernetes/service-account.pem

--client-ca-file enables authentication by client certificate (this CA signs from Article 4); --service-account-key-file enables ServiceAccount token authentication (the key pair from Article 5).

Client certificate: the one we're using

admin.kubeconfig contains a client certificate. Look at how it encodes identity:

openssl x509 -in admin.pem -noout -subject
subject=C = VN, O = system:masters, OU = k8s-scratch, CN = admin

The API server reads two fields: CN (admin) becomes the username, O (system:masters) becomes the group. Ask the API server who it sees us as:

kubectl auth whoami
ATTRIBUTE                                           VALUE
Username                                            admin
Groups                                              [system:masters system:authenticated]
Extra: authentication.kubernetes.io/credential-id   [X509SHA256=69463ed56ced...]

Username admin is correct from the CN, group system:masters is correct from the O, and system:authenticated is added by the API server for every authenticated request. The group system:masters is why admin has full power — there's a built-in ClusterRoleBinding tying it to the highest privilege:

kubectl get clusterrolebinding cluster-admin \
  -o jsonpath='{.roleRef.kind}/{.roleRef.name} <- {.subjects[0].kind}/{.subjects[0].name}'
ClusterRole/cluster-admin <- Group/system:masters

This is the point to remember: the certificate itself grants no permission, it only proves identity. Permission comes from a binding at the authorization stage. system:masters has full power only because that binding exists — and for that same reason, signing a cert with O=system:masters hands out full cluster-wide power, so the client CA must be guarded tightly.

ServiceAccount token

The second kind of identity is for processes inside the cluster. Create a ServiceAccount, then request a token for it:

kubectl create namespace authn-demo
kubectl -n authn-demo create serviceaccount bot
TOK=$(kubectl -n authn-demo create token bot --duration=10m)

This token is a JWT. Use it to ask the API server (with an empty kubeconfig so the token isn't overridden by admin's client cert):

kubectl --kubeconfig=/dev/null --token="$TOK" \
  --server=https://10.0.1.10:6443 --certificate-authority=ca.pem auth whoami
Username   system:serviceaccount:authn-demo:bot
Groups     [system:serviceaccounts system:serviceaccounts:authn-demo system:authenticated]

The username follows the pattern system:serviceaccount:<namespace>:<name>, and the token automatically lands in two groups by namespace. Decode the JWT payload (the middle part, base64) to see what the API server put there:

echo "$TOK" | cut -d. -f2 | base64 -d | python3 -m json.tool
{
  "aud": ["https://10.0.1.10:6443"],
  "exp": 1779579030,
  "iss": "https://10.0.1.10:6443",
  "sub": "system:serviceaccount:authn-demo:bot",
  "kubernetes.io": {"namespace": "authn-demo", "serviceaccount": {"name": "bot", "uid": "..."}}
}

sub is the identity, iss and aud bind the token to this exact API server (a token issued for this server can't be used elsewhere), exp is the expiry — the token has a clear lifetime rather than living forever. This is the modern bound token, different from the old never-expiring kind; Article 53 digs into the mechanism that issues these tokens and how a pod receives one automatically.

Anonymous request

If a request carries no credential, the API server doesn't reject it right at authentication — it assigns an anonymous identity and lets authorization decide:

curl -s --cacert ca.pem https://10.0.1.10:6443/api/v1/namespaces/default/pods
{
  "kind": "Status",
  "status": "Failure",
  "message": "pods is forbidden: User \"system:anonymous\" cannot list resource \"pods\" ...",
  "reason": "Forbidden",
  "code": 403
}

The request is authenticated as system:anonymous (group system:unauthenticated), passes authentication cleanly, then gets blocked by authorization with 403 Forbidden. Tell the two codes apart: 401 Unauthorized means no authenticator recognized the credential; 403 Forbidden means we know who you are but you have no permission. Here it's 403 because RBAC grants nothing to anonymous. To block it right at authentication, set --anonymous-auth=false on the API server.

🧹 Cleanup

kubectl delete namespace authn-demo

This article only creates a namespace with one ServiceAccount; deleting it is clean. No API server config is touched. The commands used here are at github.com/nghiadaulau/kubernetes-from-scratch, folder 51-authentication.

Wrap-up

Every request to the API server passes three stages — authentication, authorization, admission — and this article examined the first, where the API server attaches a username + groups to the request. The self-built cluster accepts three kinds of identity: client certificate (CN becomes username, O becomes group — admin/system:masters, configured via --client-ca-file from Article 4), ServiceAccount token (a JWT with sub/aud/exp, username system:serviceaccount:ns:name, via --service-account-key-file from Article 5), and anonymous request (system:anonymous, passes authentication but gets blocked 403 by authorization). The core point: authentication only determines who you are, it grants no permission — a certificate or token by itself opens nothing, permission comes from a binding at the next stage. Tell 401 (credential not recognized) apart from 403 (we know who you are but you lack permission).

The next stage answers the remaining question: now that we know who you are, what may you do? Article 52 digs into RBAC — Role, ClusterRole, and the bindings that turn a username/group into concrete permissions on each resource.