Skip to content

Commit 4f36bac

Browse files
bomly-guyclaude
andauthored
feat(mcp): bind npm publishing to a tag-restricted environment (#359)
npm's trusted-publisher form takes an optional environment name and checks it as an OIDC claim, so it only works if the job declares the same one. Adds `environment: npm-publish` to publish-npm and creates that environment restricted to the `v*` tag pattern, so publishing to npm cannot be reached from a branch push. Deliberately not the existing `release` environment: it is scoped to the `main` branch, and this job runs on a tag ref, so pointing npm at it would have rejected every publish. Documents the exact field values for the npm form in dev-docs/CI.md (including choosing the strictest Publishing access option, which is safe because trusted publishers are exempt), and records in SECURITY_ASSURANCE.md that npm publication now holds no credential. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent 2f903c9 commit 4f36bac

3 files changed

Lines changed: 37 additions & 2 deletions

File tree

.github/workflows/release.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,14 @@ jobs:
256256
if: startsWith(github.ref, 'refs/tags/') && vars.PUBLISH_MCP_REGISTRY == 'true'
257257
runs-on: ubuntu-latest
258258
timeout-minutes: 15
259+
# Named in npm's trusted-publisher config, which checks it as an OIDC
260+
# claim: publishing fails unless the two match. Restrict this environment
261+
# to the tag pattern `v*` so a push to any branch cannot reach npm.
262+
#
263+
# Deliberately NOT the existing `release` environment — that one is scoped
264+
# to the `main` branch, and this job runs on a tag ref, so it would be
265+
# rejected outright.
266+
environment: npm-publish
259267
permissions:
260268
contents: read
261269
id-token: write # mints the OIDC token npm exchanges for publish rights

dev-docs/CI.md

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,21 @@ Two jobs at the end of `release.yml` publish the `bomly-mcp` npm wrapper and the
248248

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

251-
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.
251+
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.
252+
253+
| Field | Value |
254+
| --- | --- |
255+
| Publisher | GitHub Actions |
256+
| Organization / Repository | `bomly-dev` / `bomly-cli` |
257+
| Workflow filename | `release.yml` |
258+
| Environment name | `npm-publish` |
259+
| Allowed actions | `npm publish` only |
260+
261+
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.
262+
263+
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.
264+
265+
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.
252266

253267
3. **Turn the jobs on:**
254268

dev-docs/SECURITY_ASSURANCE.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,13 +66,26 @@ only their required permissions.
6666
| `scorecard.yml` | `read-all` | Scorecard adds `security-events: write` and `id-token: write` to publish signed results |
6767
| `bomly-guard.yml` | `contents: read` | Guard adds pull-request, issue, and security-event writes for comments and SARIF |
6868
| `update-smoke-goldens.yml` | `contents: read` | The final job adds `contents: write` and `pull-requests: write` to publish reviewed goldens |
69-
| `release.yml` | `contents: read` | Release jobs add only the contents, packages, actions, and OIDC permissions needed to publish artifacts and provenance |
69+
| `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)) |
7070

7171
Third-party actions are pinned. Long-lived secrets are passed only to the step
7272
that needs them. Release automation prefers short-lived, repository-scoped
7373
GitHub App tokens; the Windows package publication token is validated before
7474
release work starts and remains the documented exception.
7575

76+
npm publication holds no credential of any kind. It uses npm Trusted
77+
Publishing, so npm exchanges the workflow's OIDC identity for publish rights.
78+
The `publish-npm` job is bound to the `npm-publish` environment, which npm
79+
verifies as an OIDC claim and which is restricted to the `v*` tag pattern, so
80+
publishing cannot be reached from a branch push. The package additionally
81+
requires two-factor authentication and disallows bypass-2FA tokens, so the
82+
trusted publisher is the only automated path to the registry.
83+
84+
`mcp-publisher` is downloaded into that same privileged job, so it is pinned by
85+
version and verified twice before execution: against a pinned SHA-256, and with
86+
`cosign verify-blob` against the upstream Sigstore bundle, asserting the
87+
artifact was built by the MCP Registry's own tagged release workflow.
88+
7689
## Evidence Changes
7790

7891
The assurance work is split so each control can be reviewed independently:

0 commit comments

Comments
 (0)