secrets-engine/docs/publication-scope-policy.md

75 lines
3.4 KiB
Markdown
Raw Normal View History

# 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://forgejo.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.