๐ฑ 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:
- Install ESO only on the management cluster.
- Define a
ClusterSecretStore or SecretStore
(with kubeconfig) to point to the child cluster. - Create a
PushSecret
that selects a local secret and sends it to the target cluster. - 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: