Secret Management

SOPS

age key secret store β€” decrypt-at-apply planned

SOPS β€” 🚧 store ready, decrypt-at-apply planned

SOPS encrypts values in YAML/JSON files while keeping keys readable. Wardn has a sops/age secret store for the key side β€” what isn't wired yet is the apply-time decrypt in the manifest pipeline.

Status

PieceStatus
sops/age secret storeβœ… registered β€” Test does an encrypt/decrypt round-trip
Apply-time decrypt of .sops.yaml filesπŸ“‹ not wired β€” encrypted manifests apply as-is
KMS / GCP KMS / Vault Transit / PGP providersπŸ“‹ not registered β€” 400 on create

Today the store validates keys, nothing more. If you commit a .sops.yaml-encrypted Secret, the operator applies the ENC[…] values literally. Keep plaintext-out-of-git via env.store (vault/ESO) until decrypt-at-apply lands.

Configure the store

Via the API

curl -X POST http://localhost:8080/api/v1/secret-stores -b cookies.txt \
  -H "Content-Type: application/json" \
  -d '{
    "name": "dev-age",
    "type": "sops",
    "provider": "age",
    "credentials": {"privateKey": "AGE-SECRET-KEY-1XYZ…"}
  }'

Via the UI

Secret Stores β†’ Add Store β†’ sops / age β€” paste the age private key; the provider schema drives the form.

POST /api/v1/secret-stores/{id}/test runs an actual encrypt/decrypt round-trip β€” connected means the key works end-to-end.

Intended apply-time flow (πŸ“‹)

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  encrypted  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  decrypt   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Developer  │────────────▢│   Git    │───────────▢│   Operator   β”‚
β”‚  (sops -e)  β”‚             β”‚  Repo    β”‚  at apply  β”‚ (store key)  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜             β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜            β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜
                                                           β”‚
                                                           β–Ό
                                                   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                                                   β”‚  K8s Secret  β”‚
                                                   β”‚  (plaintext) β”‚
                                                   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

When wired, the manifest pipeline will detect sops:-marked files under source.git.path and decrypt them against the registered age store before apply.

Working with SOPS today (client-side)

You can still encrypt and version secrets in Git β€” the decrypt happens on your side:

Step 1: Generate an age key

age-keygen -o key.txt
# public key printed to stderr: age1abc…

Step 2: Encrypt the secret file

sops --encrypt --age age1abc… --in-place manifests/secret.yaml

Step 3: Register the key with Wardn

curl -X POST http://localhost:8080/api/v1/secret-stores -b cookies.txt \
  -H "Content-Type: application/json" \
  -d '{"name":"dev-age","type":"sops","provider":"age","credentials":{"privateKey":"…"}}'

Step 4: Use .sops.yaml creation rules

creation_rules:
  - path_regex: manifests/production/.*\.yaml
    age: age1prod…
  - age: age1dev…

sops -e --in-place then picks the right key per path.

What works vs what doesn't

CapabilityStatus
Store an age private keyβœ… sealed via WARDN_CREDENTIALS_KEY
Round-trip Testβœ…
Auto-detect + decrypt on applyπŸ“‹
sops-aware health (Secret exists with ENC[…] values β†’ warn)πŸ“‹

Security Notes

  • Never commit the private key β€” .sops.yaml goes to git; key.txt never does
  • Sealed at rest β€” the store row encrypts to enc:v1: keyed by WARDN_CREDENTIALS_KEY
  • Session-only writes β€” creating/rotating the store requires a session, not an API key

On this page