feat(iam): allow bootstrap mode and token to be sourced from env vars (#851)

Adds an environment-variable fallback for the iam-svc bootstrap
configuration so the token can be injected from a Kubernetes Secret
(or any equivalent secret store) without ever appearing in the
processor-group YAML — which is typically version-controlled.

Resolution order is fixed and per-setting:

  bootstrap_mode  = params["bootstrap_mode"]   or  $IAM_BOOTSTRAP_MODE
  bootstrap_token = params["bootstrap_token"]  or  $IAM_BOOTSTRAP_TOKEN

If neither source supplies a value, the service refuses to start with
a clear message naming both options.  The two settings are resolved
independently, which lets operators commit the mode in YAML (it is
not a secret) while pulling the token from a Secret-backed
``IAM_BOOTSTRAP_TOKEN`` env var.

Validation invariants are unchanged:

* mode must be 'token' or 'bootstrap'
* mode='token' requires a token (from any source)
* mode='bootstrap' must NOT have a token (ambiguous intent)

There is no permissive fallback — the service fails closed in every
branch where configuration is incomplete.

docs/tech-specs/iam-protocol.md gains a 'Configuration sources'
subsection under 'Bootstrap modes' that documents the precedence
table and the K8s injection pattern.  The 'Bootstrap-token
lifecycle' step about removing the token after rotation now applies
to whichever source was used (Secret, env var, or YAML field).
This commit is contained in:
cybermaggedon 2026-04-28 15:00:33 +01:00 committed by GitHub
parent 67b2fc448f
commit 666af1c4b3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 53 additions and 10 deletions

View file

@ -273,6 +273,25 @@ cannot distinguish:
This matches the general IAM error-policy stance (see `iam.md`) and
prevents externally enumerating IAM's state.
### Configuration sources
The mode and token can be supplied two ways. Resolution order is
fixed; there is no permissive fallback.
| Source | Field |
|---|---|
| Processor-group YAML / CLI argument | `bootstrap_mode`, `bootstrap_token` |
| Environment variable | `IAM_BOOTSTRAP_MODE`, `IAM_BOOTSTRAP_TOKEN` |
For each setting the service uses the explicit param value if
present; otherwise the environment variable; otherwise the service
refuses to start. The env-var path is intended for the K8s
deployment pattern where the token is injected from a `Secret` via
`secretKeyRef`, so the plaintext never has to live in YAML or git.
A typical production manifest holds `bootstrap_mode: "token"` in
the YAML and pulls `IAM_BOOTSTRAP_TOKEN` from the Secret; the YAML
is then safe to version-control.
### Bootstrap-token lifecycle
The bootstrap token — whether operator-supplied (`token` mode) or
@ -283,7 +302,8 @@ operator's first admin action after bootstrap should be:
1. Create a durable admin user and API key (or issue a durable API
key to the bootstrap admin).
2. Revoke the bootstrap key via `revoke-api-key`.
3. Remove the bootstrap token from any deployment configuration.
3. Remove the bootstrap token from any deployment configuration
(Secret, env var, or YAML field — wherever it was sourced).
The `name="bootstrap"` marker makes bootstrap keys easy to detect in
tooling (e.g. a `tg-list-api-keys` filter).

View file

@ -7,6 +7,7 @@ Shape mirrors trustgraph.config.service.
"""
import logging
import os
from trustgraph.schema import Error
from trustgraph.schema import IamRequest, IamResponse
@ -27,6 +28,13 @@ default_ident = "iam-svc"
default_iam_request_queue = iam_request_queue
default_iam_response_queue = iam_response_queue
# Environment variables consulted as a fallback when the
# corresponding params field is not set in the processor-group YAML
# or via CLI. Intended for K8s Secret / env-var injection so the
# bootstrap token never has to live in the YAML (and thus in git).
ENV_BOOTSTRAP_MODE = "IAM_BOOTSTRAP_MODE"
ENV_BOOTSTRAP_TOKEN = "IAM_BOOTSTRAP_TOKEN"
class Processor(AsyncProcessor):
@ -39,26 +47,41 @@ class Processor(AsyncProcessor):
"iam_response_queue", default_iam_response_queue,
)
bootstrap_mode = params.get("bootstrap_mode")
bootstrap_token = params.get("bootstrap_token")
# Resolve bootstrap mode + token. Precedence: explicit
# params (CLI / processor-group YAML) → environment variable
# → unset (fail-closed). The env-var path is the K8s-native
# injection point: an `IAM_BOOTSTRAP_TOKEN` from a Secret
# never has to land in the YAML, and therefore never enters
# git history.
bootstrap_mode = (
params.get("bootstrap_mode")
or os.environ.get(ENV_BOOTSTRAP_MODE)
)
bootstrap_token = (
params.get("bootstrap_token")
or os.environ.get(ENV_BOOTSTRAP_TOKEN)
)
if bootstrap_mode not in ("token", "bootstrap"):
raise RuntimeError(
"iam-svc: --bootstrap-mode is required. Set to 'token' "
"(with --bootstrap-token) for production, or 'bootstrap' "
"iam-svc: bootstrap-mode is required. Set to 'token' "
"(with bootstrap-token) for production, or 'bootstrap' "
"to enable the explicit bootstrap operation over the "
"pub/sub bus (dev / quick-start only, not safe under "
"public exposure). Refusing to start."
"public exposure). Configurable via processor-group "
f"params or the {ENV_BOOTSTRAP_MODE} environment "
"variable. Refusing to start."
)
if bootstrap_mode == "token" and not bootstrap_token:
raise RuntimeError(
"iam-svc: --bootstrap-mode=token requires "
"--bootstrap-token. Refusing to start."
"iam-svc: bootstrap-mode=token requires bootstrap-token "
f"(or the {ENV_BOOTSTRAP_TOKEN} environment "
"variable). Refusing to start."
)
if bootstrap_mode == "bootstrap" and bootstrap_token:
raise RuntimeError(
"iam-svc: --bootstrap-token is not accepted when "
"--bootstrap-mode=bootstrap. Ambiguous intent. "
"iam-svc: bootstrap-token is not accepted when "
"bootstrap-mode=bootstrap. Ambiguous intent. "
"Refusing to start."
)