|
1 | 1 | # vault-manager |
2 | 2 |
|
3 | | -`vault-manager` bootstraps Vault for local Persys environments. |
| 3 | +`vault-manager` bootstraps Vault for Persys Cloud environments. It brings a |
| 4 | +fresh Vault instance up to a usable state — initialized, unsealed, with a |
| 5 | +full PKI chain and per-service AppRole credentials — and then stays running |
| 6 | +as a gRPC service so other Persys Cloud components can fetch or rotate |
| 7 | +their own credentials without touching Vault Environment Variables directly. |
4 | 8 |
|
5 | 9 | ## Responsibilities |
6 | 10 |
|
7 | | -- Initialize and unseal Vault (first run). |
8 | | -- Ensure PKI mounts/issuers and service roles exist. |
9 | | -- Ensure AppRole auth, policies, and role credentials for platform services. |
| 11 | +- **Initialize and unseal Vault** on first run (single key-share setup), |
| 12 | + or pick up an already-initialized Vault via `VAULT_ROOT_TOKEN`. |
| 13 | +- **Provision the PKI chain**: mounts the root and intermediate PKI |
| 14 | + secrets engines, generates the root CA, signs and installs the |
| 15 | + intermediate CA, and configures default issuers. |
| 16 | +- **Create per-service PKI roles** so each service can request leaf |
| 17 | + certificates scoped to its own name. |
| 18 | +- **Enable AppRole auth** and create one AppRole per service, each bound |
| 19 | + to a least-privilege ACL policy (issue certs, read CA/CRL — nothing |
| 20 | + else). |
| 21 | +- **Optionally hand off from the root token** (`--secure`): provisions a |
| 22 | + scoped bootstrap-manager AppRole, logs in as it, and revokes the root |
| 23 | + token so the rest of provisioning — and the live gRPC server — never |
| 24 | + hold root privileges. |
| 25 | +- **Serve a gRPC API** (`VaultManagerService`) so services can fetch their |
| 26 | + AppRole credentials at runtime or rotate their `secret_id` without a |
| 27 | + human touching Vault. |
| 28 | + |
| 29 | +## Project layout |
| 30 | + |
| 31 | +``` |
| 32 | +vault-manager/ |
| 33 | +├── cmd/ |
| 34 | +│ └── vault-manager/ |
| 35 | +│ └── main.go # entrypoint: flag parsing, bootstrap orchestration |
| 36 | +└── internal/ |
| 37 | + ├── config/ # defaults, CLI flags, shared logger |
| 38 | + ├── vaultclient/ # Vault client lifecycle: connect, init, unseal, secure handoff |
| 39 | + ├── pki/ # PKI mounts, CA chain, per-service PKI roles |
| 40 | + ├── policy/ # ACL policies (per-service + bootstrap manager) |
| 41 | + ├── approle/ # AppRole auth, credential issuance/rotation |
| 42 | + ├── server/ # gRPC API: handlers, logging interceptor, server startup |
| 43 | + └── vaultmanagerv1/ # generated protobuf/gRPC code (VaultManagerService) |
| 44 | +``` |
10 | 45 |
|
11 | 46 | ## Run |
12 | 47 |
|
13 | 48 | ```bash |
14 | 49 | cd vault-manager |
15 | | -go run ./main.go --vault-addr=http://localhost:8200 |
| 50 | +go run ./cmd/vault-manager --vault-addr=http://localhost:8200 |
| 51 | +``` |
| 52 | + |
| 53 | +On first run against an uninitialized Vault, the unseal key and root token |
| 54 | +are printed to stdout — store them immediately, they are not recoverable |
| 55 | +afterward. On subsequent runs against an already-initialized Vault, set |
| 56 | +`VAULT_ROOT_TOKEN` in the environment instead: |
| 57 | + |
| 58 | +```bash |
| 59 | +VAULT_ROOT_TOKEN=hvs.xxxxx go run ./cmd/vault-manager --vault-addr=http://localhost:8200 |
16 | 60 | ``` |
17 | 61 |
|
18 | | -In docker compose, this is used by `vault-manager-setup` profile in `infra/docker/docker-compose.yml`. |
| 62 | +To provision once with root and then drop root privileges for the life of |
| 63 | +the process: |
| 64 | + |
| 65 | +```bash |
| 66 | +go run ./cmd/vault-manager --vault-addr=http://localhost:8200 --secure |
| 67 | +``` |
| 68 | + |
| 69 | +## CLI flags |
| 70 | + |
| 71 | +| Flag | Default | Description | |
| 72 | +| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------- | |
| 73 | +| `--vault-addr` | `https://vault:8200` | Vault API address | |
| 74 | +| `--pki-root-mount` | `pki` | Root PKI secrets engine mount path | |
| 75 | +| `--pki-int-mount` | `pki_int` | Intermediate PKI secrets engine mount path | |
| 76 | +| `--root-cn` | `Persys Cloud Root CA` | Common name for the root CA | |
| 77 | +| `--intermediate-cn` | `Persys Cloud Intermediate CA` | Common name for the intermediate CA | |
| 78 | +| `--manager-role` | `vault-manager-bootstrap` | AppRole name used for the `--secure` bootstrap handoff | |
| 79 | +| `--manager-policy` | `vault-manager-bootstrap-policy` | ACL policy name for the bootstrap manager AppRole | |
| 80 | +| `--services` | `persys-gateway,persys-scheduler,persysctl,compute-agent,persys-forgery,persys-services,persys-automation,persys-intelligence,persys-sdk` | Comma-separated list of services to provision | |
| 81 | +| `--secure` | `false` | Provision a bootstrap AppRole and revoke the root token after setup | |
| 82 | + |
| 83 | +## Environment variables |
| 84 | + |
| 85 | +| Variable | Required when | Description | |
| 86 | +| ------------------- | --------------------------------------------- | ---------------------------------------------------------------- | |
| 87 | +| `VAULT_ROOT_TOKEN` | Vault is already initialized | Root token used for provisioning when no fresh init occurs | |
| 88 | + |
| 89 | +## What gets created in Vault |
| 90 | + |
| 91 | +For each service in `--services` (default 9 platform services): |
| 92 | + |
| 93 | +- A PKI role at `<pki-root-mount>/roles/<service>` (EC P-256 keys, 72h |
| 94 | + default TTL, 720h max TTL, any name allowed for internal cert issuance). |
| 95 | +- An ACL policy named `<service>-policy`, granting: |
| 96 | + - `update` on `<pki-root-mount>/issue/<service>` |
| 97 | + - `read` on `<pki-root-mount>/cert/ca`, `cert/ca_chain`, and `crl` |
| 98 | +- An AppRole at `auth/approle/role/<service>`, bound to that policy |
| 99 | + (1h token TTL, 4h max TTL, 24h secret_id TTL, unlimited secret_id uses). |
| 100 | + |
| 101 | +If `--secure` is set, an additional bootstrap-manager AppRole and a |
| 102 | +broader policy (mount/auth/policy management plus full PKI access) are |
| 103 | +created, used once to hand off from the root token, then the root token |
| 104 | +is revoked. |
| 105 | + |
| 106 | +## gRPC API |
| 107 | + |
| 108 | +`vault-manager` listens on `:50069` and exposes `VaultManagerService` |
| 109 | +(defined in `internal/vaultmanagerv1`): |
| 110 | + |
| 111 | +- **`GetServiceCredentials(service_name)`** — returns the service's |
| 112 | + current `role_id` and a freshly generated `secret_id`, plus an |
| 113 | + `expires_at` Unix timestamp (720h from issuance). |
| 114 | +- **`RotateServiceSecretID(service_name)`** — identical behavior to |
| 115 | + `GetServiceCredentials`; every call mints a new `secret_id`, so calling |
| 116 | + either RPC rotates the credential. They're exposed as two RPCs for |
| 117 | + clarity of intent at the call site (initial fetch vs. explicit |
| 118 | + rotation), not because the underlying operation differs. |
| 119 | + |
| 120 | +Every RPC is wrapped in a logging interceptor that emits structured JSON |
| 121 | +logs (method, status code, duration, and any error) for each call. |
| 122 | + |
| 123 | +## Docker Compose |
| 124 | + |
| 125 | +In docker compose, this is used by the `vault-manager` profile in |
| 126 | +`infra/docker/docker-compose.yml`. |
| 127 | + |
| 128 | +## Operational notes |
| 129 | + |
| 130 | +- Single key-share initialization (`secret_shares: 1`, `secret_threshold: |
| 131 | + 1`) is intended for local/dev environments. Production Vault deployments |
| 132 | + should use Shamir's Secret Sharing with multiple key holders or |
| 133 | + auto-unseal via a cloud KMS instead. |
| 134 | +- The printed root token and unseal key during first-run initialization |
| 135 | + are the only time they're surfaced — capture and store them securely |
| 136 | + immediately (e.g., in your team's secrets manager), since Vault does not |
| 137 | + let you retrieve them again. |
| 138 | +- `secret_id_num_uses: 0` (unlimited) on service AppRoles means a leaked |
| 139 | + `secret_id` can be reused until its 24h TTL expires; rotate via the |
| 140 | + gRPC API or by re-running `vault-manager` if a leak is suspected. |
0 commit comments