diff --git a/assets/secrets-lifecycle.svg b/assets/secrets-lifecycle.svg new file mode 100644 index 0000000..6cc65de --- /dev/null +++ b/assets/secrets-lifecycle.svg @@ -0,0 +1,4 @@ + + + +
Vault Agent
runs on the consuming host
Vault
server
Local Disk
rendered file
Application
① authenticate — AppRole (role_id + secret_id)
② short-lived token issued, cached, auto-renewed
③ secret/cert requested + rendered to file (0600, root-owned, gitignored)
④ app reads the local file — never talks to Vault directly
⑤ rotation (async): update once in Vault → picked up on next render, no redeploy
Solid = the initial bootstrap sequence, in order. Dashed = the separate, periodic rotation cycle — asynchronous, never a blocking dependency for the application.
\ No newline at end of file diff --git a/docs/secrets-and-pki.md b/docs/secrets-and-pki.md index 8fb8d67..8ec2340 100644 --- a/docs/secrets-and-pki.md +++ b/docs/secrets-and-pki.md @@ -23,18 +23,7 @@ A managed secrets service (a cloud provider's KMS/Secrets Manager) was a real al ## Why it's built this way -```mermaid -flowchart LR - Before["Before:
plaintext secrets
in git-tracked .env files
readable by anyone with repo access,
permanent in git history"] -->|replaced| After - - subgraph After["Now"] - direction TB - T["Git-tracked template
placeholder syntax only"] - V["Vault
real value, one place"] - R["Rendered file
gitignored, 0600, root-owned"] - V -->|renders| R - end -``` +Before this system existed, every stack's secrets sat in plaintext inside git-tracked `.env` files — readable by anyone with repo access, and permanently baked into git history even after being "fixed." SecureVault replaces that: - The real secret value lives in exactly one place: Vault. - What's git-tracked is a template with placeholder syntax, never a real value. @@ -46,25 +35,20 @@ flowchart LR ## Architecture -```mermaid -flowchart TD - Vault["Vault
PKI (public + internal mounts) + KV v2 store
TLS listener, cert-verified · audit log"] - Proxy["Internal app-to-app reverse-proxy zone
see docs/dns-naming.md"] - A1["Vault Agent — App Host
~12 stacks' secrets + internal CA cert"] - A2["Vault Agent — Automation Host
~10 stacks' secrets"] - A3["Vault Agent — Monitoring Host
admin credentials + OIDC secret"] - A4["Vault Agent — Edge Proxy Host
mTLS zone certs"] - A5["Vault Agent — DNS Host
admin UI cert"] - A6["Vault Agent — Security Monitoring Host
manager/indexer/dashboard secrets"] +Every Vault Agent reaches Vault through the internal app-to-app proxy zone described in [`docs/dns-naming.md`](dns-naming.md) — never directly. It's the one hop in that entire zone where the proxy actually verifies the backend's certificate instead of skipping it, which is exactly the asymmetric-trust decision explained below. - Vault -->|HTTPS, cert-verified| Proxy - Proxy --> A1 - Proxy --> A2 - Proxy --> A3 - Proxy --> A4 - Proxy --> A5 - Proxy --> A6 -``` +| Consumer | What it gets from Vault | +|---|---| +| App Host | 12 stacks' worth of application secrets, an internal CA cert to verify Vault's own listener, plus a shared registry-token render | +| Automation Host | 10 stacks' worth of application secrets, plus a shared registry-token render | +| Monitoring Host | Grafana's admin password and the Authentik OIDC secret, plus Hawser's application secret, plus a shared registry-token render | +| User-Facing Proxy | Server + client certs for the mTLS-enforced browser zone, plus a shared registry-token render and Hawser's application secret | +| Internal DNS | Server cert for its own admin UI/API only | +| Security Monitoring | Its manager and dashboard services' shared secrets (indexer, API, dashboard passwords), plus Hawser's application secret, plus a shared registry-token render | + +None of these ever call the Vault API themselves — every one of them goes through the same local-render sequence: + +![Secret lifecycle: Vault Agent authenticates via AppRole, gets a token, renders the secret to a local file, the application reads that file and never talks to Vault directly — rotation happens asynchronously, updated once in Vault and picked up on the agent's next render](../assets/secrets-lifecycle.svg) **Two authentication flows, not one:** @@ -81,10 +65,10 @@ flowchart TD | Layer | Protection | |---|---| | Transport | Every Vault Agent ↔ Vault connection is TLS, certificate-verified | -| Secrets at rest | KV v2, encrypted by Vault's storage layer; rendered files tightly permissioned, root-owned, gitignored | -| Audit trail | Every request/response logged; secret values HMAC-hashed, not plaintext — what's visible is metadata: which token, which path, when | +| Secrets at rest | KV v2, encrypted by Vault's storage layer; rendered files are `0600`, root-owned, gitignored | +| Audit trail | Every request/response logged to a file audit device; secret values HMAC-hashed, not plaintext — what's visible is metadata: which token, which path, when. Rotated weekly, 8 generations kept. | | Alerting | Root-token use or permission-denied bursts trigger an alert through [`docs/notifications.md`](notifications.md)'s pipeline | -| Cert renewal reliability | A dependent service's TLS bundle auto-rebuilds when its cert re-issues, guarded to skip unless under a week from expiry | +| Cert renewal reliability | Technitium's DNS-over-TLS/DoH cert bundle auto-rebuilds whenever Vault Agent re-renders its certs, guarded to skip unless under 7 days from expiry. This closed a real prior gap: the rebuild never used to happen automatically, and an expired cert was silently served for 19 days before anyone caught it. | ## Known gaps, ranked by actual risk — not a flat list