๐ŸŒฑ Securely Bootstrapping Secrets in a ClusterAPI Cluster with ExternalSecrets & PushSecret

When I started automating Kubernetes cluster creation using ClusterAPI, one problem kept coming back:

How can I securely inject secrets into a brand new cluster right at bootstrap โ€” without storing them in Git or relying on fragile post-install hacks?

After a lot of searching, I found a hidden gem in External Secrets Operator (ESO): the powerful but lesser-known PushSecret feature.

๐ŸŒ The Problem: Secure Secret Bootstrap

In a typical ClusterAPI setup, you have a management cluster that provisions workload clusters. That works great, but raises a tricky issue:

How can you get critical secrets (certs, tokens, credentials) into a cluster at creation time โ€” securely, and without manual steps?

Most solutions I found fell short:

  • Putting secrets in Git (security nightmare),
  • Writing brittle post-creation scripts,
  • Waiting for the cluster to be ready before installing ESO/Vault (too late for early-stage secrets).

๐Ÿ”‘ The Solution: ExternalSecrets + PushSecret

If youโ€™ve used External Secrets Operator before, you likely know it for syncing secrets from Vault, AWS Secrets Manager, GCP Secret Manager, etc. to Kubernetes.

But what many people donโ€™t know is that ESO has a very useful feature called PushSecret.

๐Ÿงช What Is PushSecret?

PushSecret lets you sync a Kubernetes secret to another cluster โ€” without installing ESO on the target cluster.

That means you can:

  • Define a secret source (Vault, AWS, or even a local Secret),
  • Push it from the management cluster to a remote child cluster,
  • Do this without Git, without ESO installed on the target, and without manual work.

โš™๏ธ How Does It Work?

Hereโ€™s the basic idea:

  1. Install ESO only on the management cluster.
  2. Define a ClusterSecretStore or SecretStore (with kubeconfig) to point to the child cluster.
  3. Create a PushSecret that selects a local secret and sends it to the target cluster.
  4. ESO does the sync as soon as the child cluster API is available.

PushSecret Example

Define a RemoteCluster:

apiVersion: external-secrets.io/v1
kind: ClusterSecretStore
metadata:
  name: mycapicluster-secretstore
  namespace: default
spec:
  provider:
    kubernetes:
      # with this, the store is able to pull only from `default` namespace
      remoteNamespace: default
      authRef:
        name: mycapicluster-kubeconfig # name of the kubeconfig child cluster on management cluster
        key: value
        namespace: default

๐Ÿ’ก These kubeconfigs are automatically generated by ClusterAPI or its bootstrap provider (CAPBK..

Create a PushSecret to synchronize your secret:

---
apiVersion: external-secrets.io/v1alpha1
kind: PushSecret
metadata:
  name: pushsecret-mysecret
spec:
  refreshInterval: 60s
  selector:
    secret:
      name: mysecret
  secretStoreRefs:
  - kind: ClusterSecretStore
    name: mycapicluster-secretstore
  data:
    - match:
        remoteRef:
          remoteKey: mysecret # Remote reference (where the secret is going to be pushed)

๐ŸŽฏ Real-World Use Cases

Here are some examples where PushSecret has been incredibly helpful:

  • ๐Ÿ” Injecting CSI driver credentials (e.g., AWS EBS, AzureDisk, Vault CSI),
  • ๐Ÿ”‘ Sharing a global TLS certificate across all clusters,
  • ๐Ÿ“ฆ Distributing container registry tokens (e.g., GitHub Container Registry),
  • โš™๏ธ Bootstrapping cert-manager or external-dns with initial secrets.

Why ClusterAPI + ExternalSecrets = โค๏ธ

The combination of ClusterAPI and ExternalSecrets is extremely powerful โ€” because they work so well together.

When a management cluster uses CAPI to create a child cluster, it automatically:

  • Generates the childโ€™s kubeconfig and CA,
  • Assigns a predictable name to the child cluster (based on the cluster object).

This means you already know the child cluster name and its credentials at creation time โ€” no waiting, no guessing.

๐ŸŽฏ As a result, you can predefine a PushSecret pointing to the future cluster. As soon as the clusterโ€™s API becomes available, ESO pushes the secret โ€” automatically.

๐Ÿ” Concrete Example

Say youโ€™re provisioning a new cluster named devcluster. When ClusterAPI creates it, a Secret named devcluster-kubeconfig is created in the cluster namespace (in our example we create the cluster in a devcluster namespace).

You can reference it like this:

apiVersion: external-secrets.io/v1
kind: ClusterSecretStore
metadata:
  name: devcluster-secretstore
  namespace: kube-system
spec:
  provider:
    kubernetes:
      # with this, the store is able to push only to `kube-system` namespace
      remoteNamespace: kube-system
      authRef:
        name: devcluster-kubeconfig # name of the kubeconfig child cluster on management cluster
        key: value
        namespace: devcluster
---
apiVersion: external-secrets.io/v1alpha1
kind: PushSecret
metadata:
  name: registry-token-push
  namespace: kube-system
spec:
  refreshInterval: 60s
  selector:
    secret:
      name: registry-token
      namespace: kube-system
  secretStoreRefs:
  - kind: ClusterSecretStore # Depending the type of secret you can use SecretStore or ClusterSecretStore
    name: devcluster-secretstore
  data:
    - match:
        remoteRef:
          remoteKey: registry-token # Remote reference (where the secret is going to be pushed)

You can now check the status using:

$ kubectl get pushsecrets -n  kube-system

NAME                    STATUS      AGE
registry-token-push     Synchronized   2m

This can be templated and automated via GitOps as soon as a new cluster is declared. The secret will be in place by the time the cluster is ready.

โœ… Benefits

  • ๐Ÿ” Secure: No secrets in Git.
  • โš™๏ธ Automated: Push as soon as the API is ready.
  • ๐Ÿงผ Minimal: Only install ESO on the management cluster.
  • ๐ŸŒ Flexible: Works with Vault, AWS, GCP, Azure, and Kubernetes native secrets.

๐Ÿงต In Summary

If youโ€™re using ClusterAPI and want a secure, automated way to inject secrets into clusters from day one โ€” check out the PushSecret feature in ExternalSecrets Operator.

It solved a long-standing pain point for me โ€” and might save you hours of scripting and debugging too.

๐Ÿ“˜ Official Docs: