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:
2 files changed
+21
-33
No files matched your search
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Do not edit this file with editors other than draw.io -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" style="background: transparent; background-color: transparent; color-scheme: light dark;" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="1282px" height="393px" viewBox="0 0 1282 393" id="ge-svg-wXejG6j39UCpMXitsgS8" content="<mxfile host="embed.diagrams.net" scale="1" border="0"> <diagram name="Secret Lifecycle" id="secret-lifecycle-1"> <mxGraphModel dx="1664" dy="931" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1340" pageHeight="620" math="0" shadow="0"> <root> <mxCell id="0" /> <mxCell id="1" parent="0" /> <mxCell id="agent" parent="1" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;fontColor=#333333;fontSize=14;fontStyle=1;shadow=1;" value="Vault Agent&lt;br&gt;&lt;font style=&quot;font-size:11px&quot;&gt;runs on the consuming host&lt;/font&gt;" vertex="1"> <mxGeometry height="90" width="200" x="40" y="260" as="geometry" /> </mxCell> <mxCell id="vault" parent="1" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;fontColor=#333333;fontSize=14;fontStyle=1;shadow=1;" value="Vault&lt;br&gt;&lt;font style=&quot;font-size:11px&quot;&gt;server&lt;/font&gt;" vertex="1"> <mxGeometry height="90" width="200" x="380" y="260" as="geometry" /> </mxCell> <mxCell id="disk" parent="1" style="shape=cylinder3;whiteSpace=wrap;html=1;fillColor=#F5F5F5;strokeColor=#666666;fontColor=#333333;fontSize=14;fontStyle=1;shadow=1;boundedLbl=1;" value="Local Disk&lt;br&gt;&lt;font style=&quot;font-size:11px&quot;&gt;rendered file&lt;/font&gt;" vertex="1"> <mxGeometry height="110" width="160" x="660" y="250" as="geometry" /> </mxCell> <mxCell id="app" parent="1" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontColor=#333333;fontSize=14;fontStyle=1;shadow=1;" value="Application" vertex="1"> <mxGeometry height="90" width="200" x="1110" y="260" as="geometry" /> </mxCell> <mxCell id="s1" edge="1" parent="1" source="agent" style="html=1;endArrow=block;strokeColor=#9673a6;fontColor=#9673a6;fontSize=11;rounded=1;edgeStyle=orthogonalEdgeStyle;exitX=0.3;exitY=0;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" target="vault" value="&lt;span style=&quot;background-color: light-dark(rgb(255, 255, 255), rgb(41, 41, 41));&quot;&gt;① authenticate — AppRole (role_id + secret_id)&lt;/span&gt;"> <mxGeometry relative="1" as="geometry"> <Array as="points"> <mxPoint x="100" y="200" /> <mxPoint x="480" y="200" /> </Array> </mxGeometry> </mxCell> <mxCell id="s2" edge="1" parent="1" source="vault" style="html=1;endArrow=block;strokeColor=#9673a6;fontColor=#9673a6;fontSize=11;rounded=1;edgeStyle=orthogonalEdgeStyle;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.603;entryY=0.996;entryDx=0;entryDy=0;entryPerimeter=0;" target="agent" value="&lt;span style=&quot;background-color: light-dark(rgb(255, 255, 255), rgb(41, 41, 41));&quot;&gt;② short-lived token issued, cached, auto-renewed&lt;/span&gt;"> <mxGeometry relative="1" as="geometry"> <Array as="points"> <mxPoint x="480" y="375" /> <mxPoint x="160" y="375" /> <mxPoint x="160" y="349.6" /> </Array> </mxGeometry> </mxCell> <mxCell id="s3" edge="1Line truncated
|
||||
|
After Width: | Height: | Size: 327 KiB |
+17
-33
@@ -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:
|
||||
|
||||

|
||||
|
||||
**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
|
||||
|
||||
|
||||
Reference in new issue
Block a user