Skip to content

Use Frostmoln Secrets with External Secrets Operator (ESO)

External Secrets Operator (ESO) syncs secrets from an external store into native Kubernetes Secret objects. Its generic webhook provider can read from any HTTP API — including Frostmoln Secrets — so you can keep your secrets managed in Frostmoln and have ESO mirror them into your clusters, with no ESO-side code and no custom provider.

This works on any Kubernetes cluster with internet egress — bring-your-own, on-prem, or a Frostmoln managed cluster — because ESO reaches the public API edge (api.frostmoln.cloud) over HTTPS.

What this does

You store a secret in Frostmoln once. On each refresh interval, ESO calls the Frostmoln lookup endpoint, reads the value, and writes it into a Kubernetes Secret your workloads consume as usual (env var or mounted file). When you rotate the value in Frostmoln, ESO picks up the new value on the next refresh.

Prerequisites

  • ESO installed in the cluster.
    • Frostmoln managed Kubernetes: ESO ships as the external-secrets cluster add-on — enable it rather than installing your own (the scoped cluster kubeconfig cannot install cluster-wide CRDs and webhooks yourself).
    • Any other cluster: install the stock chart —
      bash
      helm repo add external-secrets https://charts.external-secrets.io
      helm install external-secrets external-secrets/external-secrets \
        -n external-secrets --create-namespace --set installCRDs=true
  • A Frostmoln API key. Create one under Account → API Keys in the portal (or with fm account api-key create).
  • Your tenant ID — from fm auth whoami or the portal.

Scope the API key to secrets

When creating the key, grant it only the secrets:read scope. Frostmoln enforces API-key scopes server-side, so a key without secrets:read is refused (403) by the Secrets service at request time — scoping the key genuinely restricts what it can do, not just as a convention. Treat the key as sensitive; ESO stores it in a Kubernetes Secret.

Step 1 — Store the API key as a labeled Secret

ESO's webhook provider reads the API key from a Kubernetes Secret. The Secret must carry the label external-secrets.io/type: webhook — the webhook provider refuses to read an unlabeled Secret.

yaml
apiVersion: v1
kind: Secret
metadata:
  name: frostmoln-api-key
  namespace: default
  labels:
    external-secrets.io/type: webhook # REQUIRED — webhook provider ignores an unlabeled Secret
stringData:
  apikey: fmk_... # your Frostmoln API key (secrets:read scope)

Create this Secret from a local file with kubectl applydo not commit it to git. Only the SecretStore and ExternalSecret manifests below are safe to keep in source control.

Step 2 — Create the SecretStore

The SecretStore points ESO at the Frostmoln lookup endpoint. Use the CRD group external-secrets.io/v1 (not the older v1beta1). Replace <TENANT_ID> with your tenant ID.

yaml
apiVersion: external-secrets.io/v1
kind: SecretStore
metadata:
  name: frostmoln
  namespace: default
spec:
  provider:
    webhook:
      url: 'https://api.frostmoln.cloud/api/v1/tenants/<TENANT_ID>/secrets/lookup?name={{ .remoteRef.key }}'
      headers:
        # per-secret MAP: <secrets-entry-name>.<data-key>.
        # {{ .apikey }} renders the whole map and the API returns 401 — use the .apikey key.
        X-API-Key: '{{ .apikey.apikey }}'
      result:
        jsonPath: '$.secretValue'
      secrets:
        - name: apikey
          secretRef:
            name: frostmoln-api-key
            key: apikey

The {{ .remoteRef.key }} in the URL is filled in per ExternalSecret with the Frostmoln secret name (Step 3), so one SecretStore serves all your secrets.

Step 3 — Create an ExternalSecret

Each ExternalSecret maps one Frostmoln secret to one Kubernetes Secret. remoteRef.key is the Frostmoln secret name; target.name is the Kubernetes Secret ESO creates and keeps in sync.

yaml
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
  name: my-secret
  namespace: default
spec:
  refreshInterval: 1m
  secretStoreRef: { name: frostmoln, kind: SecretStore }
  target: { name: my-secret-synced }
  data:
    - secretKey: value # key inside the synced Kubernetes Secret
      remoteRef: { key: my-secret-name } # the Frostmoln secret name

ESO creates the my-secret-synced Secret with a value key holding the fetched secret value. Mount it or reference it from your Pod like any other Secret.

Refresh interval

refreshInterval controls how often ESO re-fetches. A short interval propagates rotations faster but makes more API calls; a longer one is lighter. Choose per secret based on how quickly a rotation must reach your workloads — a minute or a few minutes is typical.

Pin a specific version

Frostmoln secrets are versioned. To pin a version, add &version=N to the SecretStore URL (create a second SecretStore for the pinned secret):

yaml
url: 'https://api.frostmoln.cloud/api/v1/tenants/<TENANT_ID>/secrets/lookup?name={{ .remoteRef.key }}&version=2'

Without &version, ESO always reads the latest version.

Deleting a secret is fail-safe

If you fm secrets delete a secret that ESO is syncing, the lookup starts returning an error (the secret enters pending-deletion). ESO's refresh fails (SecretSyncedError) and keeps the last synced Kubernetes Secret unchanged — it does not blank it out. Your workloads keep running on the last-known-good value.

Recover to resume syncing

While the secret is deleted, ESO cannot refresh it and the value in your cluster goes stale. Run fm secrets recover <name> within the recovery window to restore it; ESO then resumes syncing on the next interval. If the recovery window passes and the secret is permanently removed, the lookup keeps failing — recreate the secret to resume.

Troubleshooting

Check the ExternalSecret status with kubectl describe externalsecret <name>.

  • secret does not contain needed label — the API-key Secret is missing the external-secrets.io/type: webhook label (Step 1).
  • 401 / unauthorized — the header template is wrong. It must be {{ .apikey.apikey }} (<secrets-entry-name>.<data-key>); {{ .apikey }} renders the whole map and the API rejects it. Also check the key value and that the key hasn't expired.
  • could not get secret data from provider — the secret name or tenant ID is wrong, or the secret is deleted / pending-deletion. Verify the name resolves: fm secrets get <name>.
  • Nothing syncs at all — confirm ESO is running (kubectl -n external-secrets get pods) and the SecretStore uses external-secrets.io/v1, not v1beta1.