secrets-engine/docs/publication-scope-policy.md
tegwick 5b48033bce feat(policy): netkingdom maturity-gated publication-scope policy
Token scope is now bound to package maturity, gated on netkingdom's own maturity:
- maturity-build -> gitea-wide, maturity-test -> org-wide, maturity-prod -> repo-scoped
  (scope narrows as stakes rise; broad tokens only for low-stakes build artifacts)
- the graduated table is DORMANT until netkingdom reaches production grade; until
  then every lane clamps to repo-scope, injected as NPM_AUTH_TOKEN (fail-safe)
- token env-var name signals blast radius: NPM_AUTH_TOKEN (repo default),
  NPM_AUTH_COULOMB_TOKEN (org), NPM_AUTH_GITEA_TOKEN (gitea), NPM_AUTH_WHYNOT_TOKEN
  (npm scope, defined but unused), NPM_AUTH_WHYNOTDESIGN (explicit repo)

netkingdom is at maturity-build today, so whynot-design resolves to repo-scope /
NPM_AUTH_TOKEN. Flip netkingdom_maturity to maturity-prod to activate graduation.

- policies/netkingdom-publication-scope.yaml: the policy data + gate
- publication_policy.py: load + resolve (clamp/active, env naming, override)
- exec delivery injects under the resolved env-var name (was fixed SE_NPM_TOKEN)
- catalog lane carries delivery_config.npm.maturity
- new CLI: `secrets-engine policy publication <lane>`
- docs/publication-scope-policy.md; tests for clamp, graduation, naming, override

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 13:14:46 +02:00

74 lines
3.4 KiB
Markdown

# netkingdom Publication-Scope Policy
**Owner:** netkingdom (security-infrastructure domain) · **Enforced by:** secrets-engine
**Data:** `policies/netkingdom-publication-scope.yaml` · **Code:** `src/secrets_engine/publication_policy.py`
## Idea
The scope of a publish token depends on the **maturity** of what is being
published. A repo's maturity translates into the maturity tag of the package it
ships, and each tag is restricted by the publication scope its token carries:
| Package maturity tag | Publication scope (when active) | Token env var | Why |
| --- | --- | --- | --- |
| `maturity-build` | **gitea-wide** | `NPM_AUTH_GITEA_TOKEN` | low-stakes build artifacts may use a broad, shared token |
| `maturity-test` | **org-wide** | `NPM_AUTH_COULOMB_TOKEN` | integration artifacts scoped to the org |
| `maturity-prod` | **repo-scoped** | `NPM_AUTH_TOKEN` (default) / `NPM_AUTH_WHYNOTDESIGN` (explicit) | high-stakes production publishes require tight, repo-scoped rights |
Scope **narrows as stakes rise** — the production token is the most tightly bound.
## The netkingdom maturity gate (why it's dormant today)
The graduated table above only takes effect once **netkingdom itself reaches
production grade** (`netkingdom_maturity: maturity-prod`). netkingdom is at
`maturity-build` today, so the policy is **dormant** and every lane **clamps to
the safest scope (`repo`)**, injected as `NPM_AUTH_TOKEN`.
This is fail-safe, not fail-open: until the policy machinery is itself
production-grade, we never hand out the broad (gitea/org-wide) tokens the
graduated table would otherwise allow.
```
$ secrets-engine policy publication whynot-design-npm-publish
netkingdom maturity: maturity-build (production_grade=False)
effective publication scope: repo (clamped from graduated table — fail-safe)
inject token as env var: NPM_AUTH_TOKEN
```
When netkingdom graduates, flip `netkingdom_maturity` to `maturity-prod` in the
policy file; the same lanes then resolve build→gitea, test→org, prod→repo
automatically. Nothing else changes.
## Token env-var naming convention
`NPM_AUTH[_<QUALIFIER>]_TOKEN`, where the qualifier signals the blast radius:
| Effective scope | Env var | Qualifier source |
| --- | --- | --- |
| repo (default) | `NPM_AUTH_TOKEN` | — (bare default) |
| repo (explicit) | `NPM_AUTH_WHYNOTDESIGN` | repo `whynot-design` |
| npm-scope / "project" | `NPM_AUTH_WHYNOT_TOKEN` | scope `@whynot`**defined, not used by the policy** |
| org | `NPM_AUTH_COULOMB_TOKEN` | org `coulomb` |
| gitea-wide | `NPM_AUTH_GITEA_TOKEN` | — |
The name is a **signal**, not the boundary. The real boundary is how the Gitea
token is minted (a dedicated bot account per grant is usually needed — Gitea
package tokens are user-scoped, not natively scope/org-scoped) plus the OpenBao
lane path + consumer policy. The catalog records the *intended* grant via
`delivery_config.npm.maturity`; a reviewer sees the intended blast radius even
when the backend can only enforce a coarser one.
## Lane configuration
```yaml
delivery_config:
npm:
registry: "https://gitea.coulomb.social/api/packages/coulomb/npm/"
scope: "@whynot"
package: "@whynot/design"
maturity: maturity-build # feeds the policy
# token_env: NPM_AUTH_WHYNOTDESIGN # optional explicit override
```
`secrets-engine exec` injects the resolved token under the resolved env-var name
into the npm child only; the parent shell never holds it.