Secrets
    Vault

    Deploy External Secrets Operator on Kubernetes

    Sync secrets from HashiCorp Vault into Kubernetes with External Secrets Operator — Vault Kubernetes auth, SecretStores, and ExternalSecret manifests.

    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:

    shell
    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:

    shell
    vault secrets enable -path=ramnode-kv kv-v2
    vault auth enable kubernetes

    2. Install External Secrets Operator

    shell
    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=true

    Verify:

    shell
    kubectl get pods -n external-secrets
    kubectl get crds | grep external-secrets

    3. Configure Vault Kubernetes auth

    On the Vault side, bind a Vault role to the ServiceAccount ESO will use:

    shell
    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=1h

    Create the matching ServiceAccount in-cluster:

    shell
    kubectl create serviceaccount eso-vault-sa -n external-secrets

    4. Create a SecretStore

    shell
    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-sa

    Use a ClusterSecretStore instead of SecretStore if multiple namespaces (or multiple apps) need to pull from the same Vault mount.

    shell
    kubectl apply -f secret-store.yaml
    kubectl describe secretstore vault-backend

    Status should show Valid.


    5. Sync a secret

    Store the source secret in Vault first, for example a provisioning API credential:

    shell
    vault kv put ramnode-kv/solusvm-api \
      api_id="<id>" \
      api_key="<key>"

    Then define the ExternalSecret:

    shell
    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_key
    shell
    kubectl apply -f external-secret.yaml
    kubectl get externalsecret
    kubectl get secret solusvm-api-secret -o yaml

    A 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 nameVault pathConsumed by
    solusvm-api-secretramnode-kv/solusvm-apiMigration/capacity collector scripts if containerized
    mariadb-credentialsramnode-kv/mariadbAny in-cluster service querying MariaDB directly
    cloudflare-api-tokenramnode-kv/cloudflarecert-manager DNS-01 solver and other DNS automation
    billing-api-credentialsramnode-kv/billingBilling/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

    • refreshInterval set 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 ExternalSecret sync fleet-wide
    • Alerting on ExternalSecret status — a stale/failed sync should page before the underlying Secret goes stale and breaks a dependent service
    • RBAC: restrict who can create SecretStore/ClusterSecretStore objects, 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.:

    shell
    spec:
      provider:
        aws:
          service: SecretsManager
          region: us-east-1
          auth:
            jwt:
              serviceAccountRef:
                name: eso-aws-sa

    This 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.