Kubernetes registry credentials that expire on their own
A GitLab deploy token in an imagePullSecret is a long-lived credential sitting in a namespace, and it does not have to be.
Pulling images from a GitLab container registry needs an imagePullSecret.
The usual way to get one is to create a deploy token, paste it into a dockerconfigjson secret, and never think about it again.
So every namespace ends up with a credential that works for a year, nobody knows which ones exist, and rotating them means finding all of them first.
The plugin mints deploy tokens on demand, and the External Secrets Operator can ask for one on a schedule, so the secret can just expire.
So we start with a role, which is where the shape of the token gets decided.
vault write gitlab/roles/registry-pull \ name="vault-{{ randHexString 4 }}" \ path=group/project \ scopes=read_registry \ token_type=project-deploy \ ttl=168hread_registry is the only scope we need here, and a deploy token holding only that can pull images and do nothing else at all.
I template the name as well, so that every token we create is distinguishable in the GitLab UI, instead of forty entries all called the same thing.
Then we need the operator side, which is two objects, a generator that knows how to ask Vault and an ExternalSecret that turns the answer into the right kind of secret.
apiVersion: generators.external-secrets.io/v1alpha1kind: VaultDynamicSecretmetadata: name: gitlab-regcredspec: path: gitlab/token/registry-pull method: POST resultType: Data provider: server: https://vault.example.com auth: kubernetes: mountPath: kubernetes role: my-app serviceAccountRef: name: defaultThere is no Vault token anywhere in the cluster, the pod’s service account is the identity, and Vault decides what that identity is allowed to ask for.
The ExternalSecret then templates the result into the format a kubelet expects.
refreshInterval: 168h target: template: type: kubernetes.io/dockerconfigjson data: .dockerconfigjson: '{"auths":{"registry.example.com":{"auth":"{{ print .username ":" .token | b64enc }}"}}}'Keep refreshInterval and the role ttl the same, because if you refresh less often than the token lives then the secret in the cluster is dead before anything gets round to replacing it.
What you end up with is a registry credential that nobody typed and nobody stored.
It stops working on its own if the cluster stops asking for it, which is the part I wanted.