Introduction
Publishing a .NET package from CI/CD traditionally means storing a NuGet API key somewhere in the build system.
That model works, but it creates a credential-management problem.
The key has to be protected, rotated, scoped, replaced, and eventually revoked. If it leaks, someone may be able to publish packages with the permissions assigned to it.
NuGet is moving toward a different model with Trusted Publishing. Instead of keeping a long-lived API key in GitHub Actions, the workflow uses an OpenID Connect (OIDC) identity token to prove where the workflow came from. NuGet then exchanges that identity for a short-lived API key that can be used to publish the package.
This changes the security architecture from:
GitHub Actions
|
v
Long-Lived API Key
|
v
NuGet
to:
GitHub Actions
|
v
OIDC Identity
|
v
NuGet Trusted Publishing Policy
|
v
Temporary Credential
|
v
NuGet
For new .NET package publishing pipelines, this is a much stronger model than storing a reusable publishing secret.
Why Long-Lived NuGet API Keys Are a Problem
An API key is essentially a password for package publishing.
If it is stored as:
GitHub Secret
|
v
NUGET_API_KEY
the release workflow can use it whenever it needs to publish.
The problem is that the credential exists outside the actual publishing event.
A compromised key can potentially be reused until it expires or is revoked.
NuGet has introduced shorter API-key lifetimes as part of its broader supply-chain security changes. Starting August 17, 2026, newly created API keys are limited to 30 days, while existing keys created before that date are scheduled to expire on November 1, 2026.
That makes manual long-lived credential management increasingly impractical for automated publishing.
What Trusted Publishing Changes
Trusted Publishing removes the need for a permanent publishing secret.
The workflow requests an OIDC token from GitHub Actions.
That token contains identity information about the workflow and repository. NuGet validates the token and checks it against a Trusted Publishing policy configured on nuget.org. If the identity matches the policy, NuGet issues a temporary API key for the workflow.
The process is:
1. GitHub Actions starts
|
v
2. Workflow requests OIDC token
|
v
3. Token sent to NuGet
|
v
4. NuGet validates identity
|
v
5. Trusted policy is evaluated
|
v
6. Temporary API key issued
|
v
7. dotnet nuget push
The temporary key is valid for one hour, and each OIDC token can be exchanged only once for a temporary key.
The Security Model
The key idea is that NuGet trusts an identity, not a stored secret.
Consider this repository:
contoso/
└── payments-sdk/
└── .github/
└── workflows/
└── release.yml
Instead of saying:
"Anyone possessing this API key can publish."
the policy effectively says:
"Trust this specific GitHub repository
and this specific workflow to publish."
You can optionally restrict the policy to a GitHub Actions environment as well.
That creates a much smaller trust boundary.
Trusted Publishing Policy
A GitHub Trusted Publishing policy identifies the source of the publishing request.
For example:
Repository Owner:
contoso
Repository:
payments-sdk
Workflow:
release.yml
Environment:
release
The workflow file should be specified by filename, rather than including the .github/workflows/ directory path.
The resulting trust relationship looks like:
NuGet Package Owner
|
v
Trusted Publishing Policy
|
+--> Repository Owner
|
+--> Repository
|
+--> Workflow
|
+--> Environment
This is considerably more precise than a shared publishing password.
GitHub Actions OIDC
The GitHub Actions job needs permission to request an OIDC token.
That permission is:
permissions:
id-token: write
A minimal publishing job therefore looks like:
jobs:
publish:
runs-on: ubuntu-latest
permissions:
id-token: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: "8.x"
The important part is not the .NET version.
It is the OIDC permission.
Without:
id-token: write
the workflow cannot request the identity token required by Trusted Publishing.
Getting the Temporary NuGet Credential
NuGet provides a GitHub Actions login action for the OIDC exchange.
The workflow can use:
- name: NuGet login
uses: NuGet/login@v1
id: login
with:
user: ${{ secrets.NUGET_USER }}
The action exchanges the GitHub identity for a temporary NuGet API key.
The resulting credential is exposed through the action output:
steps.login.outputs.NUGET_API_KEY
NuGet's current documentation recommends using the NuGet profile name for the user value rather than the account email address.
Publishing the Package
Once the temporary credential has been obtained, the normal dotnet nuget push command can be used.
- name: Publish package
run: >
dotnet nuget push ./artifacts/*.nupkg
--api-key "${{ steps.login.outputs.NUGET_API_KEY }}"
--source "https://api.nuget.org/v3/index.json"
The important difference is where the API key came from.
Traditional workflow:
GitHub Secret
|
v
Permanent API Key
|
v
dotnet nuget push
Trusted Publishing:
OIDC
|
v
Temporary API Key
|
v
dotnet nuget push
The package publishing command itself does not need to know how the credential was created.
A Complete .NET Release Workflow
A realistic workflow can combine build, test, pack, authentication, and publishing.
name: Publish NuGet Package
on:
workflow_dispatch:
jobs:
build-and-publish:
runs-on: ubuntu-latest
permissions:
id-token: write
environment: release
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: "8.x"
- name: Restore
run: dotnet restore
- name: Build
run: dotnet build --configuration Release --no-restore
- name: Test
run: dotnet test --configuration Release --no-build
- name: Pack
run: >
dotnet pack
--configuration Release
--no-build
--output ./artifacts
- name: NuGet login
uses: NuGet/login@v1
id: login
with:
user: ${{ secrets.NUGET_USER }}
- name: Publish
run: >
dotnet nuget push ./artifacts/*.nupkg
--api-key "${{ steps.login.outputs.NUGET_API_KEY }}"
--source "https://api.nuget.org/v3/index.json"
--skip-duplicate
The resulting architecture is:
Checkout
|
v
Restore
|
v
Build
|
v
Test
|
v
Pack
|
v
GitHub OIDC
|
v
NuGet Trusted Publishing
|
v
Temporary Credential
|
v
NuGet Push
Why the Credential Should Be Requested Late
The temporary API key is valid for only one hour. NuGet specifically recommends requesting it shortly before publishing so that it does not expire before the push occurs.
This means authentication should generally happen after expensive operations such as:
Restore
Build
Test
Pack
rather than at the beginning.
Prefer:
Build
|
Test
|
Pack
|
Login
|
Push
over:
Login
|
Build
|
Test
|
Pack
|
Push
The first design minimizes the time between credential issuance and package publishing.
Use GitHub Environments
A production package should not necessarily be published by every workflow execution.
GitHub environments can provide another control boundary:
environment: release
The NuGet Trusted Publishing policy can also be restricted to that environment.
The architecture becomes:
Pull Request
|
X
|
Release Workflow
|
v
release Environment
|
v
Trusted Publishing
|
v
NuGet
This prevents an arbitrary development workflow from automatically becoming a trusted package publisher.
Why Workflow Restrictions Matter
Imagine a repository containing:
.github/workflows/
├── build.yml
├── test.yml
├── release.yml
└── experimental.yml
Only the release workflow should normally have permission to publish.
The Trusted Publishing policy can identify:
release.yml
rather than trusting every workflow in the repository.
This follows the principle of least privilege.
Protect the Release Branch
Trusted Publishing does not replace normal repository security controls.
Consider this chain:

Join the conversation! Your thoughts help the community grow.