secret-distribution: verify against live-checked KB, fix diagram, add real evidence

Source Trilium note (Secret Distribution Model) was live-verified and
stamped this session - Certificate Flow, App Secret Flow, Configuration
Distribution Flow, and the technitium_api_token Vault-KV exception all
confirmed against the real hosts, not just trusted from the KB.

Replaced the Mermaid diagram, which incorrectly showed the Configuration
Distribution Flow feeding into Vault directly, with a corrected version:
that flow is a control-plane gate on what Vault Agent renders, not a
peer data flow. Added a real Vault Agent log excerpt proving the full
redeploy chain (restart -> render -> reload_command -> container
recreated) actually happens, and restored the technitium_api_token
exception that was in the source note but missing from this doc.
This commit is contained in:
thesabear committed 2026-08-26 01:23:22 +03:00
1 parent b4827a3d42
commit 1c3a2c83a5
3 files changed
+12 -11

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: #ffffff; background-color: var(--ge-adaptive-bg, #ffffff); color-scheme: light dark;" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="1085px" height="545px" viewBox="0 0 1085 545" id="ge-svg-lxAUNMmLjYII1QtcEPaS" content="&lt;mxfile host=&quot;embed.diagrams.net&quot; scale=&quot;1&quot; border=&quot;0&quot;&gt;&#10; &lt;diagram name=&quot;Secret Distribution Flows&quot; id=&quot;secret-dist-flows-1&quot;&gt;&#10; &lt;mxGraphModel dx=&quot;1414&quot; dy=&quot;791&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;1200&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;repo&quot; parent=&quot;1&quot; style=&quot;rounded=1;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;fontColor=#333333;fontSize=14;fontStyle=1;shadow=1;&quot; value=&quot;Provisioning Repo&amp;lt;br&amp;gt;&amp;lt;font style=&amp;quot;font-size:11px&amp;quot;&amp;gt;template inventory&amp;lt;/font&amp;gt;&quot; vertex=&quot;1&quot;&gt;&#10; &lt;mxGeometry height=&quot;80&quot; width=&quot;200&quot; x=&quot;40&quot; y=&quot;60&quot; as=&quot;geometry&quot; /&gt;&#10; &lt;/mxCell&gt;&#10; &lt;mxCell id=&quot;agentconfig&quot; parent=&quot;1&quot; style=&quot;rounded=1;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;fontColor=#333333;fontSize=14;fontStyle=1;shadow=1;&quot; value=&quot;Vault Agent Config&amp;lt;br&amp;gt;&amp;lt;font style=&amp;quot;font-size:11px&amp;quot;&amp;gt;on the target host&amp;lt;/font&amp;gt;&quot; vertex=&quot;1&quot;&gt;&#10; &lt;mxGeometry height=&quot;80&quot; width=&quot;200&quot; x=&quot;410&quot; y=&quot;60&quot; as=&quot;geometry&quot; /&gt;&#10; &lt;/mxCell&gt;&#10; &lt;mxCell id=&quot;bgfill_dataplane&quot; parent=&quot;1&quot; style=&quot;rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=none;&quot; vertex=&quot;1&quot;&gt;&#10; &lt;mxGeometry height=&quot;290&quot; width=&quot;440&quot; x=&quot;380&quot; y=&quot;230&quot; as=&quot;geometry&quot; /&gt;&#10; &lt;/mxCell&gt;&#10; &lt;mxCell id=&quot;dataplane&quot; parent=&quot;1&quot; style=&quot;swimlane;whiteSpace=wrap;html=1;startSize=30;fillColor=none;strokeColor=#9673a6;dashed=1;fontSize=12;fontStyle=1;fontColor=#333333;align=left;spacingLeft=8;verticalAlign=middle;&quot; value=&quot;DATA PLANE — what Vault Agent actually renders&quot; vertex=&quot;1&quot;&gt;&#10; &lt;mxGeometry height=&quot;290&quot; width=&quot;440&quot; x=&quot;380&quot; y=&quot;230&quot; as=&quot;geometry&quot; /&gt;&#10; &lt;/mxCell&gt;&#10; &lt;mxCell id=&quot;vaultagent&quot; parent=&quot;dataplane&quot; style=&quot;rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;fontColor=#333333;fontSize=13;fontStyle=1;shadow=1;&quot; value=&quot;Vault Agent&quot; vertex=&quot;1&quot;&gt;&#10; &lt;mxGeometry height=&quot;60&quot; width=&quot;140&quot; x=&quot;150&quot; y=&quot;40&quot; as=&quot;geometry&quot; /&gt;&#10; &lt;/mxCell&gt;&#10; &lt;mxCell id=&quot;certflow&quot; parent=&quot;dataplane&quot; style=&quot;rounded=1;whiteSpace=wrap;html=1;fillColor=light-dark(#FFFFFF,#292929);strokeColor=#9673a6;fontColor=#333333;fontSize=12;fontStyle=1;shadow=1;&quot; value=&quot;Certificate Flow&amp;lt;br&amp;gt;&amp;lt;font style=&amp;quot;font-size:11px&amp;quot;&amp;gt;PKI engine, short-lived&amp;lt;/font&amp;gt;&quot; vertex=&quot;1&quot;&gt;&#10; &lt;mxGeometry height=&quot;65&quot; width=&quot;170&quot; x=&quot;30&quot; y=&quot;180&quot; as=&quot;geometry&quot; /&gt;&#10; &lt;/mxCell&gt;&#10; &lt;mxCell id=&quot;secflow&quot; parent=&quot;dataplane&quot; style=&quot;rounded=1;whiteSpace=wrap;html=1;fillColor=light-dark(#FFFFFF,#292929);strokeColor=#9673a6;fontColor=#333333;fontSize=12;fontStyle=1;shadow=1;&quot; value=&quot;App Secret Flow&amp;lt;br&amp;gt;&amp;lt;font style=&amp;quot;font-size:11px&amp;quot;&amp;gt;KV, rendered 0600&amp;lt;/font&amp;gt;&quot; vertex=&quot;1&quot;&gt;&#10; &lt;mxGeometry height=&quot;65&quot; width=&quot;170&quot; x=&quot;240&quot; y=&quot;180&quot; as=&quot;geometry&quot; /&gt;&#10; &lt;/mxCell&gt;&#10; &lt;mxCell id=&quot;container&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;Container&amp;lt;br&amp;gt;&amp;lt;font style=&amp;quot;font-size:11px&amp;quot;&amp;gt;reads local disk only&amp;lt;/font&amp;gt;&quot; vertex=&quot;1&quot;&gt;&#10; &lt;mxGeometry height=&quot;8Line truncated

After

Width:  |  Height:  |  Size: 533 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 290 KiB

+8 -11
View File
@@ -3,17 +3,7 @@
> [!NOTE] > [!NOTE]
> [`docs/secrets-and-pki.md`](secrets-and-pki.md) covers *what* Vault is and *why* it exists. This doc covers something different and, arguably, harder to get right: the actual contract for how a secret value moves from being written into Vault to being consumed by a running container — and why that contract is deliberately split into four separate flows instead of one universal one. > [`docs/secrets-and-pki.md`](secrets-and-pki.md) covers *what* Vault is and *why* it exists. This doc covers something different and, arguably, harder to get right: the actual contract for how a secret value moves from being written into Vault to being consumed by a running container — and why that contract is deliberately split into four separate flows instead of one universal one.
```mermaid ![Secret distribution flows: Configuration Distribution Flow (orange) is the control plane that gates what Vault Agent renders; Certificate Flow and App Secret Flow (purple) are the data plane running through that same agent; Administrative Secret Flow (gray) is a genuinely separate system with no Vault involvement](../assets/secret-distribution-flows.svg)
flowchart TD
Op["Operator"] -->|1: writes cert/secret request| Vault["Vault"]
Vault -->|2: Certificate Flow| Cert["Short-lived cert<br/>rendered to disk"]
Vault -->|3: App Secret Flow| Sec["Rendered secret file<br/>0600, gitignored"]
Repo["Provisioning repo"] -->|4: Config Distribution Flow| Vault
Op2["Operator"] -.5: Administrative Secret Flow<br/>separate system entirely.-> EncFile["ansible-vault encrypted file<br/>in the repo itself"]
Cert --> Container["Container reads<br/>local disk only"]
Sec --> Container
```
## Why four flows instead of one ## Why four flows instead of one
@@ -51,6 +41,10 @@ The obvious simpler design is one universal "secret goes in, secret comes out" p
> [!IMPORTANT] > [!IMPORTANT]
> This is the flow with the sharpest failure mode in the whole model: a missing or malformed template referenced by a host's own config crashes that host's **entire** Vault Agent process — every other secret/cert that host was already rendering stops updating too, not just the one broken template. A single YAML typo in one app's template inventory can silently freeze every other app's secret rotation on that same host. This is why a config redeploy always requires confirming the underlying git change actually landed first, not just that the deploy command returned success. > This is the flow with the sharpest failure mode in the whole model: a missing or malformed template referenced by a host's own config crashes that host's **entire** Vault Agent process — every other secret/cert that host was already rendering stops updating too, not just the one broken template. A single YAML typo in one app's template inventory can silently freeze every other app's secret rotation on that same host. This is why a config redeploy always requires confirming the underlying git change actually landed first, not just that the deploy command returned success.
That redeploy-and-restart sequence isn't just a claim — here's a real one, start to finish: Vault Agent restarts, authenticates, renders a fresh `.env.secrets`, and its `reload_command` fires `docker compose up -d`, which recreates and restarts the actual container:
![A real Vault Agent restart: authenticates, renders triliumnext-mcp's .env.secrets, then its reload_command runs docker compose up -d, recreating and starting the container](../assets/vault-agent-reload.png)
**Shared-secret variant:** not every template is host-scoped — one credential needed by every docker host for its own registry pushes (see [`docs/registry-and-build.md`](registry-and-build.md)) is templated from a single shared path rather than a per-host one. Same mechanics, just a shared source instead of a private one — and it relies on every host's policy already granting read access to that shared path, the same "shared + private" pattern described in [`docs/secrets-and-pki.md`](secrets-and-pki.md)'s access model. **Shared-secret variant:** not every template is host-scoped — one credential needed by every docker host for its own registry pushes (see [`docs/registry-and-build.md`](registry-and-build.md)) is templated from a single shared path rather than a per-host one. Same mechanics, just a shared source instead of a private one — and it relies on every host's policy already granting read access to that shared path, the same "shared + private" pattern described in [`docs/secrets-and-pki.md`](secrets-and-pki.md)'s access model.
## 4️⃣ Administrative Secret Flow — a deliberately separate, simpler system ## 4️⃣ Administrative Secret Flow — a deliberately separate, simpler system
@@ -64,5 +58,8 @@ The obvious simpler design is one universal "secret goes in, secret comes out" p
> [!TIP] > [!TIP]
> **Why two systems, not one — the actual reasoning, not just the rule**: Vault KV is built for runtime/service/pipeline secrets a running container needs continuously and automatically. `ansible-vault` is built for secrets used by tooling that *operates on* the fleet rather than secrets *consumed by* the fleet's own running services — a Gitea admin token used only during a playbook run, for instance, doesn't need automatic rotation, per-host scoping, or a rendered file sitting on disk permanently. Building that into Vault's model would have meant either weakening Vault's per-host isolation for a use case that doesn't need it, or adding a whole parallel "admin secrets" concept inside Vault itself. Using the tool already built for exactly this case (`ansible-vault`, already part of the Ansible toolchain from [`docs/iac-pipeline.md`](iac-pipeline.md)) was less work and a better fit than extending Vault to do a second, different job. > **Why two systems, not one — the actual reasoning, not just the rule**: Vault KV is built for runtime/service/pipeline secrets a running container needs continuously and automatically. `ansible-vault` is built for secrets used by tooling that *operates on* the fleet rather than secrets *consumed by* the fleet's own running services — a Gitea admin token used only during a playbook run, for instance, doesn't need automatic rotation, per-host scoping, or a rendered file sitting on disk permanently. Building that into Vault's model would have meant either weakening Vault's per-host isolation for a use case that doesn't need it, or adding a whole parallel "admin secrets" concept inside Vault itself. Using the tool already built for exactly this case (`ansible-vault`, already part of the Ansible toolchain from [`docs/iac-pipeline.md`](iac-pipeline.md)) was less work and a better fit than extending Vault to do a second, different job.
> [!NOTE]
> **One exception to this split, flagged rather than smoothed over**: Technitium's API token moved out of `ansible-vault` into live Vault KV (fetched via a `community.hashi_vault.vault_kv2_get` lookup at playbook-run time) even though it's consumed by an ansible-playbook, not a running service — exactly the case the law above says belongs in `ansible-vault` instead. The admin/runtime split is the general rule here, not an absolute one, and this is the one place it doesn't cleanly apply.
--- ---
Related: [`docs/secrets-and-pki.md`](secrets-and-pki.md), [`docs/iac-pipeline.md`](iac-pipeline.md), [`docs/registry-and-build.md`](registry-and-build.md) Related: [`docs/secrets-and-pki.md`](secrets-and-pki.md), [`docs/iac-pipeline.md`](iac-pipeline.md), [`docs/registry-and-build.md`](registry-and-build.md)