Skip to content

Instantly share code, notes, and snippets.

@saschagrunert
Last active August 29, 2025 07:48
Show Gist options
  • Save saschagrunert/0f1f89bbfd29eea7ba94eb1aa1059cdb to your computer and use it in GitHub Desktop.
Save saschagrunert/0f1f89bbfd29eea7ba94eb1aa1059cdb to your computer and use it in GitHub Desktop.
Kubernetes Credential Provider PoC

Setup the cluster to use

export FEATURE_GATES=KubeletServiceAccountTokenForCredentialProviders=true
export KUBELET_FLAGS='--image-credential-provider-bin-dir=/path/to/provider/dir --image-credential-provider-config=/path/to/credential-provider-config.yml'

Start Kubernetes, and apply the demo files

kubectl apply -f required-rbac.yml example-secret.yml pod.yml

Verify that everything works by the logs:

cat logs
Thu Aug 28 02:40:49 PM CEST 2025: Request: {
  "kind": "CredentialProviderRequest",
  "apiVersion": "credentialprovider.kubelet.k8s.io/v1",
  "image": "docker.io/library/nginx",
  "serviceAccountToken": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjFhSFBaWXYxaVBrM3Bad3VzTzE0eUx5VlpiS0NvY0szUFpyRmJpcTlJTzgifQ.eyJhdWQiOlsiaHR0cHM6Ly9rdWJlcm5ldGVzLmRlZmF1bHQuc3ZjIl0sImV4cCI6MTc1NjM4ODQ0OSwiaWF0IjoxNzU2Mzg0ODQ5LCJpc3MiOiJodHRwczovL2t1YmVybmV0ZXMuZGVmYXVsdC5zdmMiLCJqdGkiOiJlYzRlMjhhMC1lMmE0LTRlMzctOTE4OC00MTdkZWY2MWMxOGMiLCJrdWJlcm5ldGVzLmlvIjp7Im5hbWVzcGFjZSI6ImRlZmF1bHQiLCJub2RlIjp7Im5hbWUiOiIxMjcuMC4wLjEiLCJ1aWQiOiJjNDEzZWQwZC1lMGI2LTRhMmQtODMxMy0xOTc0M2M3NzRiMTEifSwicG9kIjp7Im5hbWUiOiJ0ZXN0LXBvZCIsInVpZCI6ImFjMjU2NThkLTY3NzYtNDUyNC05ODE2LWRlNjk4Njk1ZGYxZSJ9LCJzZXJ2aWNlYWNjb3VudCI6eyJuYW1lIjoiZGVmYXVsdCIsInVpZCI6IjY2ZGIzYTc5LTFkYTEtNDQ1Mi05ZDhjLWQ4NzNhYmUxMjY1ZCJ9fSwibmJmIjoxNzU2Mzg0ODQ5LCJzdWIiOiJzeXN0ZW06c2VydmljZWFjY291bnQ6ZGVmYXVsdDpkZWZhdWx0In0.LsYqWXd5opcbqc4nDWbuiEN24f1hNs8UpLRHTpGJg54Y9NM_LlMTXKt9-_EQbQ36lF6pNOt05sRBFXTmD7OV9sfqGH7cHbM0COGxx1z8KMoUWYtHo6UmrsOvaVQ0d-telMLAMW0JGKmzqAZZgbB9WK_4toVWGGTn91yYPcDJ5l7L6vUXuQkKFz50tRrXHABnFALnYuAWHtjd-Xg6r2dUEVyjmooFL5DFFmUmibY0REBhKaD6xvXOtmeJ1J-p1qbjWEuoBD9ZFzwLfnmb9qn2FG0ovgWvQb1BrU38IZ5oH0dxynIESEaln8K6aPjiRAnTvmHUqRC_0ydXq-5aYvoZew"
}
Thu Aug 28 02:40:49 PM CEST 2025: Using Service Account "default" from namespace "default"
Thu Aug 28 02:40:49 PM CEST 2025: Secret: {
  "kind": "Secret",
  "apiVersion": "v1",
  "metadata": {
    "name": "my-secret",
    "namespace": "default",
    "uid": "fffc1832-7cce-453d-92dd-cc9163407f98",
    "resourceVersion": "353",
    "creationTimestamp": "2025-08-28T12:40:32Z",
    "annotations": {
      "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"v1\",\"data\":{\"key\":\"c3VwZXJzZWNyZXQ=\"},\"kind\":\"Secret\",\"metadata\":{\"annotations\":{},\"name\":\"my-secret\",\"namespace\":\"default\"}}\n"
    },
    "managedFields": [
      {
        "manager": "kubectl-client-side-apply",
        "operation": "Update",
        "apiVersion": "v1",
        "time": "2025-08-28T12:40:32Z",
        "fieldsType": "FieldsV1",
        "fieldsV1": {
          "f:data": {
            ".": {},
            "f:key": {}
          },
          "f:metadata": {
            "f:annotations": {
              ".": {},
              "f:kubectl.kubernetes.io/last-applied-configuration": {}
            }
          },
          "f:type": {}
        }
      }
    ]
  },
  "data": {
    "key": "c3VwZXJzZWNyZXQ="
  },
  "type": "Opaque"
}
Thu Aug 28 02:40:49 PM CEST 2025: Response: {
  "kind": "CredentialProviderResponse",
  "apiVersion": "credentialprovider.kubelet.k8s.io/v1",
  "cacheKeyType": "Registry",
  "cacheDuration": "0h5m0s",
  "auth": {
    "docker.io/library/nginx": {
      "username": "",
      "password": ""
    }
  }
}
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
DIR=$(dirname "$(realpath "$0")")
LOG="$DIR/../logs"
INPUT="$(jq </dev/stdin)"
echo "$(date): Request: $INPUT" >>"$LOG"
TOKEN=$(echo "$INPUT" | jq -r .serviceAccountToken)
PAYLOAD_K8S=$(echo "$TOKEN" | jwt decode -j - | jq '.payload."kubernetes.io"')
NAMESPACE=$(echo "$PAYLOAD_K8S" | jq -r .namespace)
SA_NAME=$(echo "$PAYLOAD_K8S" | jq -r .serviceaccount.name)
echo "$(date): Using Service Account \"$SA_NAME\" from namespace \"$NAMESPACE\"" >>"$LOG"
# Test secret retrieval
# In production we should
# 1. Get all secrets of type kubernetes.io/dockerconfigjson
# 2. Match the provided image name with the resolved registry from registries.conf
# 3. Match theh resolved registry with them from the .dockerconfigjsons
# 4. Merge the auths together for the response
SECRET_NAME=my-secret
SECRET=$(curl -k -H "Authorization: Bearer $TOKEN" "https://localhost:6443/api/v1/namespaces/$NAMESPACE/secrets/$SECRET_NAME")
echo "$(date): Secret: $SECRET" >>"$LOG"
RESPONSE=$(echo '{
"kind": "CredentialProviderResponse",
"apiVersion": "credentialprovider.kubelet.k8s.io/v1",
"cacheKeyType": "Registry",
"cacheDuration": "0h5m0s",
"auth": {
"'"$(echo "$INPUT" | jq -r .image)"'": {"username": "", "password": ""}
}
}' | jq .)
echo "$(date): Response: $RESPONSE" >>"$LOG"
echo "$RESPONSE" | jq -c
kind: CredentialProviderConfig
apiVersion: kubelet.config.k8s.io/v1
providers:
- name: credential-provider
matchImages:
- docker.io
defaultCacheDuration: "1s"
apiVersion: credentialprovider.kubelet.k8s.io/v1
tokenAttributes:
serviceAccountTokenAudience: https://kubernetes.default.svc
cacheType: "Token"
requireServiceAccount: false
apiVersion: v1
kind: Pod
metadata:
name: test-pod
namespace: default
spec:
containers:
- name: test-container
image: nginx:1.23.2
imagePullPolicy: Always
---
apiVersion: v1
kind: Secret
metadata:
name: my-secret
namespace: default
data:
# Should probably follow the .dockerconfigjson format
key: c3VwZXJzZWNyZXQ=
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: secrets-role
namespace: default
rules:
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get", "list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: secrets-role-binding
namespace: default
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: secrets-role
subjects:
- apiGroup: rbac.authorization.k8s.io
kind: User
name: system:serviceaccount:default:default
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: node-credential-providers
rules:
- apiGroups: [""]
resources: ["serviceaccounts"]
verbs: ["get", "list"]
- apiGroups: [""]
resources: ["*"]
verbs: ["request-serviceaccounts-token-audience"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: node-credential-providers-binding
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: node-credential-providers
subjects:
- apiGroup: rbac.authorization.k8s.io
kind: User
# Required for each node, is there a better way to do this?
name: system:node:127.0.0.1
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment