Skip to content

Trusted publishing

Trusted publishing lets a CI pipeline publish to NuGetKeep without a long-lived push secret: the job presents its OIDC token to a token-exchange endpoint, NuGetKeep validates it against an admin-defined trusted-publisher policy, and mints a short-lived API key the job uses with stock dotnet nuget push.

An admin registers a trusted-publisher policy that names a CI provider + the owner/repo (and optional environment) allowed to publish, plus the target feed. On the first successful exchange the policy records the provider’s immutable numeric IDs and becomes Active — thereafter a name match is not enough, the immutable IDs must also match, defeating a delete-and-recreate “resurrection” hijack. Each exchange validates the token’s signature (via the issuer’s JWKS), issuer, audience, and lifetime, then mints a Publisher-scoped key for the policy’s feed (expiring in minutes).

The feature is off by default: when disabled, the token endpoint is not mapped and no provider discovery occurs.

VariableMeaningDefault
NUGETKEEP_TRUSTED_PUBLISHING_ENABLEDMaster on/off.false
NUGETKEEP_TRUSTED_PUBLISHING_AUDIENCEThe aud the CI token must carry.nugetkeep
NUGETKEEP_TRUSTED_PUBLISHING_TTL_MINUTESDefault minted-key lifetime.15
NUGETKEEP_TRUSTED_PUBLISHING_GITHUB_ENABLEDEnable the GitHub Actions provider.false
NUGETKEEP_TRUSTED_PUBLISHING_GITHUB_ISSUEROverride the GitHub issuer (GitHub Enterprise).https://token.actions.githubusercontent.com
NUGETKEEP_TRUSTED_PUBLISHING_GITLAB_ENABLEDEnable the GitLab provider.false
NUGETKEEP_TRUSTED_PUBLISHING_GITLAB_ISSUERGitLab instance issuer.https://gitlab.com

As an admin, open /admin/trusted-publishers (or POST /api/admin/trusted-publishers with an admin API key): choose the provider, enter the owner/repo (and optional environment), the target feed, an optional package-glob, and the key lifetime. The policy starts Provisional and flips to Active on its first successful exchange.

The token endpoint is POST /api/oidc/token. The CI job obtains its OIDC token (audience = NUGETKEEP_TRUSTED_PUBLISHING_AUDIENCE), exchanges it for a short-lived key, then pushes. GitHub Actions sketch:

permissions:
id-token: write # allow the job to mint an OIDC token
steps:
- id: token
run: echo "value=$(curl -sf -X POST "$NK/api/oidc/token" \
-H "Authorization: Bearer $(curl -sf "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=nugetkeep" \
-H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" | jq -r .value)" \
| jq -r .apiKey)" >> "$GITHUB_OUTPUT"
env: { NK: https://your-nugetkeep }
- run: dotnet nuget push *.nupkg --api-key ${{ steps.token.outputs.value }} --source "$NK/v3/index.json"

The endpoint accepts the token via a Bearer header or a { "token": "..." } body and returns { "apiKey": "...", "expiration": "..." }. GitLab CI and Azure Pipelines use their own id_token/workload-identity mechanisms to obtain the OIDC token; the exchange is identical.

  • Audience is mandatory — a token whose aud ≠ the configured audience is rejected.
  • Issuer-pinned + RS256 — only enabled providers’ exact issuers are accepted; signatures are validated against the issuer’s JWKS with RS256 pinned (blocks algorithm-confusion).
  • Resurrection defense — an Active policy requires the token’s immutable IDs to equal the bound ones.
  • Bounded blast radius — the minted key is push-only, Publisher-role, scoped to the policy’s feed, expires in minutes, and (if the policy sets a package glob) is further restricted to matching ids.

GitHub Actions and GitLab CI are fully supported. Azure DevOps is present but its claim mapping is unconfirmed and it is not enabled by default.