secrets-engine/docs/cli.md
tegwick afd1c8e593
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Add response-wrapped operator handoff
secrets-engine wrap writes a single-use OpenBao wrap token to a mode-0600
out-of-repo file and never prints it. KV reads and AppRole secret_ids are
wrapped with a 15m TTL cap. Unwrapped secret payloads fail closed.
Production wrap remains fail-closed.

Assistant: grok
Assistant-Session: 01a05f07-ae72-7781-9fcb-19efd61add00
2026-09-02 08:12:42 +02:00

10 KiB

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. 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

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) Named env OpenBao token; never a fallback from service-jwt
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
SECRETS_ENGINE_KEYCAPE_TOKEN_URL (unset) KeyCape token endpoint for service-jwt
SECRETS_ENGINE_KEYCAPE_ISSUER (unset) KeyCape issuer; must match the JWT login contract
SECRETS_ENGINE_KEYCAPE_CLIENT_SECRET_FILE (unset) mode-0600 out-of-repo client secret
SECRETS_ENGINE_OPENBAO_JWT_LOGIN (unset) platform JWT mount/role contract YAML

Commands

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 wrap <catalog-id> --out F [--ttl 15m]
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 session revoke --accessor-file F [--stage stage]
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]
secrets-engine secret-use snapshot [--catalog-id 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).

<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.

wrap writes a single-use OpenBao response-wrap token to --out (mode 0600, outside Git) and never prints it. KV lanes wrap a path read; auth-capability lanes wrap a secret_id. TTL max 15m. Production live wrap stays fail-closed.

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

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 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

# 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 for the full runbook and State Hub pointer payload.

Pilot closeout: whynot-design real publish

scripts/whynot-real-publish-preflight.sh

The real publish path is documented in whynot-design-real-publish-closeout.md.

Pilot: whynot-design npm publish

# 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