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.

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. An hourly cleanup job also deletes any Control Plane secrets that ESS owns but that have been removed from your sync.yaml config. 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.

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 Icon Streamline Icon: https://streamlinehq.com

Pulumi

Declare templates in your Pulumi programs

Configuration

The default values.yaml for this template:
image: ghcr.io/controlplane-com/cpln-build/external-secret-syncer:v1.3.1

resources:
  cpu: 200m
  memory: 256Mi

port: 3004

allowedIp:
  - 1.2.3.4 # Replace with your IP

essConfig:
  providers:
    - name: my-vault
      vault:
        address: https://my-vault.com:8200
        token: <TOKEN>
      syncInterval: 1m
    - name: my-aws-ssm
      awsParameterStore:
        region: us-east-1
        accessKeyId: <ACCESS_KEY> # alternatively configure identity to natively use AWS permissions
        secretAccessKey: <SECRET_ACCESS_KEY> # alternatively configure identity to natively use AWS permissions
    # - name: my-aws-secrets-manager
    #   awsSecretsManager:
    #     region: us-east-1
    #     accessKeyId: <ACCESS_KEY>
    #     secretAccessKey: <SECRET_ACCESS_KEY>
    # - name: my-1password
    #   onePassword:
    #     serviceAccountToken: <TOKEN>
    #     integrationName: my-ess <optional - defaults to syncer.cpln.io>
    #     integrationVersion: 1.0.0 <optional - defaults to image tag>
    # - name: my-doppler
    #   doppler:
    #     accessToken: <TOKEN>
    # - name: my-gcp
    #   gcpSecretManager:
    #     projectId: 123456789876
    #     credentials:
    #       clientEmail: <EMAIL_ADDRESS>
    #       privateKey: <PRIVATE_KEY>
  secrets:
    - name: auth
      provider: my-vault
      syncInterval: 20s
      dictionary:
        PORT:
          path: /v1/secret/data/app
          parse: data.port
          default: 5432
        PASSWORD:
          path: /v1/secret/data/app
          parse: data.password
        USERNAME:
          default: "no username"
          path: /v1/secret/data/app
          parse: data.username
    - name: ssm
      provider: my-aws
      syncInterval: 20s
      opaque: /example/app
    # - name: secrets-manager
    #   provider: my-aws-secrets-manager
    #   dictionary:
    #     PASSWORD:
    #       path: /example/app
    #       parse: password
    # - name: doppler-secret
    #   provider: my-doppler
    #   opaque: /project/config/SECRET_NAME
    # - name: doppler-project
    #   provider: my-doppler
    #   dictionaryFromProject:
    #     path: project/config   # syncs all secrets from a Doppler project+config
    # - name: gcp
    #   provider: my-gcp
    #   opaque: database-password

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.
  • essConfig — The full sync configuration — providers and secrets (see below).

Providers

Each entry in essConfig.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.
ProviderRequired Fields
HashiCorp Vaultvault.address, vault.token
AWS Parameter StoreawsParameterStore.region
AWS Secrets ManagerawsSecretsManager.region
GCP Secret ManagergcpSecretManager.projectId
1PasswordonePassword.serviceAccountToken
Dopplerdoppler.accessToken
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:
# HashiCorp Vault
- name: my-vault
  vault:
    address: https://my-vault.com:8200
    token: <TOKEN>
  syncInterval: 1m

# AWS Parameter Store
- name: my-aws-ssm
  awsParameterStore:
    region: us-east-1
    accessKeyId: <ACCESS_KEY>       # optional if using an IAM-linked identity
    secretAccessKey: <SECRET_KEY>   # optional if using an IAM-linked identity

# AWS Secrets Manager
- name: my-aws-secrets-manager
  awsSecretsManager:
    region: us-east-1
    accessKeyId: <ACCESS_KEY>
    secretAccessKey: <SECRET_KEY>

# GCP Secret Manager
- name: my-gcp
  gcpSecretManager:
    projectId: 123456789876
    credentials:                    # optional — omit to use Application Default Credentials
      clientEmail: <EMAIL>
      privateKey: <PRIVATE_KEY>

# 1Password
- name: my-1password
  onePassword:
    serviceAccountToken: <TOKEN>
    integrationName: my-ess         # optional
    integrationVersion: 1.0.0       # optional

# Doppler
- name: my-doppler
  doppler:
    accessToken: <TOKEN>            # use a Doppler service token (dp.st....)

Secrets

Each entry in essConfig.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):
- name: my-secret
  provider: my-vault
  opaque: /v1/secret/data/myapp
With options:
- name: my-secret
  provider: my-vault
  opaque:
    path: /v1/secret/data/myapp    # path to fetch
    parse: data.password           # optional — extract a key from a JSON/YAML response
    default: fallback-value        # optional — used if fetch fails
    encoding: base64               # optional — base64-decode the fetched value before storing
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.
- name: auth
  provider: my-vault
  dictionary:
    PORT:
      path: /v1/secret/data/app
      parse: data.port
      default: 5432
    PASSWORD:
      path: /v1/secret/data/app
      parse: data.password
    USERNAME:
      path: /v1/secret/data/app
      parse: data.username
      default: "no username"
A failure on one key does not block the others.

dictionaryFromProject — Entire Doppler project

Syncs all secrets from a Doppler project+config in one operation, stored as a Control Plane dictionary secret. Only valid with a Doppler provider.
- name: my-doppler-config
  provider: my-doppler
  dictionaryFromProject:
    path: my-project/dev    # format: "project/config" — exactly two segments

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

Sync typePath formatExample
opaque or dictionary keyproject/config/SECRET_NAMEmy-app/production/DATABASE_URL
dictionaryFromProjectproject/configmy-app/production

Synced Secret Output

A secret created by ESS will look like:
kind: secret
name: hello
description: hello
tags:
  syncer.cpln.io/lastError: '' # populated if ESS encounters an error
  syncer.cpln.io/source: //gvc/<gvc name>/workload/<ess workload name>
type: dictionary
data:
  PORT: '1234'
  PASSWORD: 'no pass' # default used if the key was not found
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.
  • Cleanup — ESS runs an hourly job that deletes Control Plane secrets it owns but that no longer appear in sync.yaml. Removing a secret from the config will eventually result in its deletion from Control Plane.
  • 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

ESS Image Source

Source code for the External Secret Syncer image

ESS Template

View the source files, default values, and chart definition