externalsecret.md 8.7 KB

The ExternalSecret describes what data should be fetched, how the data should be transformed and saved as a Kind=Secret:

  • tells the operator what secrets should be synced by using spec.data to explicitly sync individual keys or use spec.dataFrom to get all values from the external API.
  • you can specify how the secret should look like by specifying a spec.target.template

Template

When the controller reconciles the ExternalSecret it will use the spec.template as a blueprint to construct a new Kind=Secret. You can use golang templates to define the blueprint and use template functions to transform secret values. You can also pull in ConfigMaps that contain golang-template data using templateFrom. See advanced templating for details.

Update behavior with 3 different refresh policies

You can control how and when the ExternalSecret is refreshed by setting the spec.refreshPolicy field. If not specified, the default behavior is Periodic.

CreatedOnce

With refreshPolicy: CreatedOnce, the controller syncs the Kind=Secret once per ExternalSecret object and then stops:

  • Runs a single sync on the first reconcile of the ExternalSecret, creating or overwriting the target Kind=Secret
  • Does not refresh on a schedule or when the source data changes
  • Still re-syncs if the target Kind=Secret is changed or deleted while the same ExternalSecret object still exists
  • Useful for immutable credentials or when you want to manage updates manually

The "once" is tracked on the ExternalSecret's own status (syncedResourceVersion and refreshTime), not on whether the target Secret already exists.

!!! warning "Recreating the ExternalSecret re-syncs the target Secret"

`CreatedOnce` does not check whether the target Secret already exists. If the
`ExternalSecret` object itself is deleted and recreated (for example by a GitOps
controller that prunes and re-applies it), its status resets and the controller
performs a fresh sync that overwrites the existing target Secret. With a
stateless generator such as the Password generator this produces a brand new
value, so the target Secret and any credential already derived from the old
value diverge with no way to recover the original.

`creationPolicy` does not guard against this: `Owner`, `Orphan`, `Merge` and
`CreateOrMerge` all rewrite the managed keys on that sync. The only field that prevents an existing
target Secret's data from being overwritten is `spec.target.immutable: true`,
which skips the data update whenever the Secret already exists. Combine
`refreshPolicy: CreatedOnce` with `spec.target.immutable: true` for credentials
that must be generated once and never change.

Example:

apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
  name: example
spec:
  refreshPolicy: CreatedOnce
  # other fields...

To generate a credential exactly once and never overwrite it afterward, even if the ExternalSecret object is recreated, combine refreshPolicy: CreatedOnce with spec.target.immutable: true. This is the correct pattern for a credential that an application persists on first run (for example a Keycloak admin password written into the application database at bootstrap): regenerating the value later would leave the target Secret out of sync with the live credential. creationPolicy: Orphan keeps the target Secret in place if the ExternalSecret is deleted, and immutable: true stops any later reconcile (including one triggered by recreating the ExternalSecret) from rewriting the existing data.

apiVersion: generators.external-secrets.io/v1alpha1
kind: Password
metadata:
  name: keycloak-admin-password
spec:
  length: 32
  digits: 5
  symbols: 5
  allowRepeat: true
---
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
  name: keycloak-admin
spec:
  refreshPolicy: CreatedOnce
  target:
    name: keycloak-admin
    # keep the Secret if this ExternalSecret is ever deleted
    creationPolicy: Orphan
    # never rewrite the Secret data once it exists, even on recreation
    immutable: true
  dataFrom:
    - sourceRef:
        generatorRef:
          apiVersion: generators.external-secrets.io/v1alpha1
          kind: Password
          name: keycloak-admin-password

Periodic

With refreshPolicy: Periodic (the default behavior), the controller will:

  • Create the Kind=Secret if it doesn't exist
  • Update the Kind=Secret regularly based on the spec.refreshInterval duration
  • When spec.refreshInterval is set to zero, it will only create the secret once and not update it afterward
  • When spec.refreshInterval is set to a value greater than zero, the controller will update the Kind=Secret at the specified interval or when the ExternalSecret specification changes

Example:

apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
  name: example
spec:
  refreshPolicy: Periodic
  refreshInterval: 1h0m0s  # Update every hour
  # other fields...

OnChange

With refreshPolicy: OnChange, the controller will:

  • Create the Kind=Secret if it doesn't exist
  • Update the Kind=Secret only when the ExternalSecret's metadata or specification changes
  • This policy is independent of the refreshInterval value
  • Useful when you want to manually control when the secret is updated, by modifying the ExternalSecret resource

Example:

apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
  name: example
spec:
  refreshPolicy: OnChange
  # other fields...

Manual Refresh

If supported by the configured refreshPolicy, you can manually trigger a refresh of the Kind=Secret by updating the annotations of the ExternalSecret:

kubectl annotate es my-es force-sync=$(date +%s) --overwrite

SyncWindows

syncWindows restricts when periodic refreshes may occur. It is evaluated in UTC and applies only to the Periodic refresh policy (or when refreshPolicy is unset). OnChange and CreatedOnce policies are unaffected.

A sync-windows block carries a shared kind and a list of schedule + duration entries:

  • kind: allow -- periodic syncs are permitted only while at least one window is active; all other times are blocked.
  • kind: deny -- periodic syncs are blocked while any window is active; all other times proceed normally.

Each entry in windows uses a standard 5-field cron schedule (UTC) and a duration string (e.g. 8h, 30m). The window stays open for duration after each schedule firing. A window entry with an unparseable schedule is silently ignored and treated as inactive, so a typo does not permanently block syncs.

Example: allow syncs only during business hours (Mon-Fri 09:00-17:00 UTC)

apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
  name: example
spec:
  refreshInterval: 1h
  syncWindows:
    kind: allow
    windows:
      - schedule: "0 9 * * 1-5"  # weekdays at 09:00 UTC
        duration: 8h              # window open until 17:00 UTC

Example: block syncs during a Saturday maintenance window (02:00-04:00 UTC)

apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
  name: example
spec:
  refreshInterval: 30m
  syncWindows:
    kind: deny
    windows:
      - schedule: "0 2 * * 6"  # Saturdays at 02:00 UTC
        duration: 2h            # block until 04:00 UTC

Multiple windows

You can list several entries under windows. For kind: allow, the sync is permitted when any window is active. For kind: deny, the sync is blocked when any window is active.

Interaction with refreshInterval

syncWindows only suppresses sync operations -- it does not change how often the controller checks. The controller still requeues at refreshInterval regardless of whether a sync was blocked. This means that if refreshInterval is longer than window.duration, a window could open and close entirely between two consecutive checks and the sync would be missed for that occurrence. This is by design: refreshInterval is the primary driver; syncWindows is a gate on top of it. To ensure no window occurrence is missed, set refreshInterval to a value shorter than the smallest window.duration.

Features

Individual features are described in the Guides section:

Example

Take a look at an annotated example to understand the design behind the ExternalSecret.

{% include 'full-external-secret.yaml' %}