imagePullSecrets across all namespaces with secretGenerator
Published: 2026-02-15
Private container registries require imagePullSecrets on every Pod in every namespace. If you have 15 namespaces and add one every month, manually managing registry credentials is a maintenance burden. Kustomize's secretGenerator with the FluxCD approach solves this without any operator.
The problem
A Pod needs to pull registry.example.com/myapp:1.0. The registry requires auth. The imagePullSecret must be in the same namespace as the Pod. You can't reference a secret from another namespace.
Options:
- Copy the secret everywhere manually — unmaintainable; when the token rotates, you update 15 places
- Use a Secret replication operator (e.g., Reflector, Kubernetes Replicator) — an extra component to manage, fails silently if the operator has a bug
- Use
secretGenerator— built into Kustomize, zero extra dependencies, GitOps-native
secretGenerator approach
In a FluxCD GitOps repo, create a Kustomization that generates the Secret in every namespace:
yaml# fluxcd/custom/secrets/docker-registries/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
secretGenerator:
- name: registry-example-com
namespace: app
type: kubernetes.io/dockerconfigjson
files:
- .dockerconfigjson=registry-credentials.dockerconfigjson
options:
disableNameSuffixHash: true
- name: registry-example-com
namespace: monitoring
type: kubernetes.io/dockerconfigjson
files:
- .dockerconfigjson=registry-credentials.dockerconfigjson
options:
disableNameSuffixHash: true
- name: registry-example-com
namespace: db
type: kubernetes.io/dockerconfigjson
files:
- .dockerconfigjson=registry-credentials.dockerconfigjson
options:
disableNameSuffixHash: true
disableNameSuffixHash: true keeps the secret name stable (registry-example-com instead of registry-example-com-abc123). This matters because HelmRelease values reference the secret by name. Without this flag, Kustomize adds a content hash to the name, and after any credential rotation the name changes, breaking all references.
Why secretGenerator over copying manifests
secretGenerator is idempotent: running kubectl apply on the same Kustomization will update the secret in each namespace if the content changes. If you add a new namespace, you add one block, commit, and Flux handles the rest. There's no "apply each namespace" loop to maintain.
The .dockerconfigjson file
Generate it once from your credentials:
bashkubectl create secret docker-registry registry-example-com \
--docker-server=registry.example.com \
--docker-username=robot-ci \
--docker-password=TOKEN \
--dry-run=client -o jsonpath='{.data.\.dockerconfigjson}' \
| base64 -d > registry-credentials.dockerconfigjson
The file should look like:
json{
"auths": {
"registry.example.com": {
"username": "robot-ci",
"password": "TOKEN",
"auth": "BASE64_USER_PASS"
}
}
}
Do not commit this file in plaintext. Options:
- Keep it encrypted with SOPS or SealedSecrets — use a SealedSecret per namespace (more boilerplate)
- Store in Vault and use ESO to generate the K8s secret dynamically
- Keep the file in
.gitignoreand inject via CI when generating manifests
We use the ESO approach for production: a single ExternalSecret syncs the dockerconfigjson from Vault into each namespace, using the Merge creationPolicy on the existing secret. For dev/test, the file is in .gitignore and CI injects it during bootstrap.
Rotating registry credentials
When the robot token rotates:
- Update the token in Vault (if using ESO) — ESO refreshes the Secret on next cycle
- If using the file approach: regenerate
registry-credentials.dockerconfigjson, commit, push. Flux reconciles and updates the Secret in all namespaces simultaneously.
No manual kubectl-apply-per-namespace loop needed.
Adding a new namespace
When a new namespace is added, add one block to the kustomization.yaml:
yaml - name: registry-example-com
namespace: newservice
type: kubernetes.io/dockerconfigjson
files:
- .dockerconfigjson=registry-credentials.dockerconfigjson
options:
disableNameSuffixHash: true
Commit, push, Flux reconciles, done. The new namespace gets the secret in under a minute.
Automating the namespace list
If you have many namespaces and want to avoid maintaining the list manually, a simple script generates the Kustomization from the live cluster's namespace list:
bashkubectl get namespaces -o jsonpath='{.items[*].metadata.name}' \
| tr ' ' '\n' \
| grep -v 'kube-\|flux-\|kube$' \
| while read ns; do
cat <<EOF
- name: registry-example-com
namespace: $ns
type: kubernetes.io/dockerconfigjson
files:
- .dockerconfigjson=registry-credentials.dockerconfigjson
options:
disableNameSuffixHash: true
EOF
done
Run this when adding multiple namespaces at once, then review and commit the output.
Using the secret in HelmReleases
Reference it in the HelmRelease values:
yamlvalues:
imagePullSecrets:
- name: registry-example-com
image:
repository: registry.example.com/myapp
Or globally via the Pod spec:
yaml# In your Helm chart's deployment.yaml
spec:
imagePullSecrets:
{{- toYaml .Values.imagePullSecrets | nindent 4 }}
What about default ServiceAccount patching?
You can also patch the default ServiceAccount in each namespace to add an imagePullSecrets entry. This way every Pod in the namespace gets the pull secret automatically, even if the Helm chart doesn't set it:
yamlapiVersion: v1
kind: ServiceAccount
metadata:
name: default
namespace: app
imagePullSecrets:
- name: registry-example-com
Apply this as a Kustomize resource alongside the secretGenerator. Useful for third-party charts that don't expose imagePullSecrets in their values.
The Kubernetes admission controller injects the imagePullSecrets from the ServiceAccount into each new Pod in the namespace. No changes to the Pod spec or the Helm chart required.
Using a configMapGenerator for default SA patches
To generate the ServiceAccount patch for all namespaces:
yamlapiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- sa-app.yaml
- sa-monitoring.yaml
- sa-db.yaml
Where each sa-*.yaml file is a ServiceAccount with the imagePullSecrets entry. Less DRY than secretGenerator but explicit and transparent.
Debugging image pull failures
When a Pod can't pull an image:
bashkubectl describe pod <pod> -n <namespace> | grep -A5 "Events:"
Common events:
ImagePullBackOff— pull failed, will retry with exponential backoffErrImagePull— initial pull failure403 Forbidden/401 Unauthorized— wrong or missing credentials
Check if the secret exists and has the right content:
bashkubectl get secret registry-example-com -n <namespace> -o jsonpath='{.data.\.dockerconfigjson}' | base64 -d | jq .
If the secret is missing, Flux may not have reconciled yet. Check:
bashflux get kustomization secrets -n flux-system