diff --git a/assets/vault-acl-policies.png b/assets/vault-acl-policies.png index 7cda6ae..ed29f40 100644 Binary files a/assets/vault-acl-policies.png and b/assets/vault-acl-policies.png differ diff --git a/assets/vault-secrets-engines.png b/assets/vault-secrets-engines.png new file mode 100644 index 0000000..d9db9f3 Binary files /dev/null and b/assets/vault-secrets-engines.png differ diff --git a/docs/secrets-and-pki.md b/docs/secrets-and-pki.md index 824ac6f..d88e050 100644 --- a/docs/secrets-and-pki.md +++ b/docs/secrets-and-pki.md @@ -6,7 +6,7 @@ | | | |---|---| | **Software** | HashiCorp Vault, self-hosted, native systemd service (not containerized) | -| **Role** | Certificate authority (2 PKI mounts) + secret store (KV v2) | +| **Role** | Certificate authority (2 PKI mounts + 1 SSH CA mount) + secret store (KV v2) | | **Distribution** | Vault Agent on every consuming host, AppRole auth, renders to local disk | | **Replaces** | A prior plaintext-`.env`-in-git secrets model | @@ -19,6 +19,24 @@ A managed secrets service (a cloud provider's KMS/Secrets Manager) was a real al - **Certificate authority** — two PKI mounts: one for public-facing wildcard certs (consumed by the edge in [`docs/firewall-and-network.md`](firewall-and-network.md)), one for internal mTLS certs (consumed by the browser-facing reverse-proxy zone). Both issue short-lived certificates, not long-lived ones — a deliberate choice that shrinks the window a leaked cert is useful for. - **Secret store** — a KV v2 mount holds every migrated application's real secret values, one namespace per host, enforced by policy (below). - **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 — this single rule is what makes the rest of the model tractable to reason about. +- **SSH certificate authority** — a dedicated mount issues short-lived SSH certificates for the fleet's automation identity, instead of that identity holding a single static key with standing, permanent access to every host. A signed certificate is valid for 6 hours (12-hour hard cap), scoped to exactly one principal, and every host trusts the CA's public key directly rather than storing that identity's key at all — the same short-lived-credential philosophy as the PKI mounts above, applied to SSH instead of TLS. A leaked certificate is worthless in hours; a leaked static key used to be useful forever. The one thing that can't use this model at all is Kestra's own SSH-based automation flows — its SSH plugin is a plain client with no certificate support, full stop — see [`docs/orchestration.md`](orchestration.md) for what it uses instead, and why that's a completely separate, unrelated credential rather than a workaround bolted onto this one. + +This is the real role definition, not a paraphrase: + +``` +$ vault read ssh-client-signer/roles/sign-ansible +key_type ca +allowed_users ansible +default_user ansible +ttl 6h (21600s) +max_ttl 12h (43200s) +algorithm_signer rsa-sha2-512 +default_extensions permit-pty, permit-port-forwarding +``` + +And this is the real mount list, not an assumption of what's enabled — the SSH CA sitting right alongside the two PKI mounts, not a separate system bolted on beside Vault: + +![Vault's live Secrets engines list: cubbyhole/, kv/, pki/ (public wildcard), pki_int/ (internal mTLS), and ssh-client-signer/ — the SSH certificate authority is a real mount, not a description of an aspiration](../assets/vault-secrets-engines.png) ## Why It's Built This Way