secrets-engine/docs/cli.md
tegwick 70371649af
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Harden production authorization and service auth
Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a0217e-8c4c-7383-be6b-f50a6e485306
2026-08-23 14:15:42 +02:00

206 lines
9.7 KiB
Markdown

# secrets-engine CLI
## Terminology (Gitea is overloaded — be explicit)
| Term we use | Means | Example | Not to be confused with |
| --- | --- | --- | --- |
| **org** | the Gitea organisation | `coulomb` | the npm scope |
| **repo** | the Gitea repository / product | `whynot-design` | an org; a Gitea "project" board |
| **npm scope** | the `@`-prefix npm name | `@whynot` | the org or the repo |
| **npm package** | the published artifact | `@whynot/design` | the repo it's built from |
| **lane / catalog id** | a secrets-engine secret lane | `whynot-design-npm-publish` | — |
A catalog entry carries `org` + `repo` explicitly (never a bare "owner"), and the
npm registry/scope live in `delivery_config.npm` as data — the engine never
hardcodes a registry. The pilot publishes `@whynot/design` from the
`coulomb/whynot-design` repo to `https://forgejo.coulomb.social/api/packages/coulomb/npm/`.
Existing production lanes additionally distinguish mount ownership, native
delivery auth, and workload delivery. See
[catalog-admission.md](catalog-admission.md). In particular,
`mount_management: existing` makes mount handling non-mutating; it does not
authorize secrets-engine to replace an existing ESO/Kubernetes delivery path.
## Install
```bash
uv venv && uv pip install -e ".[dev]"
source .venv/bin/activate
secrets-engine --version
```
## Environment
| Var | Default | Purpose |
| --- | --- | --- |
| `BAO_ADDR` | `http://127.0.0.1:8200` | OpenBao address |
| `BAO_TOKEN` | _(unset)_ | OpenBao token (or use `--bootstrap-token-file`) |
| `SECRETS_ENGINE_HUB_URL` | `http://127.0.0.1:8000` | State Hub for decisions + evidence (empty to disable) |
| `SECRETS_ENGINE_CATALOG` | `./catalog` | catalog directory |
| `SECRETS_ENGINE_EVIDENCE` | `./.evidence` | local non-secret evidence log |
| `SECRETS_ENGINE_UNSAFE_DEMO` | _(unset)_ | allow a prod-labeled lane only when Hub is disabled and OpenBao is loopback; throwaway demos only |
## Commands
```text
secrets-engine catalog list
secrets-engine catalog show <catalog-id>
secrets-engine decision inspect <decision-or-ccr-id>
secrets-engine plan <ref> --stage <build|test|prod>
secrets-engine apply <ref> --stage <stage> [--dry-run] [--bootstrap-token-file F]
secrets-engine provision <catalog-id> --stage <stage> --field NAME (--from-file F | --generate)
secrets-engine verify <catalog-id> [--field NAME] [--positive] [--negative] [--negative-token-file F]
secrets-engine handoff <catalog-id> --stage <stage> --role-id-file F --secret-id-file F
secrets-engine exec --catalog <catalog-id> [--field NAME] [--mode auto|npm-config|exec-env] -- CMD...
secrets-engine policy publication <catalog-id>
secrets-engine route <catalog-id> [--json]
secrets-engine revoke <catalog-id> [--dry-run]
secrets-engine lifecycle suspend <catalog-id> [--dry-run]
secrets-engine lifecycle deactivate <catalog-id> [--dry-run]
secrets-engine lifecycle destroy <catalog-id> [--dry-run] [--confirm-destroy <catalog-id>]
secrets-engine audit <catalog-id> [--json]
```
`policy publication` resolves a lane's effective publication scope and the env
var the token is injected under, per the netkingdom publication-scope policy
(see [docs/publication-scope-policy.md](publication-scope-policy.md)).
`<ref>` is a catalog id or a decision/CCR ref (matched against
`approval.decision_ref`). `plan` and `apply --dry-run` never mutate OpenBao.
For decision-gated lanes they may render with `decision: <none>` when the
approval object is not reachable. Production live commands remain disabled
until State Hub exposes the durable exact-action authorization object. A legacy
local decision is accepted for a prod-labeled lane only with
`SECRETS_ENGINE_UNSAFE_DEMO=1`, an empty Hub URL, and loopback OpenBao; the demo
scripts set those three conditions themselves.
`handoff` is for `kind: auth-capability` lanes such as `warden-sign`. It mints a
fresh AppRole `secret_id` and writes `role_id` plus `secret_id` to caller-chosen
mode-0600 files outside Git worktrees. It never prints the `secret_id`; use the
resulting files only for attended out-of-band delivery.
`revoke` currently means **deactivate native secrets-engine access**. For
engine-managed delivery auth it deletes the lane AppRole and policy; it always
preserves KV metadata/values, existing externally managed auth, and workload
delivery. There is no general KV-destruction command. `provision` uses
CAS-aware create/patch behavior with a strict temporary input reference: values
are absent from argv, sibling fields are preserved, and stale writes fail.
With no `--field`, `verify` checks every declared KV field positively and runs
one path-level negative probe. KV denial requires `--negative-token-file` with a
real unrelated identity's mode-0600 token file outside Git; absence fails the
check without calling OpenBao. `route` likewise requires every declared field
and reports only missing field names. An explicit `--field` narrows positive
verification; production use will bind such subsets to the action approval.
Explicit lifecycle commands separate intent:
- `suspend` removes the engine-managed AppRole but preserves policy and KV;
- `deactivate` removes the engine-managed AppRole and policy but preserves KV;
- `destroy --dry-run` renders deactivation plus irreversible KV metadata
deletion. Live execution is fail-closed even with exact catalog-id
confirmation until `SECRETS-WP-0007-T04` supplies the canonical exact-action
approval contract.
All three preserve externally managed auth and workload delivery. The legacy
`revoke` command is a compatibility alias for safe native deactivation, never
KV destruction.
Exec and verification AppRole logins are scoped sessions. The issued token
self-revokes on every exit path before exec starts (or when verification ends),
and evidence stores only a short accessor fingerprint plus cleanup outcome.
`audit` is a read-only, local evidence summary. It reports action and result
counts, canonical decision references, session cleanup outcomes, and State Hub
delivery outcomes for one cataloged lane. Its parser allowlists those fields and
does not echo arbitrary JSONL detail. State Hub failures are recorded locally as
append-only companion receipts. Edge-relay queued receipts and their non-secret
outbox ids are recorded too; replay remains an operator/State Hub responsibility.
Live `apply`, `provision`, `verify`, `handoff`, `exec`, `revoke`, `suspend`, and
`deactivate` share one evidence guard. It records an attempt before lane approval
is resolved and a terminal outcome on every normal or exceptional exit. Rejected
approval and backend/input failures record only approval state and exception
class, not exception text. This evidence describes what the CLI observed; it is
not an authorization decision and does not replace OpenBao audit logs.
## Exit codes
| Code | Meaning |
| --- | --- |
| 0 | success |
| 2 | catalog error (missing/invalid lane) |
| 3 | decision error (unapproved / superseded / missing) |
| 4 | policy guard error (out-of-stage / wildcard / broad admin) |
| 5 | backend error (OpenBao unreachable / failed) |
| 6 | provisioning error (bad file mode / inside repo / missing field) |
| 7 | verification failed |
| 8 | delivery error |
## End-to-end demo
```bash
SECRETS_ENGINE_HUB_URL="" bash scripts/demo-e2e.sh
```
Boots a throwaway in-memory OpenBao dev server and runs the whole pilot chain:
plan → apply → provision → verify(+/-) → exec (npm-config injection) → route →
revoke. Nothing is persisted; the token is a throwaway local string.
## npm publish wiring (dry-run)
```bash
bash scripts/npm-publish-demo.sh
```
Boots a throwaway OpenBao, applies + provisions the lane with a *fake* token, and
runs a **real `npm publish --dry-run`** through `secrets-engine exec` against a
scratch package. Proves npm in the child resolves its auth token from the temp
`.npmrc` secrets-engine injected, builds the tarball, and reaches the publish
step — while the parent shell never holds the token. For a real publish,
provision a real npm automation token the same way and drop `--dry-run`.
## Auth-capability: warden-sign
```bash
# Preview the non-KV policy/AppRole lane; no mutation.
SECRETS_ENGINE_HUB_URL="" secrets-engine apply warden-sign --stage prod --dry-run
# Live apply and handoff require an approved decision and an out-of-repo
# bootstrap token file. Values are written to files, not stdout.
BAO_ADDR=https://bao.coulomb.social \
secrets-engine handoff warden-sign --stage prod \
--bootstrap-token-file ~/.secrets-engine/bootstrap/prod-warden-sign.token \
--role-id-file ~/.secrets-engine/handoff/warden-sign.role_id \
--secret-id-file ~/.secrets-engine/handoff/warden-sign.secret_id
```
See [warden-sign-auth-capability.md](warden-sign-auth-capability.md) for the full
runbook and State Hub pointer payload.
## Pilot closeout: whynot-design real publish
```bash
scripts/whynot-real-publish-preflight.sh
```
The real publish path is documented in [whynot-design-real-publish-closeout.md](whynot-design-real-publish-closeout.md).
## Pilot: whynot-design npm publish
```bash
# 1. inspect the approved decision
secrets-engine decision inspect whynot-design-npm-publish
# 2. preview the OpenBao changes (no mutation)
secrets-engine plan whynot-design-npm-publish --stage prod
# 3. apply policy + approle
secrets-engine apply whynot-design-npm-publish --stage prod
# 4. provision the token from a mode-0600 file OUTSIDE the repo
secrets-engine provision whynot-design-npm-publish --stage prod \
--field npm_token --from-file ~/.secrets-engine/whynot.token
# 5. prove access without printing the value
secrets-engine verify whynot-design-npm-publish --field npm_token --positive --negative \
--negative-token-file /secure/path/unrelated.token
# 6. publish with the token injected into the child only
secrets-engine exec --catalog whynot-design-npm-publish -- npm publish
```