Many clusters are still reached with a kubeconfig file that was generated once, grants cluster-admin and has been copied between laptops ever since. The API server sees the same client certificate whoever uses it, so the audit log cannot say who deleted the deployment, and removing one person's access means reissuing the file for everyone. Kubernetes has the machinery to do better. It needs to be connected to an identity provider.

How Kubernetes thinks about identity

Kubernetes has no user database. There is no User object to create. The API server trusts an authenticator to tell it two things about each request: a username and a list of groups. Everything after that is authorisation.

Three kinds of caller exist:

  • Users, which are just names asserted by an authenticator: a client certificate's common name, or a claim in a token.
  • Groups, also asserted by the authenticator.
  • Service accounts, the one identity type that Kubernetes manages itself, intended for workloads.

This design means Kubernetes will trust whatever identity source you configure. The work is choosing one that knows who your people are.

OIDC to the API server

The API server can validate OpenID Connect ID tokens directly. You tell it which issuer to trust and which claims carry the username and groups.

kube-apiserver \
  --oidc-issuer-url=https://id.example.com \
  --oidc-client-id=kubernetes \
  --oidc-username-claim=email \
  --oidc-groups-claim=groups \
  --oidc-username-prefix=oidc: \
  --oidc-groups-prefix=oidc:

The flow for a person is:

  1. kubectl obtains an ID token from the identity provider, usually through a credential plugin that opens a browser for sign-in.
  2. kubectl sends the token as a bearer token with each request.
  3. The API server checks the signature against the issuer's published keys, checks the audience and expiry, and reads the username and groups from the claims.

The API server never calls the identity provider per request. It verifies the token on its own, which is why short token lifetimes matter: a token that has been issued stays valid until it expires.

When you cannot set the flags

On some managed Kubernetes services you do not control the API server's flags, and the provider offers its own identity integration instead. The portable alternative is an impersonating proxy. The proxy authenticates the person with OIDC, then forwards the request to the API server using its own credential and the Impersonate-User and Impersonate-Group headers. The API server applies RBAC as if the named user had called directly.

The trade-off is that the proxy's credential must hold the impersonate verb, which makes it a highly privileged component. In exchange it works on any conformant cluster, and it is a natural place to record sessions.

RBAC subjects

Once the API server has a username and groups, RBAC decides what they may do. A Role or ClusterRole lists rules. A RoleBinding or ClusterRoleBinding attaches a role to subjects.

apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: payments-developers
  namespace: payments
subjects:
  - kind: Group
    name: "oidc:payments-dev"
    apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: ClusterRole
  name: edit
  apiGroup: rbac.authorization.k8s.io

Three habits keep this manageable:

  • Bind groups, not users. The group name comes from the identity provider. Add a person to payments-dev there, and they gain the binding with no change in the cluster. Remove them, and their next token no longer carries the group.
  • Prefer namespaced bindings. A RoleBinding that references a ClusterRole grants that role's rules in one namespace only. Reserve ClusterRoleBinding for people who truly operate the whole cluster.
  • Use a prefix. The oidc: prefix prevents a group from your provider colliding with a built-in group such as system:masters.

See also RBAC done properly.

Short-lived kubeconfig credentials

A kubeconfig does not have to contain a credential. It can contain an instruction for obtaining one.

users:
  - name: alice
    user:
      exec:
        apiVersion: client.authentication.k8s.io/v1
        command: my-oidc-helper
        args: ["get-token", "--issuer=https://id.example.com"]
        interactiveMode: IfAvailable

With an exec credential plugin, kubectl runs the helper when it needs a token. The helper signs the person in, caches a short-lived token and refreshes it when it expires. The kubeconfig file itself holds no secret and is safe to share or commit as a template.

This changes offboarding in the same way short-lived SSH certificates do for servers. There is no file to recall. A person who can no longer sign in to the identity provider cannot obtain a new token, and the old one expires within minutes.

Workloads

The same idea applies to pods. Modern Kubernetes issues bound service account tokens through the TokenRequest API. They are projected into the pod, tied to its lifetime, scoped to an audience and rotated automatically. Two rules follow:

  • Give each workload its own service account. The default account in a namespace should have no bindings.
  • Do not create long-lived service account token secrets for use outside the cluster.

Seeing who can reach what

Configuration is half the job. The other half is being able to answer questions about it.

kubectl answers the forward question well:

kubectl auth can-i delete secrets -n payments --as alice@example.com --as-group oidc:payments-dev

The reverse question, "who can delete secrets in this namespace", has no built-in command. Answering it means reading every binding, resolving each to its role and checking the rules. It is worth doing regularly, with attention to a short list of grants that amount to more than they appear:

  • Wildcards. A * in verbs or resources, which silently includes every resource added later.
  • Read access to secrets. get or list on secrets in a namespace is access to every credential stored there.
  • Escalation verbs. escalate and bind on roles, and impersonate on users or groups, each allow a subject to gain rights it was not given.
  • pods/exec. The ability to run commands inside a pod is access to whatever that pod can reach.

Finally, turn on API server audit logging. With OIDC in place, each audit event carries the person's username, which is the point of the whole exercise.

How AuthFI does it

On the AuthFI site, Kubernetes access for people is listed as in development. The planned design is the impersonating proxy described above, so that AuthFI groups arrive as Kubernetes groups and a customer's existing RoleBindings keep working unchanged. It is not shipped.

What the console does today is read. Under Zero-Code, a connected cluster has an RBAC tab that shows subjects, the bindings that reach each one, the roles those bindings grant and the rules inside them, as a tree. Risky grants are named in words: a wildcard, a subject that reads every secret, a subject that can escalate. A "Who can" question takes a verb, a resource and a namespace and lists the subjects that match. The view says so when a cluster holds more bindings or roles than were read, and it does not evaluate aggregated roles or admission webhooks.

The same area lists a cluster's workloads and lets each be classified and protected through AuthFI's Zero-Code enforcement, which is a separate control from kubectl access: it governs what reaches a running service, not who may call the API server. The background is in what is zero-code authentication.

Key takeaways

  • Kubernetes stores no users. It trusts an authenticator for a username and groups, so connect it to your identity provider.
  • Use OIDC at the API server where you can, and an impersonating proxy where you cannot set its flags.
  • Bind RBAC roles to groups from your provider, in a namespace, with a prefix.
  • Replace static kubeconfig credentials with an exec plugin and short-lived tokens, and use bound tokens for workloads.
  • Regularly ask the reverse question, who can do this, and look for wildcards, secret readers and escalation verbs.