Putting Together the Puzzle Pieces 🧩
Rafael Franzke, SAP
Tim Ebert, STACKIT
Each Kubernetes cluster comes with a lot of credentials:
Gardener manages thousands of clusters
How to orchestrate credentials rotation at such scale?
Goals: Disruption-free, minimal ops, fully automated
ServiceAccount
apiVersion: v1
kind: ServiceAccount
metadata:
name: robot
Pods contacting the API server authenticate as part of a ServiceAccount
:
apiVersion: v1
kind: Pod
metadata:
name: my-pod
spec:
serviceAccountName: robot
Since Kubernetes v1.22
, this results in
spec: serviceAccountName: robot containers: - name: nginx image: nginx volumeMounts: - name: kube-api-access-5f65c mountPath: /var/run/secrets/kubernetes.io/serviceaccount readOnly: true volumes: - name: kube-api-access-5f65c projected: sources: - serviceAccountToken: expirationSeconds: 3607 path: token - configMap: items: - key: ca.crt path: ca.crt name: kube-root-ca.crt - downwardAPI: items: - fieldRef: apiVersion: v1 fieldPath: metadata.namespace path: namespace
spec: serviceAccountName: robot containers: - name: nginx image: nginx volumeMounts: - name: kube-api-access-5f65c mountPath: /var/run/secrets/kubernetes.io/serviceaccount readOnly: true volumes: - name: kube-api-access-5f65c projected: sources: - serviceAccountToken: expirationSeconds: 3607 path: token - configMap: items: - key: ca.crt path: ca.crt name: kube-root-ca.crt - downwardAPI: items: - fieldRef: apiVersion: v1 fieldPath: metadata.namespace path: namespace
spec: serviceAccountName: robot containers: - name: nginx image: nginx volumeMounts: - name: kube-api-access-5f65c mountPath: /var/run/secrets/kubernetes.io/serviceaccount readOnly: true volumes: - name: kube-api-access-5f65c projected: sources: - serviceAccountToken: expirationSeconds: 3607 path: token - configMap: items: - key: ca.crt path: ca.crt name: kube-root-ca.crt - downwardAPI: items: - fieldRef: apiVersion: v1 fieldPath: metadata.namespace path: namespace
TokenRequest
APISince Kubernetes v1.22
, this results in
spec: containers: - name: nginx image: nginx volumeMounts: - name: kube-api-access-5f65c mountPath: /var/run/secrets/kubernetes.io/serviceaccount readOnly: true volumes: - name: kube-api-access-5f65c projected: sources: - serviceAccountToken: expirationSeconds: 3607 path: token - configMap: items: - key: ca.crt path: ca.crt name: kube-root-ca.crt - downwardAPI: items: - fieldRef: apiVersion: v1 fieldPath: metadata.namespace path: namespace
spec: containers: - name: nginx image: nginx volumeMounts: - name: kube-api-access-5f65c mountPath: /var/run/secrets/kubernetes.io/serviceaccount readOnly: true volumes: - name: kube-api-access-5f65c projected: sources: - serviceAccountToken: expirationSeconds: 3607 path: token - configMap: items: - key: ca.crt path: ca.crt name: kube-root-ca.crt - downwardAPI: items: - fieldRef: apiVersion: v1 fieldPath: metadata.namespace path: namespace
const ( WarnOnlyBoundTokenExpirationSeconds = 60*60+7 // 3607 ExpirationExtensionSeconds = 24*365*60*60 // 1y ) exp := req.Spec.ExpirationSeconds if pod != nil && r.isKubeAudiences(req.Spec.Audiences) && r.extendExpiration && req.Spec.ExpirationSeconds == WarnOnlyBoundTokenExpirationSeconds { exp = ExpirationExtensionSeconds }
const ( WarnOnlyBoundTokenExpirationSeconds = 60*60+7 // 3607 ExpirationExtensionSeconds = 24*365*60*60 // 1y ) exp := req.Spec.ExpirationSeconds if pod != nil && r.isKubeAudiences(req.Spec.Audiences) && r.extendExpiration && req.Spec.ExpirationSeconds == WarnOnlyBoundTokenExpirationSeconds { exp = ExpirationExtensionSeconds }
const ( WarnOnlyBoundTokenExpirationSeconds = 60*60+7 // 3607 ExpirationExtensionSeconds = 24*365*60*60 // 1y ) exp := req.Spec.ExpirationSeconds if pod != nil && r.isKubeAudiences(req.Spec.Audiences) && r.extendExpiration && req.Spec.ExpirationSeconds == WarnOnlyBoundTokenExpirationSeconds { exp = ExpirationExtensionSeconds }
const ( WarnOnlyBoundTokenExpirationSeconds = 60*60+7 // 3607 ExpirationExtensionSeconds = 24*365*60*60 // 1y ) exp := req.Spec.ExpirationSeconds if pod != nil && r.isKubeAudiences(req.Spec.Audiences) && r.extendExpiration && req.Spec.ExpirationSeconds == WarnOnlyBoundTokenExpirationSeconds { exp = ExpirationExtensionSeconds }
kube-apiserver silently overwrites the expiration seconds 👻 Source
Set --service-account-extend-token-expiration=false
to ensure tokens are indeed only valid for 1h
If you cannot control the flag (e.g., in managed clusters), overwrite the expirationSeconds
to ensure short validity
Before Kubernetes v1.24
, a static token Secret
was automatically generated for ServiceAccount
s:
apiVersion: v1 kind: Secret metadata: name: robot annotations: kubernetes.io/service-account.name: robot kubernetes.io/service-account.uid: da68f9c6-9d26-11e7-b84e-002dc52800da data: ca.crt: <cluster-ca> namespace: default token: <some-static-token>
apiVersion: v1 kind: Secret metadata: name: robot annotations: kubernetes.io/service-account.name: robot kubernetes.io/service-account.uid: da68f9c6-9d26-11e7-b84e-002dc52800da data: ca.crt: <cluster-ca> namespace: default token: <some-static-token>
Such tokens have no expiration date! 😱
Feature | Alpha | Beta | GA |
---|---|---|---|
No auto-generation ✅ | - | 1.24 | 1.26 |
Usage tracking ✅ | 1.26 | 1.27 | 1.28 |
Auto-cleanup ⚠️ | 1.28 | 1.29 | 1.30 |
Most probably static tokens still exist in your clusters! 👹
If you are on v1.24
or higher, manually delete still remaining static token secrets
If you are stuck below v1.24
, consider invalidating the tokens (talk to us!)
Rotation in two phases:
phase | cert signed by | clients trust |
---|---|---|
0 | old CA | old CA |
1 | old CA | old+new CA |
2 | new CA | new CA |
phase | cert signed by | servers trust |
---|---|---|
0 | old CA | old CA |
1 | new CA | old+new CA |
2 | new CA | new CA |
Live Coding!
Slides Available Online:
Follow us on Twitter: |
Check out project Gardener: |