-
Notifications
You must be signed in to change notification settings - Fork 357
Trusted Publishing
Trusted publishing lets a CI workflow publish an extension without storing an Open VSX access token as a secret. Instead of a long-lived token, the workflow proves its identity to the registry with an OpenID Connect (OIDC) ID token issued by the CI system itself, and the registry exchanges that proof for a short-lived token that may publish exactly one extension.
The model follows the OpenSSF Securing Software Repositories WG guidance, Trusted Publishers for All Package Repositories, which is also what PyPI, npm, RubyGems, NuGet, crates.io and pub.dev implement.
Contents
- Why use it
- Before you start
- Register a trusted publisher
- Publish from CI
- How the exchange works
- What the registry matches
- The issued token
- Security model
- Changing or removing a registration
- For registry operators
A personal access token stored in CI is long-lived, may publish anything its owner may publish, and has to be rotated by hand. It is only as safe as every place it has been copied to.
A trusted publisher replaces it with a registration that says this specific workflow, in this specific repository, may publish this specific extension. There is no secret to leak, nothing to rotate, and a token the registry issues under it expires within minutes and is scoped to a single extension.
To register a trusted publisher you need:
- Ownership of the namespace. Being a contributor is not enough — see Namespace Access for how to claim ownership.
- A signed Publisher Agreement, the same one that ordinary publishing requires (see Publishing Extensions).
- The extension to exist, with at least one active version. A trusted publisher is registered for an extension, so the first version has to be published the ordinary way, with an access token. Registrations for extensions that do not exist yet are rejected.
Each extension can have one trusted publisher at a time. A workflow that publishes several extensions needs a registration per extension, all of which may point at the same workflow.
The extension has to stay active. The requirement is not only about registering: every token exchange looks the extension up among the active ones. Delete every version of an extension and its registration stops working — the exchange answers "No trusted publisher matches the presented token" even though the registration is still listed on the settings page. It starts working again as soon as a version is active again. Deleting only some versions changes nothing.
- Go to Settings → Trusted Publishers (
/user-settings/trusted-publishers) and pick the namespace. - Choose the provider. Which providers appear depends on what the registry has enabled; the dialog lists them.
- Fill in the fields that identify the workflow and confirm.
GitHub Actions
| Field | Meaning | Example |
|---|---|---|
| Organization or User name | Owner of the repository | octo-org |
| Repository name | Repository the workflow lives in | octo-repo |
| Workflow filename | Filename only, must exist in .github/workflows/
|
publish.yml |
| Environment name | Optional. When set, only runs in that environment are trusted | publish |
GitLab CI/CD
| Field | Meaning | Example |
|---|---|---|
| Namespace | Group or user the project belongs to | my-group |
| Project name | Project the pipeline runs in | my-project |
| Top-level CI filename | Filename of the pipeline definition | .gitlab-ci.yml |
| Environment name | Optional. When set, only jobs in that environment are trusted | production |
On confirmation the registry resolves the names against the provider's API and stores the resulting numeric IDs alongside them. See Resurrection and rename for why.
A registration is not tied to a branch or tag: any ref that runs the registered workflow file is trusted. Pin an environment if you need publishing restricted further.
The job needs the id-token: write permission; the ovsx CLI detects the rest.
permissions:
contents: read
id-token: write
jobs:
publish:
runs-on: ubuntu-latest
# only needed if the trusted publisher pins an environment
environment: publish
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
- run: npm ci
- run: npx ovsx publish --trusted-publishingGitLab hands the ID token to the job as a variable, which ovsx reads from OVSX_ID_TOKEN:
publish:
id_tokens:
OVSX_ID_TOKEN:
aud: https://open-vsx.org
script:
- npx ovsx publish --trusted-publishing| Option | Environment variable | Meaning |
|---|---|---|
--trusted-publishing |
Require trusted publishing; fail if no ID token can be obtained. Without it, trusted publishing is used whenever an ID token is available and no access token was given | |
--idToken <token> |
OVSX_ID_TOKEN |
Pass the ID token explicitly, for CI systems that expose it as a variable |
--oidcAudience <aud> |
OVSX_OIDC_AUDIENCE |
Audience to request for the ID token; defaults to the registry URL |
A --pat (or OVSX_PAT) always takes precedence, so an existing workflow keeps publishing with its access token until you remove it.
- The workflow asks its CI system for an OIDC ID token, for an audience that is the registry's URL.
-
ovsxposts it toPOST /api/-/trusted-publishing/token, together with the namespace and extension name it intends to publish. - The registry parses the token without trusting it to read the
issclaim, and selects the provider implementation registered for that issuer. - That provider fetches the issuer's OpenID Connect discovery document and JSON Web Key Set, and verifies the signature and standard claims. The
audclaim must equal the audience the registry is configured for, and tokens carryingx5u,x5c,jkuorjwkheaders are rejected outright. - The claims of interest are extracted and matched against the stored registration for that extension, as described below.
- On a match the registry issues a short-lived access token and returns it.
ovsxuploads with it, and never writes it to the token store.
A token that verifies but matches nothing answers with a generic "No trusted publisher matches the presented token" — the same message whether the namespace, the extension or the registration was the mismatch, so the endpoint cannot be used to probe which extensions have a registration.
The registration stores immutable IDs as well as the names you typed, and matching is done on the IDs:
| Provider | Claims that must match |
|---|---|
| GitHub Actions |
repository_id, repository_owner_id, workflow_ref (ignoring the trailing @<ref>), and environment when the registration pins one |
| GitLab CI/CD |
project_id, namespace_id, ci_config_ref_uri (ignoring the trailing @<ref>), and environment when the registration pins one |
Owner, repository and project names are mutable: they can be renamed or deleted and re-registered by somebody else. Numeric IDs cannot. Because the registry resolves and stores the IDs when you create the registration, a repository that is deleted and re-created under the same name by another party gets a new ID and no longer matches — the registration simply stops working instead of trusting the impostor.
The workflow filename has no ID and is matched by name, which is why the file itself must stay under your control.
The token the exchange returns is an ordinary registry access token with three restrictions:
- it expires within minutes (five by default),
- it is scoped to the one extension the registration was made for, and
- it acts as the user who created the registration, so publishing shows that user as the publisher.
Versions published this way are marked on the extension page: a rocket icon next to Published by means the version came from a trusted publishing workflow rather than a personal access token.
Trusted publishing removes the long-lived secret, not every risk. Two things stay your responsibility, as the OpenSSF guidance puts it:
- The tokens are still sensitive. Both the OIDC ID token and the token the registry issues can publish until they expire. Do not print them, and treat CI logs accordingly.
- A registration is trust in external state. Whoever can change the registered workflow file, or can run it, can publish the extension. Protect the branch, review changes to the workflow, and prefer a pinned environment with required reviewers when the extension warrants it.
A registration cannot be edited — delete it and create a new one. Both are on the same Trusted Publishers settings page. Deleting takes effect on the next exchange, which then fails and stops CI publishing until a new registration exists; a token issued moments earlier stays valid until it expires.
Delete a registration when the repository moves, when the workflow file is renamed, or when the extension should no longer be published from CI.
The registry deletes registrations by itself in two cases, since a registration must not outlive what it depends on:
- Its author stops being an owner of the namespace — by being demoted to contributor, by being removed from the namespace, or by having their contributions revoked by an administrator. Only that person's registrations go; the other owners' remain. Publishing tokens already issued under a deleted registration stop working immediately.
- The extension is purged. Deleting an extension's versions leaves the registration in place (inactive, as described above); purging the extension outright removes it.
Trusted publishing is off by default. The relevant server properties:
| Property | Default | Meaning |
|---|---|---|
ovsx.trusted-publishing.enabled |
false |
Enables the feature |
ovsx.trusted-publishing.audience |
${ovsx.webui.url} |
The aud claim that ID tokens must carry |
ovsx.trusted-publishing.token-expiration |
PT5M |
Lifetime of the issued publishing token. Must be positive — issued tokens always expire |
ovsx.trusted-publishing.active-providers |
github |
Comma-separated provider ids that may be used |
ovsx.trusted-publishing.forbidden-jwt-headers |
x5u,x5c,jku,jwk |
JWT headers that cause a token to be rejected |
ovsx.trusted-publishing.gitlab.<id>.name / .url / .issuer
|
gitlab → gitlab.com |
Defines a GitLab instance as a provider; issuer defaults to url
|
GitHub is a single, built-in provider. GitLab instances are configuration, so any instance — the public one, the Eclipse Foundation's, a self-hosted one — is added by declaring it and listing its id under active-providers:
ovsx:
trusted-publishing:
enabled: true
active-providers: github,eclipse-gitlab
gitlab:
eclipse-gitlab:
name: Eclipse GitLab
url: https://gitlab.eclipse.orgThe provider id is stored with every registration, so renaming an id hides the registrations made under it.
- Trusted Publishers for All Package Repositories — the OpenSSF guidance this implementation follows
- Publishing Extensions — publishing with an access token
- Namespace Access — claiming ownership of a namespace
ovsxCLI documentation