diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6c770df0..c19c5e76 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -256,6 +256,14 @@ jobs: if: startsWith(github.ref, 'refs/tags/') && vars.PUBLISH_MCP_REGISTRY == 'true' runs-on: ubuntu-latest timeout-minutes: 15 + # Named in npm's trusted-publisher config, which checks it as an OIDC + # claim: publishing fails unless the two match. Restrict this environment + # to the tag pattern `v*` so a push to any branch cannot reach npm. + # + # Deliberately NOT the existing `release` environment — that one is scoped + # to the `main` branch, and this job runs on a tag ref, so it would be + # rejected outright. + environment: npm-publish permissions: contents: read id-token: write # mints the OIDC token npm exchanges for publish rights diff --git a/dev-docs/CI.md b/dev-docs/CI.md index 5752933d..d4ff2551 100644 --- a/dev-docs/CI.md +++ b/dev-docs/CI.md @@ -248,7 +248,21 @@ Two jobs at the end of `release.yml` publish the `bomly-mcp` npm wrapper and the The version must match a GitHub release that is already published: the package's postinstall step downloads that release's archive, so publishing ahead of it ships a package that cannot install. -2. **Set the trusted publisher** at `https://www.npmjs.com/package/bomly-mcp/access` → repository `bomly-dev/bomly-cli`, workflow `release.yml`. Configure it on the *package* access page; the account-level packages page does not have this setting. +2. **Set the trusted publisher** at `https://www.npmjs.com/package/bomly-mcp/access`. Configure it on the *package* access page; the account-level packages page does not have this setting. + + | Field | Value | + | --- | --- | + | Publisher | GitHub Actions | + | Organization / Repository | `bomly-dev` / `bomly-cli` | + | Workflow filename | `release.yml` | + | Environment name | `npm-publish` | + | Allowed actions | `npm publish` only | + + The environment name is checked as an OIDC claim, so it must match the `environment:` on the `publish-npm` job exactly. The `npm-publish` environment is restricted to the `v*` **tag** pattern, so a push to any branch cannot reach npm. + + Do **not** use the existing `release` environment here: it is scoped to the `main` branch, and this job runs on a tag ref, so the deployment would be rejected. + + Under **Publishing access**, choose *Require two-factor authentication and disallow bypass 2fa tokens*. Trusted publishers keep working under it, and it blocks token-based publishing entirely — which is the point, since there is no token. 3. **Turn the jobs on:** diff --git a/dev-docs/SECURITY_ASSURANCE.md b/dev-docs/SECURITY_ASSURANCE.md index 64d88b34..5b1e6653 100644 --- a/dev-docs/SECURITY_ASSURANCE.md +++ b/dev-docs/SECURITY_ASSURANCE.md @@ -66,13 +66,26 @@ only their required permissions. | `scorecard.yml` | `read-all` | Scorecard adds `security-events: write` and `id-token: write` to publish signed results | | `bomly-guard.yml` | `contents: read` | Guard adds pull-request, issue, and security-event writes for comments and SARIF | | `update-smoke-goldens.yml` | `contents: read` | The final job adds `contents: write` and `pull-requests: write` to publish reviewed goldens | -| `release.yml` | `contents: read` | Release jobs add only the contents, packages, actions, and OIDC permissions needed to publish artifacts and provenance | +| `release.yml` | `contents: read` | Release jobs add only the contents, packages, actions, and OIDC permissions needed to publish artifacts and provenance. `publish-npm` and `publish-mcp-registry` add `id-token: write` and use **no secret at all** — npm and the MCP Registry both authenticate by exchanging the workflow's OIDC identity (see [MCP Registry Publishing](CI.md#mcp-registry-publishing)) | Third-party actions are pinned. Long-lived secrets are passed only to the step that needs them. Release automation prefers short-lived, repository-scoped GitHub App tokens; the Windows package publication token is validated before release work starts and remains the documented exception. +npm publication holds no credential of any kind. It uses npm Trusted +Publishing, so npm exchanges the workflow's OIDC identity for publish rights. +The `publish-npm` job is bound to the `npm-publish` environment, which npm +verifies as an OIDC claim and which is restricted to the `v*` tag pattern, so +publishing cannot be reached from a branch push. The package additionally +requires two-factor authentication and disallows bypass-2FA tokens, so the +trusted publisher is the only automated path to the registry. + +`mcp-publisher` is downloaded into that same privileged job, so it is pinned by +version and verified twice before execution: against a pinned SHA-256, and with +`cosign verify-blob` against the upstream Sigstore bundle, asserting the +artifact was built by the MCP Registry's own tagged release workflow. + ## Evidence Changes The assurance work is split so each control can be reviewed independently: