KubeDojo

Authentication: Users, Groups, and Service Accounts

AK
by Alexis Kinsella··15 min read
Authentication: Users, Groups, and Service Accounts

You can configure seven different authentication methods on a single Kubernetes API server, and a credential valid through any one of them grants access. Most production clusters run at least three simultaneously: X.509 client certificates for system components, service account tokens for workloads, and something else for humans. Do you know which ones yours is running?

Authentication is the first of three gates every API request passes through: authentication, then authorization, then admission control. If authentication succeeds, the request carries an identity into the authorization layer. If it fails, the request is rejected with a 401. The KCSA exam covers this within the Security Fundamentals domain (22% weight). The questions expect you to know not just what each mechanism does, but when it's appropriate and what can go wrong.

We'll work through the Kubernetes identity model, each authentication mechanism as it appears in the API server source code, and the security trade-offs that matter when you're choosing between them.

The Kubernetes Identity Model

Kubernetes distinguishes between two categories of identity: human users and service accounts.

Human users are not stored anywhere in the cluster. Kubernetes has no user database, no user creation API, no password management. It relies entirely on external systems to assert who a user is. This has a critical operational implication: to audit user access, you must query every configured authentication provider, not the cluster itself.

Service accounts, by contrast, are first-class Kubernetes objects. They're namespaced (ServiceAccount resources in a specific namespace), managed by the API, and designed for workloads running inside the cluster. Every namespace gets a default service account automatically.

The naming convention follows a system: prefix pattern that you'll see throughout Kubernetes:

  • system:anonymous / system:unauthenticated for requests that don't present credentials
  • system:authenticated group for any successfully authenticated request
  • system:serviceaccount:namespace:name for service accounts
  • system:serviceaccounts and system:serviceaccounts:namespace as groups for service account RBAC
  • system:masters is a special group that bypasses authorization entirely

The API server's X.509 authenticator extracts identity directly from certificate fields. In the source code at staging/src/k8s.io/apiserver/pkg/authentication/request/x509/x509.go:

// x509.go (CommonNameUserConversion)
var CommonNameUserConversion = UserConversionFunc(func(chain []*x509.Certificate) (*authenticator.Response, bool, error) {
    if len(chain[0].Subject.CommonName) == 0 {
        return nil, false, nil
    }
    fp := sha256.Sum256(chain[0].Raw)
    id := "X509SHA256=" + hex.EncodeToString(fp[:])
    // ... UID parsing omitted
    return &authenticator.Response{
        User: &user.DefaultInfo{
            Name:   chain[0].Subject.CommonName,
            Groups: chain[0].Subject.Organization,
            Extra: map[string][]string{
                user.CredentialIDKey: {id},
            },
        },
    }, true, nil
})

The CommonName field becomes the username, the Organization field becomes the list of groups, and a SHA256 fingerprint of the certificate is stored as a credential ID for audit tracking. This is why, when kubeadm generates a kubelet certificate, it sets CN=system:node:node-name and O=system:nodes.

Internal Authentication Mechanisms

The API server supports multiple built-in authenticators, all defined in pkg/kubeapiserver/options/authentication.go. The BuiltInAuthenticationOptions struct shows the full list:

// authentication.go (BuiltInAuthenticationOptions)
type BuiltInAuthenticationOptions struct {
    APIAudiences    []string
    Anonymous       *AnonymousAuthenticationOptions
    BootstrapToken  *BootstrapTokenAuthenticationOptions
    ClientCert      *genericoptions.ClientCertAuthenticationOptions
    OIDC            *OIDCAuthenticationOptions
    RequestHeader   *genericoptions.RequestHeaderAuthenticationOptions
    ServiceAccounts *ServiceAccountAuthenticationOptions
    TokenFile       *TokenFileAuthenticationOptions
    WebHook         *WebHookAuthenticationOptions
    AuthenticationConfigFile string
    // ... cache TTL fields omitted
}

Each field corresponds to a pluggable authenticator. The API server initializes all enabled authenticators and chains them: each request is tried against every authenticator until one succeeds or all fail.

Kubernetes API request authentication flow showing the authenticator chain, first-match-wins logic, and identity extraction

X.509 Client Certificates

When the API server starts with --client-ca-file, it enables X.509 client certificate authentication. Any client presenting a certificate signed by that CA is authenticated with the identity encoded in the certificate.

This works well for system components. The kubelet, controller manager, and scheduler all use client certificates to talk to the API server. But for human users, X.509 certificates have serious limitations:

  • No revocation: Kubernetes has no mechanism to revoke individual certificates. If a certificate is compromised, your only option is rotating the entire cluster CA, which disrupts all components that depend on it. There's an open issue for this that's been open since 2016.
  • Immutable groups: Group memberships are embedded in the certificate's O field at creation time and cannot be changed until the certificate expires.
  • No audit trail: The cluster doesn't track which certificates have been issued. Multiple certificates can exist for the same user with no central record.

warning: The CSR API (CertificateSigningRequest) lets anyone with create and approve permissions mint new client certificates with arbitrary usernames and groups. The system:masters group is blocked, but other credentials can last as long as the CA lifetime. On GKE, the default CSR-issued certificate lifetime is 5 years. This is a known persistence vector for attackers.

Service Account Tokens

Service account tokens are the only identity type where Kubernetes manages both the account and the credential.

Legacy tokens (pre-1.22) were stored as Secrets. They never expired and were visible to anyone who could read secrets in the namespace. Clusters upgraded from older Kubernetes versions may still have these legacy secrets.

Bound tokens (1.22+) use the TokenRequest API. The kubelet requests tokens from the API server, and these tokens are bound to specific pods. When the pod is deleted, the token is invalidated. The kubelet's token manager (pkg/kubelet/token/token_manager.go) handles caching and refresh:

// token_manager.go (requiresRefresh — nil-check on ExpirationSeconds omitted)
func (m *Manager) requiresRefresh(ctx context.Context, tr *authenticationv1.TokenRequest) bool {
    // ...
    now := m.clock.Now()
    exp := tr.Status.ExpirationTimestamp.Time
    iat := exp.Add(-1 * time.Duration(*tr.Spec.ExpirationSeconds) * time.Second)

    jitter := time.Duration(rand.Float64()*maxJitter.Seconds()) * time.Second
    if now.After(iat.Add(maxTTL - jitter)) {
        return true
    }
    // Require a refresh if within 20% of the TTL plus a jitter from the expiration time.
    if now.After(exp.Add(-1*time.Duration((*tr.Spec.ExpirationSeconds*20)/100)*time.Second - jitter)) {
        return true
    }
    return false
}

The token is refreshed when it reaches 80% of its TTL or 24 hours, whichever comes first. The default token lifetime in kubeadm is 1 hour.

A bound service account token is a JWT. When decoded, the key fields look like this (real tokens also include iat, nbf, jti, and a node binding on clusters running 1.32+):

{
  "aud": ["https://kubernetes.default.svc.cluster.local"],
  "exp": 1729605240,
  "iss": "https://my-cluster.example.com",
  "kubernetes.io": {
    "namespace": "default",
    "pod": {
      "name": "my-pod",
      "uid": "5e0bd49b-f040-43b0-99b7-22765a53f7f3"
    },
    "serviceaccount": {
      "name": "my-sa",
      "uid": "14ee3fa4-a7e2-420f-9f9a-dbc4507c3798"
    }
  },
  "sub": "system:serviceaccount:default:my-sa"
}

The token encodes the namespace, service account, bound pod, and audience. The API server validates the signature, checks that the referenced pod still exists, and verifies the audience matches.

Starting from Kubernetes 1.29, auto-generated legacy tokens are marked invalid after one year of non-use and eventually purged. If you're running a cluster that was originally set up on 1.21 or earlier, check for lingering legacy token secrets.

Bootstrap Tokens

Bootstrap tokens exist for one purpose: joining new nodes to the cluster during TLS bootstrapping. They are {token-id}.{token-secret} pairs stored in kube-system secrets of type bootstrap.kubernetes.io/token.

The authenticator (plugin/pkg/auth/authenticator/token/bootstrap/bootstrap.go) maps them to the username system:bootstrap:{token-id} in the system:bootstrappers group:

// bootstrap.go (AuthenticateToken)
return &authenticator.Response{
    User: &user.DefaultInfo{
        Name:   bootstrapapi.BootstrapUserPrefix + string(id),
        Groups: groups,
    },
}, true, nil

Bootstrap tokens have hard-coded group memberships, no lockout mechanism, and are not designed for user authentication. Treat them as cluster setup artifacts that should be cleaned up after bootstrapping.

Static Token File

The --token-auth-file flag lets the API server read credentials from a CSV file on disk. Don't use this in production. Credentials are stored in cleartext, changes require an API server restart, there's no rotation mechanism, and it's not available on managed Kubernetes services.

External Authentication Mechanisms

Kubernetes authentication mechanisms comparing internal and external strategies

For human users in production, external authentication is the recommended path.

OpenID Connect (OIDC)

OIDC is the standard recommendation for authenticating humans to Kubernetes clusters. The API server validates JWT tokens from an external identity provider and extracts identity from the token claims.

The key API server flags configure the OIDC authenticator:

# kube-apiserver OIDC flags
--oidc-issuer-url=https://accounts.example.com
--oidc-client-id=kubernetes
--oidc-username-claim=email          # default: "sub"
--oidc-groups-claim=groups
--oidc-ca-file=/etc/kubernetes/oidc-ca.pem

Newer Kubernetes versions support a --authentication-config file that allows multiple JWT authenticators with CEL-based claim mappings and automatic reload, replacing the per-flag approach. This is mutually exclusive with the --oidc-* flags.

In practice, most clusters use an OIDC bridge like Dex or Keycloak to connect Kubernetes to corporate identity providers (Active Directory, Okta, Google Workspace). Tools like kubelogin handle the browser-based OIDC flow and inject tokens into kubeconfig.

tip: OIDC requires modifying API server startup flags, which means it's often not available on managed Kubernetes services by default. EKS and GKE support OIDC identity providers, but AKS relies on Entra ID integration instead.

Webhook Token Authentication

With webhook authentication, the API server sends a TokenReview request to an external URL for every authentication attempt. The external service responds with an allow/deny decision.

The webhook authenticator caches responses (default 2-minute TTL) and supports retry backoff:

// authentication.go (WebHookAuthenticationOptions)
type WebHookAuthenticationOptions struct {
    ConfigFile   string
    Version      string
    CacheTTL     time.Duration
    RetryBackoff *wait.Backoff
}

The default cache TTL is 2 minutes, set in WithWebHook(). AWS uses this mechanism for EKS: the aws-iam-authenticator webhook validates AWS IAM identities and maps them to Kubernetes users and groups. The webhook approach gives cloud providers flexibility without requiring OIDC support on the API server itself.

Authenticating Proxy

An authenticating proxy sits in front of the API server, authenticates the user using its own logic, and passes identity via HTTP headers (X-Remote-User, X-Remote-Group, X-Remote-Extra-).

The proxy must authenticate to the API server via a client certificate from a separate CA (--requestheader-client-ca-file). The source code explicitly validates that the request header CA and the client CA don't overlap unless --requestheader-allowed-names is set, preventing regular client certificates from spoofing proxy headers.

Choosing an External Mechanism

Mechanism Best for Requires control plane access? Managed K8s support
OIDC Direct IdP integration, corporate SSO Yes (API server flags) EKS, GKE; AKS uses Entra ID
Webhook Cloud IAM integration, custom auth backends Yes (config file on disk) Used internally by EKS
Authenticating proxy Legacy SSO, custom auth logic Yes (separate CA setup) Rare on managed K8s

If your cluster runs on a managed service, the provider likely made this choice for you. EKS uses webhook authentication under the hood (via aws-iam-authenticator). GKE supports both OIDC and Google Cloud IAM. AKS integrates with Entra ID. For self-managed clusters, OIDC is the default recommendation unless you have a specific reason to go another route.

Securing Authentication Across Cluster Components

The main API server isn't the only component that requires authentication. Every Kubernetes API endpoint is a potential entry point.

Component Authentication Method Security Notes
API server All configured authenticators (chained) First successful match wins
kubelet API Webhook (TokenRequest) or X.509 Anonymous access disabled by most distributions
etcd Client certificates (dedicated CA) Any valid cert gets full database access
Controller manager Service account token Via TokenRequest API
Scheduler Service account token Via TokenRequest API
kube-proxy None (localhost-only) Sensitive endpoints bound to loopback interface

The etcd authentication model deserves special attention. etcd uses client certificate authentication, and any certificate signed by its configured CA gets full read/write access to the entire database, including all Secrets. This is why etcd should use a dedicated CA, separate from the cluster's main CA. Most Kubernetes distributions handle this automatically, but misconfiguration can give unintended access to anyone holding a cluster client certificate.

Gotchas

  • Multiple authenticators run simultaneously. The API server tries every configured authenticator on every request. A valid credential from any one source grants access. If you migrate from X.509 to OIDC but forget to remove the old CA, those old certificates still work.
  • X.509 certificates cannot be individually revoked. The only option is rotating the entire cluster CA. Plan certificate lifetimes accordingly and prefer short-lived credentials.
  • The CSR API is a persistence vector. Anyone with create + approve permissions on CertificateSigningRequest resources can create long-lived client certificates. Monitor all CSR activity through audit logs and restrict approval permissions tightly.
  • GKE defaults to 5-year certificate lifetimes for CSR-issued certs. Always set explicit spec.expirationSeconds when creating CSRs on GKE.
  • Service account tokens work from anywhere. A token extracted from a pod's filesystem works just as well in an external kubectl session. Bound tokens are better than legacy ones, but stolen tokens are still usable until they expire or the pod is deleted.
  • Legacy service account secrets persist after upgrades. Clusters upgraded from pre-1.22 may still have non-expiring token Secrets. The cleanup controller (1.29+) handles auto-generated ones, but manually created token Secrets persist indefinitely.

Wrap-up

To audit your cluster's authentication posture: list the API server flags that enable authenticators (--client-ca-file, --oidc-issuer-url, --authentication-token-webhook-config-file, --token-auth-file, --enable-bootstrap-token-auth), verify each one is intentional, and check for legacy service account token Secrets that predate 1.22. The fewer authentication methods active on your cluster, the smaller the attack surface.

The next article, kcsa-authorization, covers what happens after authentication succeeds: how Kubernetes decides what an authenticated identity is allowed to do through RBAC, ABAC, webhook authorization, and Node authorization.

AK
Alexis Kinsella

Languages (Rust, Go & Python), container orchestration (Kubernetes), data and cloud providers (AWS & GCP) lover. Runner & Cyclist.

Subscribe to KubeDojo

Get the latest articles delivered to your inbox.