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 .gitignore and 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:

  1. Update the token in Vault (if using ESO) — ESO refreshes the Secret on next cycle
  2. 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 backoff
  • ErrImagePull — initial pull failure
  • 403 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