Skip to main content

Overview

The External Secret Syncer (ESS) keeps Control Plane secrets in step with an external secret store. It polls each configured provider on an interval and creates or updates the matching Control Plane secret, so your workloads consume ordinary Control Plane secrets while the source of truth stays in HashiCorp Vault, AWS Secrets Manager, AWS Parameter Store, GCP Secret Manager, 1Password, 1Password Connect, Doppler or Infisical. Your provider connections and the list of secrets to sync live in a sync.yaml file that you store as an opaque secret before installing. It carries provider credentials, so it is never a Helm value. Every secret ESS writes is tagged syncer.cpln.io/source with the ESS workload, and ESS refuses to overwrite a secret tagged by a different ESS instance.
This template does not create a GVC. It deploys into an existing GVC that you already have.

What Gets Created

The policy is org-wide, regardless of which secrets you sync. It grants manage on every secret in the org, not just the targets named in sync.yaml. Anything that can run as RELEASE_NAME-ess-identity can read, change and delete every secret in the org. Treat the ESS workload as security-sensitive, keep allowedIp narrow, and restrict who can edit the release, its sync.yaml secret and the workload.
The template creates no secret of its own: it mounts the secret named by configSecretName at /usr/src/app/sync.yaml. The identity carries no cloud-provider binding, so every provider authenticates with the credentials you put in sync.yaml.

Prerequisites

One opaque secret holding your entire sync.yaml must exist before you install. Secrets are org-level, so no GVC flag is involved.
1

Gather read-only provider credentials

ESS only reads from your providers. Create a credential scoped to the secrets you want synced:
2

Write your sync.yaml

A minimal file that syncs one Vault value into an opaque Control Plane secret. The full schema is under Configuration.
3

Store it as an opaque secret

Set configSecretName to this name. Secret names are org-wide, so give each release its own. Read it back any time with:
If the secret does not exist at install time, the deployment wedges silently. cpln logs returns zero lines, because the container never starts. The missing secret is named only in status.versions[].message:
Use get-deployments; plain cpln workload get has no versions key. The deployment recovers on its own once the secret exists, or force a redeployment to skip the wait.

Installation

Once the secret exists, install from the marketplace registry with its name:
Or 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

Configuration

Image and Resources

resources sets the container limits. Change image only when moving to a newer ESS release.

Admin API Access

port is the ESS admin API, used by the /about health check. allowedIp lists the CIDRs that may reach the admin API on its public endpoint; the shipped 1.2.3.4 is a placeholder that admits nobody. Other workloads in the GVC cannot reach the admin API.

Sync Configuration Secret

configSecretName names the opaque secret holding your sync.yaml (see Prerequisites). The rest of this section is the schema of that file: a providers list and a secrets list.

Providers

Each provider has a unique name and exactly one provider block. An optional syncInterval sets the default interval for every secret that uses it.
The template’s identity has no AWS or GCP binding, so supply the access keys or service-account credentials shown above for the AWS and GCP providers.

Secrets

Each entry in secrets writes one Control Plane secret. It needs a name (the Control Plane secret to create or update), a provider that matches a provider name, an optional syncInterval, and exactly one of the sync types below. opaque stores one value as an opaque secret. Use the shorthand when you need only a path:
Or the long form for options:
Vault KV secrets are nested under a data key, so parse paths for Vault start with data.. With the shorthand and no default, a failed fetch fails the sync with no fallback. dictionary stores several values as a dictionary secret. Each key is fetched independently, supports the same path, parse, default and encoding options, and a failure on one key does not block the others:
dictionaryFromProject syncs a whole provider project into one dictionary secret. Doppler takes a project/config path; GCP Secret Manager takes true and pulls every accessible secret from the provider’s project, one key per secret:
GCP secrets with no accessible latest version (no versions, disabled or destroyed) are skipped. A form that does not match its provider stops ESS at startup. dictionaryFromJson fetches one value holding a JSON object and flattens it into a dictionary secret. It works with any provider:
Nested objects become dot-notation keys (db.host), arrays are stored as their JSON string, and null, numbers and booleans are stored as strings. A value that is not a JSON object is stored under a single __raw key, and ESS logs a warning. discoverAllSecrets (GCP Secret Manager only, must be true) creates a separate Control Plane secret for every accessible secret in the provider’s project:
  • A cpln-type: dictionary label on the GCP secret flattens its JSON value into a dictionary secret (same rules as dictionaryFromJson); cpln-type: opaque or no label stores an opaque secret.
  • Opaque values that are canonical base64 are marked encoding: base64, so workloads receive the decoded value. Label the GCP secret cpln-encoding: disable to store a value verbatim, for example an API key that happens to look like base64.
  • Names are normalized to valid Control Plane names (lowercased, unsupported characters such as _ replaced by -, so MY_API_KEY becomes my-api-key). If two names collide, the last one wins and ESS logs a warning.
  • Each created secret carries a syncer.cpln.io/discoveredBy tag set to the entry’s name.

Path Formats

Sync Interval

Intervals use <hours>h<minutes>m<seconds>s; every part is optional but at least one is required (10s, 5m, 1h30m). The most specific setting wins: the secret’s syncInterval, then the provider’s, then the default of 300s.

Connecting

ESS has no client endpoint of its own. Workloads consume the secrets it writes by name, exactly like any other Control Plane secret, with no dependency on ESS being reachable. To confirm a sync happened, read the secret back and check its tags and data:
A synced secret looks like this:
The syncer logs each secret it schedules and any error it hits, by secret name:

Operations

Updating the Sync Configuration

To add a provider, add a secret or replace a credential, export the secret, edit sync.yaml in its payload, and apply it:
Then force a redeployment so the running syncer mounts the new file:
Removing an entry from sync.yaml stops syncing it but leaves the Control Plane secret it created in place.

Upgrading From 2.0.x

Version 2.1.0 removes the essConfig values block. The schema is unchanged: migrating is a verbatim lift of that block into sync.yaml, minus the top-level essConfig: wrapper. Version 2.0.x does not create a GVC, so this is an ordinary in-place upgrade of the same release.
Carrying essConfig forward stops the upgrade. It is rejected at render, so cpln helm upgrade fails and your existing syncer keeps running untouched:
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 under a new name

Do not reuse RELEASE_NAME-ess-config: that is the secret 2.0.x created, and the upgrade removes it.
3

Upgrade with essConfig removed

Delete essConfig from your values file, then upgrade:
4

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 as described in Updating the Sync Configuration.

Troubleshooting

Cause: the secret named by configSecretName does not exist, so the container never starts.Fix: read status.versions[].message with cpln workload get-deployments RELEASE_NAME-ess --gvc GVC_NAME -o yaml, create the secret it names (see Prerequisites), and wait for the deployment to recover or force a redeployment.
Cause: sync.yaml does not match the schema. Common messages are Secrets must only reference one secret type, Provider must have exactly one provider, Secrets must have a valid provider and Provider names must be unique.Fix: correct the file against Sync Configuration Secret, re-apply the secret and force a redeployment.
Cause: ESS could not fetch the value from the provider, for example a wrong path, an expired token or missing read access. On Doppler, parse on a value that is not JSON or YAML also fails.Fix: read the message from the tag with cpln secret reveal SYNCED_SECRET_NAME -o yaml, fix the path or credential in sync.yaml, and apply it. The tag clears on the next successful sync.
Cause: the target secret’s syncer.cpln.io/source tag names a different ESS workload, for example a release installed earlier under another name.Fix: sync into a different secret name, or delete the existing secret so this ESS can create it.
Cause: allowedIp still holds the 1.2.3.4 placeholder, or your address is not in it.Fix: set allowedIp to your CIDR and upgrade, or reach the API privately with cpln port-forward RELEASE_NAME-ess 3004:3004 --gvc GVC_NAME.

Important Notes

  • Create the sync.yaml secret before installing, and force a redeployment after every change to it.
  • Two ESS instances cannot manage the same secret; give each release its own target secret names.
  • ESS never deletes a secret when its entry or upstream source is removed. Removing an entry from sync.yaml, or deleting the source upstream, leaves the Control Plane secret in place; delete it yourself.
  • Changing a secret between opaque and dictionary makes ESS delete and recreate it, so it briefly does not exist.
  • Version 2.1.0 removed essConfig; follow Upgrading From 2.0.x before upgrading an older release.

External References