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.
Model in one paragraph
Section titled “Model in one paragraph”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).
Configuration (environment variables)
Section titled “Configuration (environment variables)”The feature is off by default: when disabled, the token endpoint is not mapped and no provider discovery occurs.
| Variable | Meaning | Default |
|---|---|---|
NUGETKEEP_TRUSTED_PUBLISHING_ENABLED | Master on/off. | false |
NUGETKEEP_TRUSTED_PUBLISHING_AUDIENCE | The aud the CI token must carry. | nugetkeep |
NUGETKEEP_TRUSTED_PUBLISHING_TTL_MINUTES | Default minted-key lifetime. | 15 |
NUGETKEEP_TRUSTED_PUBLISHING_GITHUB_ENABLED | Enable the GitHub Actions provider. | false |
NUGETKEEP_TRUSTED_PUBLISHING_GITHUB_ISSUER | Override the GitHub issuer (GitHub Enterprise). | https://token.actions.githubusercontent.com |
NUGETKEEP_TRUSTED_PUBLISHING_GITLAB_ENABLED | Enable the GitLab provider. | false |
NUGETKEEP_TRUSTED_PUBLISHING_GITLAB_ISSUER | GitLab instance issuer. | https://gitlab.com |
Registering a publisher
Section titled “Registering a publisher”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.
CI usage
Section titled “CI usage”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 tokensteps: - 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.
Security notes
Section titled “Security notes”- 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.