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

+4
View File
@@ -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="&lt;mxfile host=&quot;embed.diagrams.net&quot; scale=&quot;1&quot; border=&quot;0&quot;&gt;&#10; &lt;diagram name=&quot;Secret Lifecycle&quot; id=&quot;secret-lifecycle-1&quot;&gt;&#10; &lt;mxGraphModel dx=&quot;1664&quot; dy=&quot;931&quot; grid=&quot;1&quot; gridSize=&quot;10&quot; guides=&quot;1&quot; tooltips=&quot;1&quot; connect=&quot;1&quot; arrows=&quot;1&quot; fold=&quot;1&quot; page=&quot;1&quot; pageScale=&quot;1&quot; pageWidth=&quot;1340&quot; pageHeight=&quot;620&quot; math=&quot;0&quot; shadow=&quot;0&quot;&gt;&#10; &lt;root&gt;&#10; &lt;mxCell id=&quot;0&quot; /&gt;&#10; &lt;mxCell id=&quot;1&quot; parent=&quot;0&quot; /&gt;&#10; &lt;mxCell id=&quot;agent&quot; parent=&quot;1&quot; style=&quot;rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;fontColor=#333333;fontSize=14;fontStyle=1;shadow=1;&quot; value=&quot;Vault Agent&amp;lt;br&amp;gt;&amp;lt;font style=&amp;quot;font-size:11px&amp;quot;&amp;gt;runs on the consuming host&amp;lt;/font&amp;gt;&quot; vertex=&quot;1&quot;&gt;&#10; &lt;mxGeometry height=&quot;90&quot; width=&quot;200&quot; x=&quot;40&quot; y=&quot;260&quot; as=&quot;geometry&quot; /&gt;&#10; &lt;/mxCell&gt;&#10; &lt;mxCell id=&quot;vault&quot; parent=&quot;1&quot; style=&quot;rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;fontColor=#333333;fontSize=14;fontStyle=1;shadow=1;&quot; value=&quot;Vault&amp;lt;br&amp;gt;&amp;lt;font style=&amp;quot;font-size:11px&amp;quot;&amp;gt;server&amp;lt;/font&amp;gt;&quot; vertex=&quot;1&quot;&gt;&#10; &lt;mxGeometry height=&quot;90&quot; width=&quot;200&quot; x=&quot;380&quot; y=&quot;260&quot; as=&quot;geometry&quot; /&gt;&#10; &lt;/mxCell&gt;&#10; &lt;mxCell id=&quot;disk&quot; parent=&quot;1&quot; style=&quot;shape=cylinder3;whiteSpace=wrap;html=1;fillColor=#F5F5F5;strokeColor=#666666;fontColor=#333333;fontSize=14;fontStyle=1;shadow=1;boundedLbl=1;&quot; value=&quot;Local Disk&amp;lt;br&amp;gt;&amp;lt;font style=&amp;quot;font-size:11px&amp;quot;&amp;gt;rendered file&amp;lt;/font&amp;gt;&quot; vertex=&quot;1&quot;&gt;&#10; &lt;mxGeometry height=&quot;110&quot; width=&quot;160&quot; x=&quot;660&quot; y=&quot;250&quot; as=&quot;geometry&quot; /&gt;&#10; &lt;/mxCell&gt;&#10; &lt;mxCell id=&quot;app&quot; parent=&quot;1&quot; style=&quot;rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontColor=#333333;fontSize=14;fontStyle=1;shadow=1;&quot; value=&quot;Application&quot; vertex=&quot;1&quot;&gt;&#10; &lt;mxGeometry height=&quot;90&quot; width=&quot;200&quot; x=&quot;1110&quot; y=&quot;260&quot; as=&quot;geometry&quot; /&gt;&#10; &lt;/mxCell&gt;&#10; &lt;mxCell id=&quot;s1&quot; edge=&quot;1&quot; parent=&quot;1&quot; source=&quot;agent&quot; style=&quot;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;&quot; target=&quot;vault&quot; value=&quot;&amp;lt;span style=&amp;quot;background-color: light-dark(rgb(255, 255, 255), rgb(41, 41, 41));&amp;quot;&amp;gt;① authenticate — AppRole (role_id + secret_id)&amp;lt;/span&amp;gt;&quot;&gt;&#10; &lt;mxGeometry relative=&quot;1&quot; as=&quot;geometry&quot;&gt;&#10; &lt;Array as=&quot;points&quot;&gt;&#10; &lt;mxPoint x=&quot;100&quot; y=&quot;200&quot; /&gt;&#10; &lt;mxPoint x=&quot;480&quot; y=&quot;200&quot; /&gt;&#10; &lt;/Array&gt;&#10; &lt;/mxGeometry&gt;&#10; &lt;/mxCell&gt;&#10; &lt;mxCell id=&quot;s2&quot; edge=&quot;1&quot; parent=&quot;1&quot; source=&quot;vault&quot; style=&quot;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;&quot; target=&quot;agent&quot; value=&quot;&amp;lt;span style=&amp;quot;background-color: light-dark(rgb(255, 255, 255), rgb(41, 41, 41));&amp;quot;&amp;gt;② short-lived token issued, cached, auto-renewed&amp;lt;/span&amp;gt;&quot;&gt;&#10; &lt;mxGeometry relative=&quot;1&quot; as=&quot;geometry&quot;&gt;&#10; &lt;Array as=&quot;points&quot;&gt;&#10; &lt;mxPoint x=&quot;480&quot; y=&quot;375&quot; /&gt;&#10; &lt;mxPoint x=&quot;160&quot; y=&quot;375&quot; /&gt;&#10; &lt;mxPoint x=&quot;160&quot; y=&quot;349.6&quot; /&gt;&#10; &lt;/Array&gt;&#10; &lt;/mxGeometry&gt;&#10; &lt;/mxCell&gt;&#10; &lt;mxCell id=&quot;s3&quot; edge=&quot;1Line truncated

After

Width:  |  Height:  |  Size: 327 KiB

+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 ## Why it's built this way
```mermaid 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:
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
```
- The real secret value lives in exactly one place: Vault. - The real secret value lives in exactly one place: Vault.
- What's git-tracked is a template with placeholder syntax, never a real value. - What's git-tracked is a template with placeholder syntax, never a real value.
@@ -46,25 +35,20 @@ flowchart LR
## Architecture ## Architecture
```mermaid 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.
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"]
Vault -->|HTTPS, cert-verified| Proxy | Consumer | What it gets from Vault |
Proxy --> A1 |---|---|
Proxy --> A2 | App Host | 12 stacks' worth of application secrets, an internal CA cert to verify Vault's own listener, plus a shared registry-token render |
Proxy --> A3 | Automation Host | 10 stacks' worth of application secrets, plus a shared registry-token render |
Proxy --> A4 | Monitoring Host | Grafana's admin password and the Authentik OIDC secret, plus Hawser's application secret, plus a shared registry-token render |
Proxy --> A5 | User-Facing Proxy | Server + client certs for the mTLS-enforced browser zone, plus a shared registry-token render and Hawser's application secret |
Proxy --> A6 | 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:** **Two authentication flows, not one:**
@@ -81,10 +65,10 @@ flowchart TD
| Layer | Protection | | Layer | Protection |
|---|---| |---|---|
| Transport | Every Vault Agent ↔ Vault connection is TLS, certificate-verified | | 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 | | Secrets at rest | KV v2, encrypted by Vault's storage layer; rendered files are `0600`, 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 | | 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 | | 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 ## Known gaps, ranked by actual risk — not a flat list