Overview
How Wardn handles secrets — three layers, one unified interface
Secret Management Overview
Secrets are hard in GitOps — you can't commit plaintext secrets to Git, but Git is your source of truth. Wardn handles this through secret stores configured on the server plus three consumption paths — all manageable from a single UI.
The Three Layers
┌─────────────────────────────────────────────────────┐
│ Server (Postgres) │
│ │
│ secret_stores table — sealed credentials │
│ ├─ vault/openbao ├─ sops/age └─ eso │
└───────┬────────────┬──────────────┬─────────────────┘
│ │ │
▼ ▼ ▼
env.store 📋 decrypt-at- env.store
(vault fetch apply pipeline (renders
at deploy) (not wired) ExternalSecret)| Layer | Best For | Status |
|---|---|---|
| Vault / OpenBao | Teams wanting Wardn to fetch values at deploy time | ✅ — env.store on image sources resolves {id,path,key} from the store at apply |
| SOPS | Teams encrypting files in Git | 🚧 — sops/age store exists (encrypt/decrypt round-trip Test); decrypt-at-apply isn't wired |
| External Secrets Operator | Teams with existing secret managers (AWS SM, GCP SM, Vault) | ✅ — env.store entries on image sources render ExternalSecret objects; the ESO CRD is applied like any manifest |
Layer 1: Vault / OpenBao
A vault-type secret store (providers vault / openbao) lets image-source env vars pull values at deploy time:
spec:
source:
type: Image
image:
repository: ghcr.io/acme/api
workload:
env:
- name: DB_PASSWORD
store: # secret-store reference
id: sts_01H8…
path: prod/my-app/db
key: passwordAt render, the operator calls the store's Fetch(ctx, "prod/my-app/db") and injects password as a literal env value — no K8s Secret object, no ESO hop. The vault Fetch reads KV v2.
Best when: env values need to come from Vault and you don't want per-key Secret objects.
Layer 2: SOPS
The sops/age store registers an age private key — Test does an encrypt/decrypt round-trip to prove it works. What's missing is the apply-time hook: the renderer doesn't yet decrypt .sops.yaml-encrypted manifests on their way into the cluster.
curl -X POST http://localhost:8080/api/v1/secret-stores -b cookies.txt \
-d '{"name":"dev-age","type":"sops","provider":"age","credentials":{"privateKey":"AGE-SECRET-KEY-…"}}'Best when (today): validating that an age key works and preparing the store for the decrypt-at-apply pipeline.
Layer 3: External Secrets Operator
ESO syncs secrets from external providers into K8s Secrets. Two integration paths:
- Git-source apps — commit
ExternalSecret/SecretStoremanifests; the operator applies them like any resource (they show asHealthyonce they exist) - Image-source apps —
env.storeon a workload rendersExternalSecretobjects pointing at the referenced store path
spec:
source:
type: Image
image:
workload:
env:
- name: API_KEY
store:
id: sts_eso…
path: prod/api/key
key: api-keyBest when: secrets live in a dedicated manager and you want ESO to own the K8s Secret lifecycle.
Unified UI
- Secret Stores page — configure and test vault/sops/eso stores; provider schemas drive the form
- Applications —
env.storeentries on image sources show which store + path + key each env var resolves from - Health — applied
ExternalSecretobjects get the defaultHealthy-if-existsverdict like any other kind
Security Principles
- Never plaintext in Git —
env.storereferences + ESO keep values out of the repo - Sealed at rest — store credentials encrypt to
enc:v1:AES-256-GCM blobs keyed byWARDN_CREDENTIALS_KEY - Write-only — credentials never come back in API responses
- Session-only management —
secretstore:managerejects API-key callers
Choosing an Approach
| Criteria | Vault direct | SOPS | ESO |
|---|---|---|---|
| Secret values in Git | No | Encrypted (planned) | No |
| Extra operator | No | No | Yes (ESO) |
| Works today for | Image env injection | Store validation | Image env + git manifests |
| Rotation | Provider-side | Re-encrypt files | Provider-side |