Skip to content

Injected pull secrets

This documents the in-development version. Switch to v2.3 for the latest stable release.

When the webhook rewrites a container to a registry that needs a credential, the kubelet needs that credential to pull. An auth whose injectPullSecret is on makes kuik provide it: the webhook adds kuik-inject-<kind>-<name> to the pod’s imagePullSecrets, and the secret-syncer process writes that Secret in the pod’s namespace.

One Secret exists per routing resource and namespace. It is a kubernetes.io/dockerconfigjson Secret labelled app.kubernetes.io/managed-by: kuik-secret-syncer and owned by the resource, so deleting the resource deletes its Secrets in every namespace.

rewritePolicyThe Secret is writtenIt holds
Alwaysin every namespace the namespaceSelector selects, before any pod existsevery entry of the resource whose auth injects
OnFailurein a namespace once a pod there has been rewritten by the resourcethe entries that served the rewrites of its live pods

Under Always, podSelector does not narrow where the Secret lands: an absent or empty namespaceSelector puts it in every namespace of the cluster. Set a namespaceSelector to bound it. A resource none of whose entries injects writes no Secret at all.

Under OnFailure, the first rewritten pod of a namespace may start before its Secret exists. The kubelet retries the pull and reads the Secret again on each attempt, so the pod starts a few seconds later; the next pods find the Secret in place.

Each entry gets one auths key, its registry path: the repository or repositoryGroup value as written, or an ImageMirror’s destination.path without its trailing slash. Two entries of one host, quay.io/acme and quay.io/other, keep their own credentials: the kubelet picks the most specific key matching the image.

The value is the credential of the secretRef Secret that the kubelet would use for that path, the most specific of its own auths keys. A secretRef that is missing, is not a docker-registry Secret, or holds no credential for the path is left out, and the other entries are still written. kuik reports it with a PullSecretInjectionFailed event on the resource.

provider credentials are not injected yet: an entry with a provider is left out, whatever its injectPullSecret.

The syncer writes with a server-side apply and never reads the Secrets it wrote: it cannot read any Secret outside the cluster resource namespace. It writes again when:

  • a secretRef Secret is created or changes in the cluster resource namespace;
  • the resource changes, or a namespace enters or leaves its scope;
  • the pods of a namespace change what an OnFailure resource needs;
  • the informers resync, which restores a Secret someone else deleted.

A write that changes nothing is a no-op on the API server.

The syncer may create and patch Secrets in every namespace, which RBAC cannot narrow further. A ValidatingAdmissionPolicy installed by the chart bounds it at the API server. A Secret the syncer creates or updates must:

  • be of type kubernetes.io/dockerconfigjson;
  • carry the label app.kubernetes.io/managed-by: kuik-secret-syncer;
  • be named kuik-inject-* in any namespace, or kuik-provider-* in the namespace kuik is installed in.

The two prefixes are reserved both ways: any other identity is refused the creation or update of a Secret named kuik-inject-* or kuik-provider-*, in every namespace. Two updates are let through, both leaving the type, the data, the labels and the immutability unchanged: one that also leaves the owner references unchanged, such as a kubectl replace of every Secret to rotate the encryption at rest, and the garbage collector removing an owner reference. Deletions are not checked, so deleting a resource still deletes its Secrets. The policy fails closed: when the API server cannot evaluate it, the write is refused. Since the policy sees every Secret write in the cluster, a failure to evaluate it would refuse all of them, Helm releases included: its match conditions limit it to the writes of the syncer and to the reserved names, and every other Secret write skips it.

A backup tool that recreates Secrets on restore, such as Velero, is refused these names. It is harmless, since the syncer writes them again, but the refusals show in the restore log, and Velero marks the restore PartiallyFailed, which fires any alert on that status: exclude these Secrets from the backup to avoid them.

Under OnFailure, an entry that no live pod uses any more stays 15 minutes before it is removed: the pods of a rollout or a scale-up during an outage come back and find it. The Secret itself is never deleted by the syncer. With no entry left, it holds {"auths":{}}. A namespace that leaves the scope of an Always resource gets the same empty Secret.

The syncer keeps which Secrets it wrote in memory. After a restart, it does not know a Secret nothing needs any more, so such a Secret keeps its last content:

  • under OnFailure, when the pods of its namespace all went away while the syncer was stopped, until a new rewrite lands in that namespace;
  • under Always, when its namespace left the scope while the syncer was stopped, until the namespace comes back into scope.

Deleting the resource deletes those Secrets in both cases.