Secret Management

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)
LayerBest ForStatus
Vault / OpenBaoTeams wanting Wardn to fetch values at deploy time✅ — env.store on image sources resolves {id,path,key} from the store at apply
SOPSTeams encrypting files in Git🚧 — sops/age store exists (encrypt/decrypt round-trip Test); decrypt-at-apply isn't wired
External Secrets OperatorTeams 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: password

At 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/SecretStore manifests; the operator applies them like any resource (they show as Healthy once they exist)
  • Image-source apps — env.store on a workload renders ExternalSecret objects 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-key

Best 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.store entries on image sources show which store + path + key each env var resolves from
  • Health — applied ExternalSecret objects get the default Healthy-if-exists verdict like any other kind

Security Principles

  1. Never plaintext in Git — env.store references + ESO keep values out of the repo
  2. Sealed at rest — store credentials encrypt to enc:v1: AES-256-GCM blobs keyed by WARDN_CREDENTIALS_KEY
  3. Write-only — credentials never come back in API responses
  4. Session-only management — secretstore:manage rejects API-key callers

Choosing an Approach

CriteriaVault directSOPSESO
Secret values in GitNoEncrypted (planned)No
Extra operatorNoNoYes (ESO)
Works today forImage env injectionStore validationImage env + git manifests
RotationProvider-sideRe-encrypt filesProvider-side

On this page