Skip to content

Commit 0f7e113

Browse files
author
Jakob Nagel
committed
ESS version 1.5.0
1 parent b877faa commit 0f7e113

8 files changed

Lines changed: 476 additions & 0 deletions

File tree

ess/versions/1.7.0/Chart.yaml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
apiVersion: v2
2+
name: ess
3+
description: External Secret Syncer for Control Plane
4+
type: application
5+
version: 1.7.0
6+
appVersion: v1.5.0
7+
8+
dependencies:
9+
- name: cpln-common
10+
version: 1.0.0
11+
repository: "oci://ghcr.io/controlplane-com/templates"
12+
13+
annotations:
14+
created: "2025-03-12"
15+
lastModified: "2026-06-05"
16+
category: "secrets"
17+
createsGvc: false

ess/versions/1.7.0/README.md

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
## External Secret Syncer (ESS)
2+
3+
### Overview
4+
5+
Creates an application that continuously syncs secrets from external providers into Control Plane secrets on a configurable schedule. Supported providers: **HashiCorp Vault**, **AWS Secrets Manager**, **AWS Parameter Store**, **Doppler**, **GCP Secret Manager**, **1Password**, **1Password Connect**, and **Infisical**.
6+
7+
---
8+
9+
### How It Works
10+
11+
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.
12+
13+
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.
14+
15+
---
16+
17+
### Patch Notes
18+
19+
This version of ESS fixes a bug preventing the cleanup from running
20+
21+
### Configuring `values.yaml`
22+
23+
#### Top-level fields
24+
25+
| Field | Description |
26+
|---|---|
27+
| `image` | The ESS container image. Do not change unless upgrading. |
28+
| `resources.cpu` / `resources.memory` | Resource limits for the workload container. |
29+
| `port` | Port for the ESS HTTP admin API (default: `3004`). Used for health checks and manual sync triggers. |
30+
| `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. |
31+
| `essConfig` | The full sync configuration — providers and secrets (see below). |
32+
33+
---
34+
35+
#### `essConfig.providers`
36+
37+
Each provider entry requires a unique `name` and exactly one provider block. An optional `syncInterval` sets the default interval for all secrets using that provider.
38+
39+
**Vault**
40+
```yaml
41+
- name: my-vault
42+
vault:
43+
address: https://my-vault.com:8200 # required
44+
token: <TOKEN> # required
45+
syncInterval: 1m # optional — overrides global default
46+
```
47+
48+
**AWS Parameter Store**
49+
```yaml
50+
- name: my-aws-ssm
51+
awsParameterStore:
52+
region: us-east-1
53+
accessKeyId: <ACCESS_KEY> # optional if using an IAM-linked identity
54+
secretAccessKey: <SECRET_KEY> # optional if using an IAM-linked identity
55+
```
56+
57+
**AWS Secrets Manager**
58+
```yaml
59+
- name: my-aws-secrets-manager
60+
awsSecretsManager:
61+
region: us-east-1
62+
accessKeyId: <ACCESS_KEY>
63+
secretAccessKey: <SECRET_KEY>
64+
```
65+
66+
**Doppler**
67+
```yaml
68+
- name: my-doppler
69+
doppler:
70+
accessToken: <TOKEN> # use a Doppler service token (dp.st....)
71+
```
72+
73+
**GCP Secret Manager**
74+
```yaml
75+
- name: my-gcp
76+
gcpSecretManager:
77+
projectId: 123456789876
78+
credentials: # optional — omit to use Application Default Credentials
79+
clientEmail: <EMAIL>
80+
privateKey: <PRIVATE_KEY>
81+
```
82+
83+
**1Password**
84+
```yaml
85+
- name: my-1password
86+
onePassword:
87+
serviceAccountToken: <TOKEN>
88+
integrationName: my-ess # optional
89+
integrationVersion: 1.0.0 # optional
90+
```
91+
92+
**1Password Connect**
93+
```yaml
94+
- name: my-1password-connect
95+
onePasswordConnect:
96+
serverURL: https://my-connect-server.example.com # required
97+
token: <TOKEN> # required
98+
```
99+
100+
**Infisical**
101+
```yaml
102+
- name: my-infisical
103+
infisical:
104+
clientId: <CLIENT_ID> # required — from an Infisical machine identity
105+
clientSecret: <CLIENT_SECRET> # required
106+
projectId: <PROJECT_ID> # required
107+
```
108+
109+
---
110+
111+
#### `essConfig.secrets`
112+
113+
Each secret entry syncs one value (or a set of values) from a provider into a Control Plane secret.
114+
115+
| Field | Description |
116+
|---|---|
117+
| `name` | Name of the Control Plane secret to create or update. |
118+
| `provider` | Must match a provider `name` defined above. |
119+
| `syncInterval` | Optional. Overrides the provider-level and global default for this specific secret. |
120+
121+
Each secret must use exactly one of the following sync types:
122+
123+
---
124+
125+
##### `opaque` — Single value (stored as a Control Plane `opaque` secret)
126+
127+
Shorthand (path only, no fallback):
128+
```yaml
129+
- name: my-secret
130+
provider: my-vault
131+
opaque: /v1/secret/data/myapp
132+
```
133+
134+
With options:
135+
```yaml
136+
- name: my-secret
137+
provider: my-vault
138+
opaque:
139+
path: /v1/secret/data/myapp # path to fetch
140+
parse: data.password # optional — extract a key from a JSON/YAML response
141+
default: fallback-value # optional — used if fetch fails
142+
encoding: base64 # optional — base64-decode the fetched value
143+
```
144+
145+
> **Note:** If you use the shorthand form (`opaque: /some/path`) with no `default`, a fetch failure causes the sync to fail with no fallback.
146+
147+
---
148+
149+
##### `dictionary` — Multiple values (stored as a Control Plane `dictionary` secret)
150+
151+
Each key in the dictionary is fetched independently:
152+
```yaml
153+
- name: my-secret
154+
provider: my-vault
155+
dictionary:
156+
PORT:
157+
path: /v1/secret/data/app
158+
parse: data.port
159+
default: 5432
160+
PASSWORD:
161+
path: /v1/secret/data/app
162+
parse: data.password
163+
USERNAME:
164+
path: /v1/secret/data/app
165+
parse: data.username
166+
default: "no username"
167+
```
168+
169+
Each key supports `path`, `parse`, `default`, and `encoding` — the same options as `opaque`. A failure on one key does not block others.
170+
171+
---
172+
173+
##### `dictionaryFromProject` — Sync an entire project (Doppler or GCP Secret Manager)
174+
175+
Syncs all secrets from a provider project in one operation, stored as a Control Plane `dictionary` secret. The expected shape depends on the provider.
176+
177+
**Doppler** — specify a `project/config` path:
178+
```yaml
179+
- name: my-doppler-config
180+
provider: my-doppler
181+
dictionaryFromProject:
182+
path: my-project/dev # format: "project/config" — exactly two segments
183+
```
184+
185+
**GCP Secret Manager** — set to `true` to pull every accessible secret from the project configured on the provider:
186+
```yaml
187+
- name: my-gcp-config
188+
provider: my-gcp
189+
dictionaryFromProject: true
190+
```
191+
192+
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.
193+
194+
> **Note:** `dictionaryFromProject` is only valid with the Doppler or GCP Secret Manager providers. Doppler requires the `{ path: ... }` object form; GCP requires the `true` form. Mixing them (or using either with another provider) causes ESS to exit at startup.
195+
196+
---
197+
198+
#### Doppler Path Formats
199+
200+
| Sync type | Path format | Example |
201+
|---|---|---|
202+
| `opaque` or `dictionary` key | `project/config/SECRET_NAME` | `my-app/production/DATABASE_URL` |
203+
| `dictionaryFromProject` | `project/config` | `my-app/production` |
204+
205+
---
206+
207+
#### Infisical Path Formats
208+
209+
The Infisical project is set on the provider (`infisical.projectId`). Secret paths are scoped to an environment within that project.
210+
211+
| Sync type | Path format | Example |
212+
|---|---|---|
213+
| `opaque` or `dictionary` key | `<environmentID>/<secret>` | `dev/DATABASE_URL` |
214+
215+
---
216+
217+
#### Sync Interval Format
218+
219+
Intervals use the format `<hours>h<minutes>m<seconds>s`. All parts are optional but at least one is required.
220+
221+
Examples: `10s`, `5m`, `1h`, `1h30m`, `1h30m10s`
222+
223+
Priority (highest wins):
224+
1. Secret-level `syncInterval`
225+
2. Provider-level `syncInterval`
226+
3. Global default (`300s`)
227+
228+
---
229+
230+
### Important Notes
231+
232+
- **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.
233+
- **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.
234+
- **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.
235+
- **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.
236+
- **`sync.yaml` 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.
237+
238+
### Resources
239+
240+
- [ESS Documentation](https://docs.controlplane.com/template-catalog/templates/external-secret-syncer)
241+
- [Image Source Code](https://github.com/controlplane-com/external-secret-syncer)
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
{{/* Resource Naming */}}
2+
3+
{{/*
4+
ESS Workload Name
5+
*/}}
6+
{{- define "ess.name" -}}
7+
{{- printf "%s-ess" .Release.Name }}
8+
{{- end }}
9+
10+
{{/*
11+
ESS Identity Name
12+
*/}}
13+
{{- define "ess.identity.name" -}}
14+
{{- printf "%s-ess-identity" .Release.Name }}
15+
{{- end }}
16+
17+
{{/*
18+
ESS Policy Name
19+
*/}}
20+
{{- define "ess.policy.name" -}}
21+
{{- printf "%s-ess-policy" .Release.Name }}
22+
{{- end }}
23+
24+
{{/*
25+
ESS Secret Config Name
26+
*/}}
27+
{{- define "ess.secret.name" -}}
28+
{{- printf "%s-ess-config" .Release.Name }}
29+
{{- end }}
30+
31+
32+
{{/* Labeling */}}
33+
34+
{{/*
35+
Common labels
36+
*/}}
37+
{{- define "ess.tags" -}}
38+
{{- include "cpln-common.tags" . }}
39+
{{- end }}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
kind: identity
2+
gvc: {{ .Values.global.cpln.gvc }}
3+
name: {{ include "ess.identity.name" . }}
4+
description: ESS identity
5+
tags: {{- include "ess.tags" . | nindent 4 }}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
kind: policy
2+
name: {{ include "ess.policy.name" . }}
3+
description: ESS policy
4+
bindings:
5+
- permissions:
6+
- manage
7+
principalLinks:
8+
- //gvc/{{ .Values.global.cpln.gvc }}/identity/{{ include "ess.identity.name" . }}
9+
target: all
10+
targetKind: secret
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
kind: secret
2+
name: {{ include "ess.secret.name" . }}
3+
description: ESS config
4+
tags: {{- include "ess.tags" . | nindent 4 }}
5+
type: opaque
6+
data:
7+
encoding: plain
8+
payload: |
9+
{{- toYaml .Values.essConfig | nindent 4 }}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
kind: workload
2+
name: {{ include "ess.name" . }}
3+
description: External Secret Syncer
4+
tags: {{- include "ess.tags" . | nindent 4 }}
5+
spec:
6+
type: standard
7+
containers:
8+
- name: ess
9+
cpu: {{ .Values.resources.cpu | quote }}
10+
image: {{ .Values.image }}
11+
inheritEnv: false
12+
memory: {{ .Values.resources.memory | quote }}
13+
ports:
14+
- number: {{ .Values.port }}
15+
protocol: http
16+
readinessProbe:
17+
failureThreshold: 3
18+
httpGet:
19+
httpHeaders: []
20+
path: /about
21+
port: {{ .Values.port }}
22+
scheme: HTTP
23+
initialDelaySeconds: 0
24+
periodSeconds: 10
25+
successThreshold: 1
26+
timeoutSeconds: 1
27+
volumes:
28+
- path: /usr/src/app/sync.yaml
29+
recoveryPolicy: retain
30+
uri: cpln://secret/{{ include "ess.secret.name" . }}
31+
defaultOptions:
32+
autoscaling:
33+
maxConcurrency: 0
34+
maxScale: 3
35+
metric: cpu
36+
minScale: 1
37+
scaleToZeroDelay: 300
38+
target: 100
39+
capacityAI: false
40+
debug: false
41+
suspend: false
42+
timeoutSeconds: 5
43+
firewallConfig:
44+
external:
45+
inboundAllowCIDR:
46+
{{- toYaml .Values.allowedIp | nindent 8 }}
47+
inboundBlockedCIDR: []
48+
outboundAllowCIDR:
49+
- 0.0.0.0/0
50+
outboundAllowHostname: []
51+
outboundAllowPort: []
52+
outboundBlockedCIDR: []
53+
internal:
54+
inboundAllowType: none
55+
inboundAllowWorkload: []
56+
identityLink: //gvc/{{ .Values.global.cpln.gvc }}/identity/{{ include "ess.identity.name" . }}
57+
loadBalancer:
58+
direct:
59+
enabled: false
60+
ports: []
61+
supportDynamicTags: false

0 commit comments

Comments
 (0)