click the screen · press Enter
← back to blog
Kubernetes Journey · Topic 6

Kubernetes Authentication: Static Credential Files Are a Trap

Kubernetes security primitives, authentication and service accounts - Kubernetes Journey

Every command you run against a cluster goes through one door: the API server. So the first real question in Kubernetes security isn't "how do I stop attackers", it's simpler and sharper. Who are you, and what are you allowed to do? Topic 6 is about the first half of that, proving who you are, and the messy, genuinely-changed world of service accounts underneath it.

This is the groundwork the rest of cluster security stands on. If the front door is weak, nothing behind it matters.

The security primitives, from the outside in

Before the auth detail, it helps to zoom out. When I think about locking a cluster down I picture a few layers, each protecting the one inside it.

First, the hosts themselves. The nodes running your control plane and workloads are ordinary machines, and they deserve ordinary server hygiene: no root login, no password SSH, keys only, and whatever else you'd normally do to a box you care about. This sounds obvious, but it's the layer people skip because it feels like "not Kubernetes". If someone owns the host, they own everything scheduled on it. The cluster's security can't be better than the machine it runs on.

Then the API server, which is the part I care about most here, because it's the entry point for every human and every bot. Two gates sit in front of it: authentication (are you who you say you are) and authorisation (are you allowed to do this). Get past the first and you still have to clear the second.

Under the hood, the components talk to each other constantly. etcd, the controller manager, the scheduler, the API server, and out on the nodes the kubelet and kube-proxy. All of that chatter is wrapped in TLS, so it's encrypted and mutually authenticated with certificates. I'll come back to certificates properly in a later post, they deserve their own.

And finally, inside the cluster, pods can talk to each other freely by default. That "by default" is doing a lot of work. Network policies are how you carve that open network into something with walls, deciding which pods may reach which. I'll dig into those separately too.

Authentication asks "who are you?". Authorisation asks "what can you do?". Two different questions, two different gates, and you need both.
One door, two gates
REQUEST
kubectl / app

Hits the API server

GATE 1
Authentication

Who are you?

GATE 2
Authorisation

Are you allowed?

THEN
Admission & etcd

Validated, then stored

Every request goes through one door. Authentication proves who you are; authorisation decides what you can do. You have to clear both.

Who actually logs in

Here's the bit that surprised me when it clicked: Kubernetes doesn't manage human users. There's no table of accounts you can list, no kubectl create user. For people, it leans entirely on something outside itself, a file of credentials, client certificates, or an identity provider like LDAP or Kerberos.

What it does manage natively is service accounts, the identities that apps and automation use to talk to the API. So the mental split is: humans (admins, developers) come from an external source, and machines (a monitoring agent, a CI job, a dashboard app) get first-class service accounts inside the cluster. Same API server checking both, different origins.

Humans

Admins and developers. Identity comes from OUTSIDE the cluster: a credentials file, client certificates, or an IdP (LDAP, OIDC, Kerberos). There is no kubectl create user.

Service accounts

Apps and automation. First-class identities Kubernetes manages ITSELF, and mounts into pods so code can talk to the API.

Same API server checks both, but they come from different places: humans from an external source, machines from native service accounts.

The API server supports a spread of ways to verify a human request: static credential files, tokens, certificates, and third-party identity protocols. Let's start with the two simplest, mostly because they're the clearest way to see how the mechanism works, not because you should run them.

Static password and token files (the training-wheels method)

The most basic option is a plain CSV of credentials that you hand to the API server. Each row is a password, a username and a user ID, with an optional group in a fourth column.

password123,user1,u0001,group1
password123,user2,u0002,group1
password123,user3,u0003,group2

You then point the API server at it with a flag:

kube-apiserver --basic-auth-file=user-details.csv

On a kubeadm cluster you don't edit a service unit, you edit the API server's static pod manifest (under /etc/kubernetes/manifests/) and add the flag to its command, and kubeadm restarts the API server for you:

spec:
  containers:
  - name: kube-apiserver
    command:
      - kube-apiserver
      - --authorization-mode=Node,RBAC
      - --basic-auth-file=user-details.csv
      # ...other flags...

The token version is the same idea, you just swap the password column for a long random token:

KpjCVbI7cFAHYPkByTIzRb7gulcUc4B,user10,u0010,group1
rJjncHmvtXHc6MlWQddhtvNyvhgTdXSC,user11,u0011,group1
kube-apiserver --token-auth-file=user-token-details.csv

and then you present that token as a bearer header on each request:

curl -v -k https://master-node-ip:6443/api/v1/pods \
  --header "Authorization: Bearer KpjCVbI7cFAHYPkByTIzRb7gulcUc4B"
Don't run this in anger Passwords and tokens sitting in a plaintext file on the control plane is about as fragile as auth gets. One file read and it's game over, and nothing rotates. Worth knowing: the static password file (--basic-auth-file) was actually removed from Kubernetes back in 1.19, so on any modern cluster it isn't even an option. Treat this section as a mental model, then reach for client certificates or a real identity provider for anything you'd actually deploy.

A quick word on authorisation

Clearing authentication only gets you through the first gate. What you can then do is decided by an authorisation mode, and Kubernetes has a few. RBAC (role-based access control) is the one you'll meet most, mapping users and groups to specific permissions. There's also ABAC (attribute-based), Node authorisation (which scopes what a kubelet can touch), and Webhook mode (which hands the decision to an external service). Most clusters run Node,RBAC, which you'll spot in the API server flags above. I'll give RBAC its own write-up, it's worth the space.

Service accounts: identity for machines

Now the part I find genuinely useful day to day. Say you build a small dashboard app that lists pods by hitting the Kubernetes API. It needs to authenticate somehow, and you don't want to bake a human's credentials into it. That's exactly what a service account is for.

Creating one is a one-liner:

kubectl create serviceaccount dashboard-sa
kubectl get serviceaccount

On older clusters, creating the service account also minted a token and stashed it in a Secret. You could describe the account, find its token secret, and pull the token out to use as a bearer credential, same header trick as before:

kubectl describe serviceaccount dashboard-sa
kubectl describe secret dashboard-sa-token-kbbdm

The convenient part is what happens when your app actually runs inside the cluster. Kubernetes mounts the service account token straight into the pod, so the code can read it off disk without you wiring anything up.

The default service account, and turning it off

Every namespace ships with a default service account, and if you don't name one, your pod gets it automatically. The token lands at a predictable path inside the container:

kubectl exec -it my-app -- ls /var/run/secrets/kubernetes.io/serviceaccount
# ca.crt  namespace  token

That auto-mount is convenient and also a little scary, because a pod that never needed API access is still carrying a credential an attacker would love to find. If your workload doesn't talk to the API, switch it off:

apiVersion: v1
kind: Pod
metadata:
  name: my-app
spec:
  automountServiceAccountToken: false
  containers:
    - name: my-app
      image: my-app

And when you do want a specific identity, name it in the spec. Heads up: you can't swap the service account on a running pod, you delete and recreate it (a Deployment handles that with a rollout):

spec:
  serviceAccountName: dashboard-sa
  containers:
    - name: my-app
      image: my-app

The token story changed: 1.22 and 1.24

This is the bit I'd actually flag in a review, because the behaviour depends heavily on your cluster version, and the old default was quietly dangerous.

Before 1.22, every service account got a Secret holding a token that never expired. A non-expiring bearer credential, sitting in etcd, is exactly the kind of thing that leaks once and haunts you forever. So the project fixed it.

1.22 brought the TokenRequest API (KEP-1205) and "bound" tokens. Instead of a forever-token, the token a pod gets is audience-bound, time-bound and object-bound, tied to a specific use, a lifetime, and the pod itself. In a pod spec that shows up as a projected volume with an actual expiry on it:

volumes:
  - name: kube-api-access
    projected:
      sources:
        - serviceAccountToken:
            expirationSeconds: 3607
            path: token
        - configMap:
            name: kube-root-ca.crt
        - downwardAPI:
            items:
              - fieldRef:
                  fieldPath: metadata.namespace

1.24 went further: creating a service account no longer auto-generates a Secret with a long-lived token. If you want a token now, you ask for one explicitly, and it comes with an expiry (an hour by default):

kubectl create token dashboard-sa

That gives you a signed JWT you can decode (JWT.io, or a bit of jq) to see its subject, audience and expiry. You can still force the old behaviour by hand-creating a Secret of type kubernetes.io/service-account-token with the right annotation, but unless you have a very specific reason, don't. A non-expiring token is a liability you're choosing to keep.

How the service-account token changed
PRE-1.22
Forever token

Non-expiring Secret in etcd

1.22
Bound tokens

Audience, time and object-bound

1.24
On demand

No auto Secret; request with expiry

The token behaviour depends heavily on your cluster version. The trend: from a non-expiring credential sitting in etcd to short-lived, request-on-demand tokens.
The takeaway If you inherit a cluster, check its version and check for stray long-lived service account token secrets. On 1.24+ prefer kubectl create token and short lifetimes. On anything you own, set automountServiceAccountToken: false for workloads that don't need the API, so there's simply no token to steal.

What stuck after writing it

What stuck with me writing this up is how much of Kubernetes auth is really just "identity from somewhere else, checked at one door". Humans come from a file or an IdP, machines get service accounts, and both hit the same API server. The interesting security work is in the details: not leaving plaintext credentials around, not shipping pods with tokens they don't need, and knowing which version's token behaviour you're actually running. I'm still early on certificates and RBAC, so if I've oversimplified the bound-token stuff, tell me, I'd rather be corrected than confidently wrong.

Next in the series I want to take certificates apart properly, since that TLS mesh between components is the thing everything else trusts. If this helped, come say hi on LinkedIn or the contact page, and tell me whether you'd want RBAC or certificates broken down first.

Further reading

FAQ

How does Kubernetes authenticate a request?

The API server checks who you are before it checks what you can do. It supports certificates, tokens and external identity, and if none prove your identity the request is rejected as anonymous.

What is a Kubernetes service account?

It is an identity for a pod, not a human. Pods use a service account token to talk to the API server. Treating those tokens as secrets matters, because a leaked one is a foothold for an attacker.

What changed with service account tokens in 1.22 and 1.24?

Tokens moved to short-lived, audience-bound tokens instead of long-lived secrets that never expired. It is a real security win, but it caught out anyone relying on the old always-mounted token behaviour.