secrets-and-pki: verify against Trilium, replace diagrams with real content

Removed the before/after plaintext-secrets Mermaid diagram (the bullets
right below it already said the same thing more clearly) and the
Architecture flowchart (its per-host labels were silently missing
registry-token and Hawser-secret details that didn't fit in the boxes).

Replaced the flowchart with an accurate, complete "who gets what" table
sourced directly from Trilium, one sentence for the proxy-routing fact
it was trying to show, and a new sequence diagram for the actual secret
lifecycle (auth -> render -> read -> async rotation) - a genuinely
different kind of diagram since that's process/time-ordered information
a table can't represent well.

Also restored real specificity Trilium had that the doc had flattened
into vague language: the cert-renewal row now names Technitium
specifically and states the actual incident (an expired cert silently
served for 19 days before the auto-rebuild hook existed), and the audit
trail row now states the real rotation policy (weekly, 8 generations).
This commit is contained in:
thesabear committed 2026-08-25 23:30:58 +03:00
1 parent 5fddb5b8f7
commit 818892c8a5
2 files changed
+21 -33

No files matched your search

+17 -33
View File
@@ -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:<br/>plaintext secrets<br/>in git-tracked .env files<br/>readable by anyone with repo access,<br/>permanent in git history"] -->|replaced| After
subgraph After["Now"]
direction TB
T["Git-tracked template<br/>placeholder syntax only"]
V["Vault<br/>real value, one place"]
R["Rendered file<br/>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<br/>PKI (public + internal mounts) + KV v2 store<br/>TLS listener, cert-verified · audit log"]
Proxy["Internal app-to-app reverse-proxy zone<br/>see docs/dns-naming.md"]
A1["Vault Agent — App Host<br/>~12 stacks' secrets + internal CA cert"]
A2["Vault Agent — Automation Host<br/>~10 stacks' secrets"]
A3["Vault Agent — Monitoring Host<br/>admin credentials + OIDC secret"]
A4["Vault Agent — Edge Proxy Host<br/>mTLS zone certs"]
A5["Vault Agent — DNS Host<br/>admin UI cert"]
A6["Vault Agent — Security Monitoring Host<br/>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