Secret Stores
Configure Vault/OpenBao, SOPS age keys, and ESO references via the API
Secret Stores API
Secret stores tell Wardn how to reach your secret providers. Each store is a type/provider pair backed by a registered provider — credentials are sealed at rest (enc:v1: AES-256-GCM via WARDN_CREDENTIALS_KEY) and never returned by the API.
Providers
The registry is self-describing — GET /secret-stores/providers returns every type/provider with its config + credential field schema so the UI renders forms without hardcoding:
type | provider | Status | Notes |
|---|---|---|---|
vault | vault | ✅ | HashiCorp Vault — token or AppRole auth; Test does lookup-self, Fetch reads KV v2 for direct env injection |
vault | openbao | ✅ | Same implementation, separate registration |
sops | age | ✅ | Age private key — Test does an encrypt/decrypt round-trip |
eso | eso | 🚧 | Registered for reference; Test returns untested — ESO manages its own credentials in-cluster |
sops | aws-kms/gcp-kms/pgp/vault-transit | 📋 | Not registered — 400 on create |
vault stores validate config server-side — public endpoints require https:// when the server runs in production mode. Test/Fetch calls are rate-limited per user.
Endpoints
| Method | Path | Permission | Description |
|---|---|---|---|
GET | /api/v1/secret-stores/providers | secretstore:read | Provider schemas for form rendering |
GET | /api/v1/secret-stores | secretstore:read | List stores — {items:[…]} |
GET | /api/v1/secret-stores/{id} | secretstore:read | One store |
GET | /api/v1/secret-stores/{id}/references | secretstore:read | App references — 📋 always returns {items:[]} (not wired) |
POST | /api/v1/secret-stores | secretstore:manage | Create — {name,type,provider,config,credentials} |
PATCH | /api/v1/secret-stores/{id} | secretstore:manage | Update name/config/credentials — omitted fields unchanged |
DELETE | /api/v1/secret-stores/{id} | secretstore:manage | Delete |
POST | /api/v1/secret-stores/test | secretstore:manage | Test an unsaved {type,provider,config,credentials} before creating |
POST | /api/v1/secret-stores/{id}/test | secretstore:manage | Re-test a saved store (rate-limited) |
secretstore:manage is session-only — API keys cannot create or modify stores, so a leaked wrd_ key can't register credentials.
The store object
{
"id": "sts_01H8X2K3…",
"name": "prod-vault",
"type": "vault",
"provider": "vault",
"status": "connected",
"statusDetail": "",
"lastTestedAt": "2026-01-15T10:00:00Z",
"credentialsExpireAt": "2026-01-15T11:00:00Z",
"config": { "address": "https://vault.example.com:8200", "mountPath": "secret" },
"references": 0,
"createdAt": "…",
"updatedAt": "…"
}status is connected | error | untested (errorClass on the test result buckets failures: dns/tls/connect/timeout/auth/permission/notfound/provider). credentialsExpireAt surfaces provider-reported TTLs (e.g. Vault token expiry) so the UI can warn before auth breaks.
Create examples
# Vault — token auth
curl -X POST http://localhost:8080/api/v1/secret-stores -b cookies.txt \
-H "Content-Type: application/json" -d '{
"name": "prod-vault", "type": "vault", "provider": "vault",
"config": {"address": "https://vault.example.com:8200", "mountPath": "secret"},
"credentials": {"token": "hvs.…"}}'
# SOPS age key
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…"}}'201 → the store object (credentials stripped). 409 on duplicate name; 400 on failed validation (including SSRF checks on the address).
Test a store
curl -X POST http://localhost:8080/api/v1/secret-stores/sts_01H8…/test -b cookies.txt{ "status": "connected", "latency": "118ms", "details": "authenticated; token ttl 1h" }A failed test sets the store's status to error and returns the structured result:
{ "status": "error", "error": "authentication failed", "errorClass": "auth" }Related
- Secret Management Overview — the three layers
- SOPS — age keys and the decrypt-at-apply plan
- Vault Direct — what's implemented vs planned
- Audit API —
secretstore.*actions