# ๐Ÿ” Secrets & PKI > [!NOTE] > A single Vault cluster serves as both the internal certificate authority and the secret store for the whole fleet โ€” one trust root, not two parallel systems. | | | |---|---| | **Role** | Certificate authority + secret store | | **Distribution** | Vault Agent on every host, AppRole auth, renders locally | | **Replaces** | A prior plaintext-`.env`-in-git secrets model | | **Biggest known risk** | A non-expiring root token โ€” ranked, tracked, not ignored | ## What it does - **Certificate authority** โ€” two PKI mounts: one for public-facing wildcard certs, one for internal mTLS certs. Both issue short-lived certificates to the hosts that need them. - **Secret store** โ€” a KV v2 mount holds every migrated application's real secret values, one namespace per host. - **Distribution** โ€” Vault Agent runs on every fleet host, authenticates via AppRole, and renders both certificates and secrets to local disk. No application or service ever talks to Vault directly. ## Why it's built this way ```mermaid flowchart LR Before["Before:
plaintext secrets
in git-tracked .env files"] -->|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"] T -.no real value ever.-> T V -->|renders| R end ``` - The real secret value lives in exactly one place: Vault. - What's git-tracked is a template with placeholder syntax, never a real value. - The rendered file is gitignored, tightly permissioned, and owned entirely by Vault Agent โ€” never hand-edited. - If a secret rotates, updating it once in Vault is enough; every consumer picks it up on its next render cycle. > [!TIP] > The mechanism (Vault Agent authenticating via AppRole, rendering a template to disk) already existed for TLS certificates before it was extended to cover application secrets โ€” reusing a trust model that was already proven, rather than inventing a second one. ## 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"] 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"] Vault -->|HTTPS, cert-verified| Proxy Proxy --> A1 Proxy --> A2 Proxy --> A3 Proxy --> A4 Proxy --> A5 Proxy --> A6 ``` **Two authentication flows, not one:** | Flow | When | Path | |---|---|---| | AppRole auth | Every host, ongoing | Vault Agent โ†’ role_id + secret_id โ†’ Vault โ†’ short-lived token, cached, auto-renewed | | Bootstrap auth | Rare, human-initiated | Operator uses a root token to mint new AppRole credentials or update policies โ€” never part of any workload's runtime path | > [!IMPORTANT] > **The one deliberately-asymmetric trust decision**: every other backend connection inside the internal zone skips certificate verification on the reverse proxy hop (still encrypted, just doesn't check identity) โ€” a pragmatic tradeoff for low-stakes internal traffic. The connection to Vault is the single exception: the proxy verifies Vault's certificate for real. Given what's on the other end of that connection, the asymmetry is intentional, not an oversight. ## What's protected | 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 | | Alerting | Root-token use or permission-denied bursts trigger an alert through the fleet's notification pipeline | | Cert renewal | A dependent service's TLS bundle auto-rebuilds when its cert re-issues โ€” closed a real gap where an expired cert was silently served for over two weeks | ## Known gaps, ranked by actual risk 1. **The bootstrap root token never expires.** Unlimited access, mints every AppRole, writes every policy. Encrypted at rest, but that doesn't help if the plaintext ever gets used somewhere it shouldn't. The single highest-value target in the whole system โ€” tracked for replacement with a narrowly-scoped, short-TTL token. 2. **No SSH certificate signing.** Every host's SSH key is static, baked in at image build time, no rotation, no revocation short of rebuilding the host. Vault's SSH secrets engine is the natural fit โ€” a bigger lift since it touches every host's SSH config. 3. **Single-node storage, no replication.** One disk failure is a full outage and, without a recent backup, real data loss. An availability concern, not a confidentiality one โ€” deliberately lower priority than the two above. > [!TIP] > **Deliberately accepted, not gaps**: memory locking is disabled (normally a real concern, but this host has zero swap, so there's no actual leak vector) โ€” and the Vault server's own config is intentionally kept out of the same automation that manages every client-side Vault Agent. ## Access model One policy per host, scoped to exactly what that host needs โ€” no host's credentials can read another host's secrets. Every host-scoped policy follows the same shape: a shared grant for lightweight self-registration, plus a private grant with full read/write access only to that host's own namespace. Onboarding a new host copies this shape rather than reinventing it โ€” and the shared grant does *not* automatically imply the private one, so both get verified before assuming a host is fully provisioned. --- Related stacks: [`stacks/identity-and-security/vaultwarden/`](../stacks/identity-and-security/vaultwarden/)