Overview
External Secrets Operator syncs secrets from an external secret store (Vault, AWS Secrets Manager, GCP Secret Manager, etc.) into native Kubernetes Secret objects, so credentials for things like database passwords, API keys, and DNS provider tokens live in one central store instead of being scattered across cluster manifests.
Recommended backend for RamNode: HashiCorp Vault, self-hosted. This keeps secret storage on RamNode-controlled infrastructure rather than depending on a third-party cloud KMS, and keeps the whole secret path self-managed. If a cloud KMS is preferred instead, see the alternate backend section at the end.
1. Prerequisites
- Kubernetes 1.27+, Helm 3
- A running Vault cluster (or a single-node Vault for smaller deployments) reachable from the Kubernetes cluster network
- Vault KV v2 secrets engine enabled
- Kubernetes auth method enabled on Vault, so pods can authenticate via their ServiceAccount token rather than static Vault tokens
If Vault isn't deployed yet:
helm repo add hashicorp https://helm.releases.hashicorp.com
helm repo update
helm install vault hashicorp/vault \
--namespace vault --create-namespace \
--set "server.dev.enabled=false" \
--set "server.ha.enabled=true" \
--set "server.ha.replicas=3"Initialize and unseal per standard Vault procedure (out of scope here — see Vault's own operations docs), then enable KV v2:
vault secrets enable -path=ramnode-kv kv-v2
vault auth enable kubernetes2. Install External Secrets Operator
helm repo add external-secrets https://charts.external-secrets.io
helm repo update
helm install external-secrets external-secrets/external-secrets \
--namespace external-secrets --create-namespace \
--set installCRDs=trueVerify:
kubectl get pods -n external-secrets
kubectl get crds | grep external-secrets3. Configure Vault Kubernetes auth
On the Vault side, bind a Vault role to the ServiceAccount ESO will use:
vault write auth/kubernetes/config \
kubernetes_host="https://kubernetes.default.svc:443"
vault policy write ramnode-eso-policy - <<EOF
path "ramnode-kv/data/*" {
capabilities = ["read"]
}
EOF
vault write auth/kubernetes/role/eso-role \
bound_service_account_names=eso-vault-sa \
bound_service_account_namespaces=external-secrets \
policies=ramnode-eso-policy \
ttl=1hCreate the matching ServiceAccount in-cluster:
kubectl create serviceaccount eso-vault-sa -n external-secrets4. Create a SecretStore
apiVersion: external-secrets.io/v1
kind: SecretStore
metadata:
name: vault-backend
namespace: default
spec:
provider:
vault:
server: "https://vault.internal.ramnode.com:8200"
path: "ramnode-kv"
version: "v2"
auth:
kubernetes:
mountPath: "kubernetes"
role: "eso-role"
serviceAccountRef:
name: eso-vault-saUse a ClusterSecretStore instead of SecretStore if multiple namespaces (or multiple apps) need to pull from the same Vault mount.
kubectl apply -f secret-store.yaml
kubectl describe secretstore vault-backendStatus should show Valid.
5. Sync a secret
Store the source secret in Vault first, for example a provisioning API credential:
vault kv put ramnode-kv/solusvm-api \
api_id="<id>" \
api_key="<key>"Then define the ExternalSecret:
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: solusvm-api-credentials
namespace: default
spec:
refreshInterval: 1h
secretStoreRef:
name: vault-backend
kind: SecretStore
target:
name: solusvm-api-secret
creationPolicy: Owner
data:
- secretKey: api_id
remoteRef:
key: solusvm-api
property: api_id
- secretKey: api_key
remoteRef:
key: solusvm-api
property: api_keykubectl apply -f external-secret.yaml
kubectl get externalsecret
kubectl get secret solusvm-api-secret -o yamlA pod can then mount solusvm-api-secret as a normal Kubernetes Secret — the pod never talks to Vault directly.
6. Suggested secret map for RamNode workloads
| ExternalSecret name | Vault path | Consumed by |
|---|---|---|
solusvm-api-secret | ramnode-kv/solusvm-api | Migration/capacity collector scripts if containerized |
mariadb-credentials | ramnode-kv/mariadb | Any in-cluster service querying MariaDB directly |
cloudflare-api-token | ramnode-kv/cloudflare | cert-manager DNS-01 solver and other DNS automation |
billing-api-credentials | ramnode-kv/billing | Billing/automation integrations running in-cluster |
Centralizing these in Vault means rotating a credential is a single vault kv put plus a refreshInterval cycle, rather than editing group_vars and re-running Ansible across the fleet.
7. Operational checklist
-
refreshIntervalset appropriately per secret — short (5–15m) for credentials rotated frequently, longer (1h+) for stable ones, to avoid hammering Vault - Vault policy scoped per-path, not
ramnode-kv/*blanket read, if multiple teams/apps share the mount - Vault auto-unseal configured (cloud KMS or Shamir with documented key holders) so a Vault restart doesn't stall every
ExternalSecretsync fleet-wide - Alerting on
ExternalSecretstatus — a stale/failed sync should page before the underlying Secret goes stale and breaks a dependent service - RBAC: restrict who can create
SecretStore/ClusterSecretStoreobjects, since misconfiguration can expose unrelated Vault paths to a namespace
Alternate backend: cloud KMS instead of Vault
If a cloud secrets manager is preferred over self-hosted Vault, ESO supports it with a different SecretStore.spec.provider block, e.g.:
spec:
provider:
aws:
service: SecretsManager
region: us-east-1
auth:
jwt:
serviceAccountRef:
name: eso-aws-saThis requires IRSA-equivalent setup (workload identity federation) which is more natural on EKS/GKE than on a self-managed RamNode cluster — Vault with Kubernetes auth is the lower-friction path for this environment.
