Skip to content

Trusted Publishing

Thomas Neidhart edited this page Sep 1, 2026 · 3 revisions

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

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.

Before you start

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.

Register a trusted publisher

  1. Go to SettingsTrusted Publishers (/user-settings/trusted-publishers) and pick the namespace.
  2. Choose the provider. Which providers appear depends on what the registry has enabled; the dialog lists them.
  3. 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.

Publish from CI

GitHub Actions

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-publishing

GitLab CI/CD

GitLab 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

CLI options

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.

How the exchange works

  1. The workflow asks its CI system for an OIDC ID token, for an audience that is the registry's URL.
  2. ovsx posts it to POST /api/-/trusted-publishing/token, together with the namespace and extension name it intends to publish.
  3. The registry parses the token without trusting it to read the iss claim, and selects the provider implementation registered for that issuer.
  4. That provider fetches the issuer's OpenID Connect discovery document and JSON Web Key Set, and verifies the signature and standard claims. The aud claim must equal the audience the registry is configured for, and tokens carrying x5u, x5c, jku or jwk headers are rejected outright.
  5. The claims of interest are extracted and matched against the stored registration for that extension, as described below.
  6. On a match the registry issues a short-lived access token and returns it. ovsx uploads 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.

What the registry matches

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

Resurrection and rename

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 issued token

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.

Security model

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.

Changing or removing a registration

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.

For registry operators

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.org

The provider id is stored with every registration, so renaming an id hides the registrations made under it.

Further reading

Clone this wiki locally