Skip to main content
6 min read·1,115 words

Service Account Token Integration for Image Pulls

Required knowledge for the CKS certification.

Container registry credentials stored in imagePullSecrets are high-value targets: a single compromised Secret exposes every registry it authenticates to, and those credentials often live indefinitely. Starting with Kubernetes v1.33 (alpha) and graduating to Beta (enabled by default) in v1.34, the kubelet credential provider mechanism gained the ability to pass a pod-bound service account token to a credential provider plugin, which the plugin can exchange for short-lived registry credentials — eliminating the need for static, long-lived imagePullSecrets entirely. The feature is on track to graduate to GA in v1.38.

This article explains the mechanism, shows the complete configuration, and covers how to verify that image pulls are using the new identity path.


How It Works​

The kubelet credential provider is a plugin system (stable since Kubernetes v1.26) in which the kubelet calls an out-of-process binary to obtain registry credentials for each image pull. The binary communicates with the kubelet through stdin and stdout using versioned Kubernetes API objects.

The service account token integration extends this plugin contract. When tokenAttributes is configured in the plugin's CredentialProviderConfig entry, the kubelet performs the following steps for each image pull:

  1. Identifies the matching credential provider plugin by comparing the image reference against matchImages.
  2. Requests a service account token bound to the pod performing the pull, scoped to the audience specified in tokenAttributes.serviceAccountTokenAudience.
  3. Sends a CredentialProviderRequest to the plugin via stdin; this request includes the bound token in the serviceAccountToken field and the pod's service account annotations in serviceAccountAnnotations.
  4. The plugin exchanges the token with an external identity or registry service (cloud IAM, an OIDC-aware registry, or a custom token exchange endpoint).
  5. The plugin returns a CredentialProviderResponse with credentials; the kubelet uses those credentials to pull the image.

The credential is never stored on disk or in the Kubernetes API. Each pull uses a fresh token bound to that specific pod.


Prerequisites​

  • Kubernetes v1.34 or later (the feature is Beta and enabled by default; GA is targeted for v1.38).
  • A credential provider plugin binary installed on every node that:
    • Accepts serviceAccountToken and serviceAccountAnnotations from a CredentialProviderRequest.
    • Knows how to exchange that token for valid registry credentials.
  • The registry (or an intermediate auth service) must accept the pod's service account token as proof of identity — for example via OIDC federation.

What is not covered by this feature: The feature gate controls the kubelet side only. The plugin binary itself, and the registry-side token validation, are outside Kubernetes core. This means the feature is available in any Kubernetes v1.34+ cluster (Beta, enabled by default) but is only usable when a compatible plugin is deployed.


Version Compatibility​

Kubernetes versionStageFeature gate
v1.33AlphaKubeletServiceAccountTokenForCredentialProviders=true (off by default)
v1.34BetaEnabled by default
v1.38 (planned)StableGate locked to enabled; can no longer be disabled

On v1.33, you must explicitly enable the feature gate on the kubelet. On v1.34 and later, the gate is enabled by default (no explicit setting required). When the feature graduates to GA in v1.38, the gate is locked to enabled and can no longer be disabled.


1. Install the Credential Provider Plugin​

Place the credential provider plugin binary in a directory on every node. The path must be the same on all nodes, and the binary must be executable.

# Example: install to /usr/local/bin
sudo install -m 0755 your-credential-provider /usr/local/bin/your-credential-provider

2. Configure the Kubelet​

Set two kubelet flags pointing to the configuration file and the directory containing plugin binaries:

--image-credential-provider-config=/etc/kubernetes/credential-providers/config.yaml
--image-credential-provider-bin-dir=/usr/local/bin

On nodes managed with KubeletConfiguration, add the equivalent fields:

apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
imageCredentialProviderConfigFile: /etc/kubernetes/credential-providers/config.yaml
imageCredentialProviderBinDir: /usr/local/bin

3. Create the CredentialProviderConfig​

The CredentialProviderConfig file lists the plugins and their matching image patterns. Add tokenAttributes to the plugin entry to enable service account token passing:

apiVersion: kubelet.config.k8s.io/v1
kind: CredentialProviderConfig
providers:
- name: your-credential-provider
matchImages:
- "registry.example.com"
- "*.registry.example.com"
defaultCacheDuration: "10m"
apiVersion: credentialprovider.kubelet.k8s.io/v1
tokenAttributes:
serviceAccountTokenAudience: "registry.example.com"
cacheType: "Token"

tokenAttributes fields:

  • serviceAccountTokenAudience (required): The audience claim embedded in the bound token. The registry or auth service validates this audience when the plugin presents the token.
  • cacheType: Controls how the kubelet caches credentials returned by the plugin. "Token" caches on a per-token basis — credentials are considered valid for the lifetime of the specific service account token. Use "Token" when the plugin's returned credential lifetime is tied to the service account token's lifetime.

4. What the Plugin Receives​

The kubelet sends a CredentialProviderRequest to the plugin's stdin. When tokenAttributes is configured, the request includes:

{
"apiVersion": "credentialprovider.kubelet.k8s.io/v1",
"kind": "CredentialProviderRequest",
"image": "registry.example.com/myapp:v1.2.3",
"serviceAccountToken": "<pod-bound JWT>",
"serviceAccountAnnotations": {
"example.com/role-arn": "arn:aws:iam::123456789012:role/RegistryPullRole"
}
}

The plugin reads the token from serviceAccountToken, exchanges it with the appropriate service, and writes a CredentialProviderResponse to stdout:

{
"apiVersion": "credentialprovider.kubelet.k8s.io/v1",
"kind": "CredentialProviderResponse",
"cacheKeyType": "Token",
"auth": {
"registry.example.com": {
"username": "oauth2accesstoken",
"password": "<short-lived-token-from-exchange>"
}
}
}

5. Deploy Pods Without imagePullSecrets​

Once the plugin is installed and the kubelet is configured, pods pulling from the matching registry do not need an imagePullSecrets field:

Before: Pod spec requires a static credential Secret.

apiVersion: v1
kind: Pod
metadata:
name: myapp
spec:
imagePullSecrets:
- name: registry-credentials
containers:
- name: myapp
image: registry.example.com/myapp:v1.2.3

After: The kubelet obtains credentials dynamically via the credential provider plugin using the pod's own service account token.

apiVersion: v1
kind: Pod
metadata:
name: myapp
spec:
serviceAccountName: myapp-sa
containers:
- name: myapp
image: registry.example.com/myapp:v1.2.3

The pod's service account (myapp-sa) must be federated with the identity provider that the registry trusts. The setup of that federation (IRSA on EKS, Workload Identity on GKE, or a custom OIDC provider) is outside the scope of this article and depends on your registry.


6. Verify the Configuration​

After configuring the kubelet and deploying a pod, verify that the image pull succeeds without an imagePullSecrets entry:

# Confirm the pod started without imagePullSecrets
kubectl get pod myapp -o jsonpath='{.spec.imagePullSecrets}'
# Expected: empty output (no imagePullSecrets)

# Confirm the pod is Running
kubectl get pod myapp
# Expected: STATUS Running

Check kubelet logs on the node to confirm the credential provider was invoked:

# On the node — kubelet logs show credential provider calls
journalctl -u kubelet --since "5 minutes ago" | grep -i "credential.*provider\|image.*pull"

Confirm that no imagePullSecrets-holding Secrets exist for this registry in the namespace:

# Audit: list Secrets of type kubernetes.io/dockerconfigjson
kubectl get secrets -n myapp-namespace \
--field-selector type=kubernetes.io/dockerconfigjson \
-o custom-columns='NAME:.metadata.name,NAMESPACE:.metadata.namespace'

Security Properties​

PropertyTraditional imagePullSecretsSA token credential provider
Credential lifetimeLong-lived until rotated manuallyShort-lived; expires with the SA token
Credential storageStored in a Kubernetes Secret (etcd)Never stored; obtained dynamically
Blast radius if stolenAll workloads using that SecretThe specific pod; token is pod-bound
RBAC exposureAny principal with get secrets accessNo Secret to read; no RBAC pivot
Registry-side revocationRequires credential rotation in SecretToken expiry or SA deletion is sufficient
Audit trailSecret access eventsToken issuance + exchange events

The pod-bound token has a limited audience (serviceAccountTokenAudience) and expires automatically. A compromised node cannot reuse the token for a different pod or a different audience.


Operational Notes​

SA token audience must match the registry's expected audience. If the registry validates the aud claim and it does not match serviceAccountTokenAudience, the exchange will fail with an authorization error. Verify the audience configuration with your registry documentation.

Clusters on managed Kubernetes (EKS, GKE, AKS) may lag upstream by one or more minor versions. Confirm that your managed cluster version is v1.34 or later before relying on this feature. On v1.34 and later the gate is enabled by default; on v1.33 it must be explicitly enabled on the kubelet.

The feature covers the kubelet → plugin communication only. Network policies, admission controllers, and registry-side access controls remain separate concerns and are not replaced by this mechanism.


References​

This article is based on information from the following official sources:

  1. Configure a kubelet image credential provider - Kubernetes Documentation
  2. Kubelet CredentialProvider (v1) - Kubernetes API Reference
  3. From Secrets to Service Accounts: Kubernetes Image Pulls Evolved - Kubernetes Blog (v1.33 alpha)
  4. Service Account Token Integration for Image Pulls Graduates to Beta - Kubernetes Blog (v1.34 beta)