Skip to main content

Overview

The External Secret Syncer (ESS) continuously syncs secrets and parameters from external providers into Control Plane secrets. This template deploys ESS as a workload that polls your configured providers on a set interval and creates or updates Control Plane secrets to match. The sync configuration is not a template value. It carries provider credentials — a Vault token, AWS access keys, a 1Password service-account token — so you supply it as an opaque secret holding your whole sync.yaml.
Template version 2.1.0 is a breaking security change. essConfig was removed, and an install or upgrade that still sets it now fails at render. If you are running 2.0.x, read Upgrading From 2.0.x before you touch the release.

How It Works

ESS runs as a workload on Control Plane. Your provider configuration and secrets list are stored in a Control Plane secret and mounted into the workload as sync.yaml. On startup, ESS schedules a polling loop for each configured secret. At each interval, it fetches the latest value from the external provider and creates or updates the corresponding Control Plane secret via the API. ESS tags every secret it manages with syncer.cpln.io/source (set to the workload path). This prevents two ESS instances from accidentally overwriting each other’s secrets. ESS only creates and updates secrets — it never deletes them, so removing a secret from sync.yaml leaves the existing Control Plane secret in place. ESS watches its config file and automatically restarts when changes are detected (every ~5 seconds). No workload restart is needed after updating the config secret.

Supported Providers

What Gets Created

  • Standard ESS Workload — An ESS container with a readiness probe on /about.
  • Identity & Policy — An identity bound to the workload with manage permissions on all secrets, allowing ESS to create and update Control Plane secrets.
  • Secret — An opaque secret containing the sync configuration (sync.yaml) with providers and secret mappings.
This template does not create a GVC. You must deploy it into an existing GVC.

Prerequisites

  1. A secret or parameter stored in one of the supported providers.
  2. Credentials with read access to the desired secret (API token, IAM keys, etc.). Alternatively, you can use a cloud access identity instead of supplying keys directly.
Your sync configuration must exist as an opaque secret before you install. It carries those provider credentials, so it is not a template value — a value would put every one of them in the Helm release. It is also a configuration file, and a cpln://secret reference inside a file is never resolved, so individual keys cannot be swapped out either. The whole file is therefore yours.
1

Write your sync.yaml

The providers and secrets schema is documented under Configuration.
2

Create the secret and point the chart at it

Set configSecretName to the name you used. Secret names are org-wide, so give each release its own.
If the secret does not exist at install time, the deployment wedges silently. cpln logs returns zero lines — the container never starts, so it has nothing to log. Read status.versions[].message instead:
Use get-deployments — plain cpln workload get has no versions key.

Installation

To install, follow the instructions for your preferred method:

UI

Browse, install, and manage templates visually

CLI

Manage templates from your terminal

Terraform

Declare templates in your Terraform configurations

Pulumi

Declare templates in your Pulumi programs

Upgrading From 2.0.x

Version 2.1.0 removes essConfig. The schema is unchanged — migrating is a verbatim lift of that block into a file, minus the top-level essConfig: wrapper.
Carrying essConfig forward stops the upgrade. It is rejected at render, so cpln helm upgrade fails and your existing syncer is left untouched and running:
1

Move your essConfig block into sync.yaml

Copy it verbatim and drop the essConfig: wrapper, so providers: and secrets: become top-level keys.
2

Create the secret and point the chart at it

Delete essConfig from your values and set configSecretName.
3

Rotate every credential that was in your values file

They were stored in the Helm release, so treat them as exposed — reissue the Vault token, AWS keys or 1Password service-account token, then update sync.yaml and the secret.
Why the whole file, and not individual keys. A cpln://secret reference is resolved only for environment variables. Inside a mounted configuration file it stays literal text, so there is no way to swap out just the token — the file itself has to be the secret.

Configuration

The default values.yaml for this template:

Top-Level Fields

  • image — The ESS container image. Do not change unless upgrading.
  • resources.cpu / resources.memory — Resource limits for the workload container.
  • port — Port for the ESS HTTP admin API (default: 3004). Used for health checks and manual sync triggers.
  • allowedIp — List of CIDRs allowed to reach the ESS admin API externally. Replace the placeholder with your IP, or use 0.0.0.0/0 to allow all.
  • configSecretName — Name of the opaque secret holding your sync.yaml, the full sync configuration. See Prerequisites.

Providers

Each entry in providers defines a connection to an external secret store. Every provider must have a unique name. An optional syncInterval sets the default polling interval for all secrets using that provider. AWS providers optionally accept accessKeyId and secretAccessKey. If omitted, ESS falls back to credentials provided through the workload’s cloud access identity. GCP Secret Manager optionally accepts credentials.clientEmail and credentials.privateKey. If omitted, ESS uses Application Default Credentials. Provider examples:

Secrets

Each entry in secrets maps an external secret to a Control Plane secret. Each secret must specify a name, a provider, and exactly one sync type.

opaque — Single value

Creates a Control Plane opaque secret from a single fetched value. Shorthand (path only):
With options:
Vault KV engine secrets are nested under a data key. When using parse, start with data to access the secret content (e.g., data.password).If you use the shorthand form with no default, a fetch failure causes the sync to fail with no fallback.

dictionary — Multiple values

Creates a Control Plane dictionary secret. Each key is fetched independently and supports path, parse, default, and encoding.
A failure on one key does not block the others.

dictionaryFromProject — Sync an entire project

Syncs all secrets from a provider project in one operation, stored as a Control Plane dictionary secret. Only valid with a Doppler or GCP Secret Manager provider. The expected shape differs per provider. Doppler — specify a project/config path:
GCP Secret Manager — set to true to pull every accessible secret from the project configured on the provider. Each fetched secret’s latest version becomes one key in the resulting dictionary. Secrets with no accessible latest version (no versions, disabled, or destroyed) are skipped.
Doppler requires the { path: ... } object form; GCP Secret Manager requires the true form. Mixing them — or using either with another provider — causes ESS to exit at startup.

dictionaryFromJson — Flatten a JSON object

Fetches a single value that contains a JSON object and flattens it into a Control Plane dictionary secret. Set it to the path of the value to fetch. Valid with any provider.
Each leaf in the JSON object becomes one dictionary key:
  • Nested objects are flattened using dot notation ({ "db": { "host": "x" } } → key db.host).
  • Arrays are JSON-stringified into the value ({ "tags": ["a", "b"] } → key tags with value ["a","b"]).
  • null is stored as the string "null"; numbers and booleans are stored as their string form.
For example, the JSON object {"db":{"host":"db.internal","port":5432},"tags":["a","b"]} produces a dictionary with keys db.host (db.internal), db.port (5432), and tags (["a","b"]).
If the fetched value is not a valid JSON object — a raw string, number, array, or malformed JSON — ESS stores the raw value under a single __raw key and logs a warning, so the secret stays usable as a dictionary type.

discoverAllSecrets — Mirror an entire GCP project (one secret each)

Discovers every accessible secret in the GCP project configured on the provider and creates a separate Control Plane secret for each one. This differs from dictionaryFromProject: true, which combines every secret into a single dictionary. GCP Secret Manager only, and must be set to true.
The type of each created secret is controlled by a cpln-type label on the GCP secret:
  • cpln-type: dictionary — the value is parsed as JSON and flattened into a Control Plane dictionary secret (same flattening rules as dictionaryFromJson, including the __raw fallback for non-JSON values).
  • cpln-type: opaque (or no label) — stored as a Control Plane opaque secret.
Each Control Plane secret is named after its GCP secret, normalized to a valid Control Plane name (lowercased, with unsupported characters such as _ replaced by - — e.g. MY_API_KEYmy-api-key). If two names normalize to the same value, the last one wins and a warning is logged. Created secrets carry a syncer.cpln.io/discoveredBy tag set to this entry’s name. Secrets with no accessible latest version (no versions, disabled, or destroyed) are skipped.
discoverAllSecrets is only valid with a GCP Secret Manager provider. ESS does not delete a discovered Control Plane secret when its source GCP secret is later removed.

Sync Interval

Intervals use the format <hours>h<minutes>m<seconds>s. All parts are optional but at least one is required. Examples: 10s, 5m, 1h, 1h30m, 1h30m10s. Priority (highest wins):
  1. Secret-level syncInterval
  2. Provider-level syncInterval
  3. Global default (300s)

Doppler Path Formats

Infisical Path Formats

The Infisical project is set on the provider (infisical.projectId). Secret paths are scoped to an environment within that project.

Synced Secret Output

A secret created by ESS will look like:
The syncer.cpln.io/lastError tag is empty on success. If ESS encounters an error syncing a secret, the tag is populated with the error message.

Important Notes

  • Conflict protection — If a Control Plane secret already exists and is managed by a different ESS instance, the sync for that secret will fail. Two ESS instances cannot manage the same secret.
  • Secret type changes — Changing a secret from opaque to dictionary (or vice versa) causes ESS to delete the existing secret and recreate it. There is a brief window where the secret does not exist.
  • No automatic deletion — ESS only creates and updates secrets; it never deletes them. Removing a secret from sync.yaml, or deleting a source secret upstream, leaves the existing Control Plane secret in place — delete unwanted secrets manually. ESS-managed secrets carry the syncer.cpln.io/source tag (and discovered secrets additionally carry syncer.cpln.io/discoveredBy) to help identify them.
  • Doppler parse — The parse field only works when the Doppler secret’s value is JSON or YAML. Using parse on a plain string secret throws an error.
  • Hot reload — ESS watches its config file and automatically restarts when changes are detected (every ~5 seconds). No workload restart is needed after updating the config secret.

External References

AWS Parameter Store

AWS Systems Manager Parameter Store documentation

AWS Secrets Manager

AWS Secrets Manager documentation

HashiCorp Vault

HashiCorp Vault secret management

GCP Secret Manager

Google Cloud Secret Manager documentation

1Password

1Password secret management

Doppler

Doppler secrets platform

Infisical

Infisical secret management platform

ESS Image Source

Source code for the External Secret Syncer image

ESS Template

View the source files, default values, and chart definition