Vault secrets
Firstlume doesn't store your Vault secrets. The executor reads Vault inside the run's pod, and the values don't pass through the server or the database. For this, Firstlume issues a JWT for each phase (ES256, valid for 15 minutes) that says which stack is running, and the executor exchanges it for a Vault token. The token is revoked when the phase ends.
1. In Firstlume
Set the organization's connection with PUT /api/v1/orgs/{org}/vault:
{"address": "https://vault.example.com:8200"}
Optional fields are ca_cert, namespace, auth_path (default jwt) and audience (default vault).
Set the role, inherited like the other settings, on a folder or on the stack:
{"vault_role": "infra-dev"}
FIRSTLUME_PUBLIC_URL (in Helm, controlPlane.publicURL, which defaults to the ingress host) is the iss of the JWTs.
2. In Vault
Enable the jwt method trusting Firstlume, with discovery or with the public key:
vault auth enable jwt
vault write auth/jwt/config bound_issuer=https://firstlume.example.com \
oidc_discovery_url=https://firstlume.example.com # or jwt_validation_pubkeys=@firstlume.pem
vault policy write infra-dev - <<'EOF'
path "secret/data/infra/dev/*" { capabilities = ["read"] }
EOF
vault write auth/jwt/role/infra-dev role_type=jwt user_claim=sub bound_audiences=vault \
bound_claims_type=glob bound_claims='{"stack_path": "infra/dev/*"}' \
token_policies=infra-dev token_ttl=15m
The public key is at GET /api/v1/orgs/{org}/vault/public-key (PEM) and at /.well-known/jwks.json.
3. In the repository
Use {vault: secret/infra/dev/${name}, field: ip} in any value of the declaration. The path is the same as in vault kv get. KV v1 and v2 are detected, and other engines work too.
JWT claims
| Claim | Value |
|---|---|
sub |
stack:<id> |
org |
the organization |
stack_id, stack_name |
the stack |
stack_path |
folders and name, such as infra/dev/machines |
folder_ids |
the folder ids |
run_id, run_type |
the run |
phase |
plan, apply or drift |
With bound_claims, a dev stack can't read prod secrets, and a role with phase = apply doesn't work for the plan. Prefer folder_ids to stack_path if renaming folders could change access.
How values are handled
- Everything from Vault is sensitive: masked in the log, and files get
0600permissions. - Plan and apply: the apply reads Vault again and checks it against the plan, with a keyed hash and no stored values. If a secret changed between the two, the apply fails with
the secret in <path>#<field> changed since the plan: plan again. - Errors name the declaration item, the path and the field, never the value: no connection, no role, login refused, permission denied, missing secret or field, Vault down.
Test without running anything
POST /api/v1/orgs/{org}/stacks/{id}/vault/check logs in as the stack and says, per path, whether the role can read, without reading any value:
{"paths": ["secret/infra/dev/n01"]}