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:
Attacker
|
v
Modify Release Workflow
|
v
Trusted Workflow Executes
|
v
Package Published
Therefore, protect the branch containing the publishing workflow.
Use controls such as:
Protected branch
Required reviews
CODEOWNERS
Environment protection
Restricted workflow changes
The goal is to ensure that changing the publishing workflow is itself a controlled operation.
Repository Ownership Matters
Trusted Publishing policies are associated with a package owner.
NuGet's current implementation allows the policy to be owned by an individual user or an organization. Organization ownership is generally more appropriate for packages maintained by a team.
Consider:
Individual Ownership
|
v
Developer Account
|
v
Package
versus:
Organization Ownership
|
v
Team
|
v
Package
For an organization-maintained library, organization ownership avoids making the publishing trust relationship dependent on a single developer's account.
What Happens When Organization Membership Changes?
Trusted Publishing policies account for ownership changes.
NuGet documents that if the creator of an organization-owned policy loses organization membership, the policy becomes inactive. Restoring the required membership can reactivate it. An organization becoming inactive can also disable the policy.
This is an important operational consideration.
The publishing identity is not independent of the package owner's lifecycle.
Private Repository Activation
There is another security mechanism worth understanding.
For some private GitHub repositories, a newly created Trusted Publishing policy can initially enter a temporary activation period. NuGet documents a seven-day pending activation window and explains that successful publishing provides repository and owner identifiers that help protect against repository deletion and recreation attacks.
The important lesson is that repository names alone are not sufficient for strong identity.
A secure trust relationship should be tied to the actual repository identity.
The Resurrection Attack Problem
Consider this scenario:
Original Repository
|
v
trusted-repository
|
X
Repository Deleted
|
v
Attacker Creates Same Name
|
v
trusted-repository
If trust were based only on the repository name, the attacker could potentially impersonate the original publishing source.
NuGet's activation mechanism is designed to obtain repository and owner identifiers from GitHub's identity information before the policy becomes permanently active.
This is an important distinction between:
Name-based trust
and:
Identity-based trust
What Secrets Still Exist?
Moving to Trusted Publishing does not necessarily mean your workflow contains zero secrets.
For example, NuGet's documentation recommends supplying the NuGet username through a GitHub secret.
The important difference is:
Old Model
NUGET_USER
NUGET_API_KEY
versus:
Trusted Publishing
NUGET_USER
GitHub OIDC Identity
Temporary NuGet Credential
The high-value reusable publishing secret has been removed.
That is the main security improvement.
Traditional API Keys vs Trusted Publishing
| Area | Traditional API Key | Trusted Publishing |
|---|---|---|
| Permanent publishing secret | Yes | No |
| GitHub Secret for API key | Required | Not required |
| OIDC | No | Yes |
| Credential lifetime | Limited and increasingly short | About 1 hour |
| Rotation | Required | Not required |
| Workflow identity | Indirect | Explicit |
| Repository restriction | Limited to key scope | Policy-based |
| Environment restriction | Separate GitHub control | Can be included in policy |
| Secret exposure risk | Higher | Lower |
| CI/CD suitability | Legacy-compatible | Preferred |
NuGet explicitly recommends Trusted Publishing as a safer approach than managing long-lived API keys.
Migrating an Existing Pipeline
If your current workflow looks like:
- name: Publish
run: >
dotnet nuget push ./artifacts/*.nupkg
--api-key "${{ secrets.NUGET_API_KEY }}"
--source "https://api.nuget.org/v3/index.json"
you do not need to redesign the entire build pipeline.
Change the authentication layer.
Before
Build
|
Test
|
Pack
|
GitHub Secret
|
API Key
|
NuGet
After
Build
|
Test
|
Pack
|
GitHub OIDC
|
NuGet Login
|
Temporary Key
|
NuGet
The package build remains largely unchanged.
Migration Checklist
[ ] Identify packages published by GitHub Actions
[ ] Identify current API-key-based workflows
[ ] Create Trusted Publishing policy
[ ] Select organization or user ownership
[ ] Restrict repository
[ ] Restrict workflow file
[ ] Restrict environment where appropriate
[ ] Add id-token: write
[ ] Add NuGet/login@v1
[ ] Store NuGet username securely
[ ] Move login close to publishing
[ ] Test package publication
[ ] Verify package ownership
[ ] Verify workflow restrictions
[ ] Remove old API key
[ ] Confirm no workflow still depends on it
Common Mistakes
Giving OIDC Permission to Every Job
Keep:
permissions:
id-token: write
at the narrowest appropriate job level.
The build job does not need an OIDC token if only the publishing job requires it.
Authenticating at the Start
The temporary credential has a short lifetime. Request it close to publication.
Trusting the Entire Repository
Restrict the policy to the specific workflow that should publish.
Ignoring Environments
For production publishing, an environment can provide an additional protection layer.
Keeping the Old API Key Forever
Once Trusted Publishing is verified, remove the old credential.
A migration is incomplete if both systems remain unnecessarily active.
Treating OIDC as a Secret
An OIDC token is an identity assertion, not a replacement password that should be manually copied around.
Let GitHub Actions issue it to the workflow.
Testing the Migration
Before removing the old credential, test the new pipeline.
Use a controlled release process:
Trusted Policy
|
v
Test Workflow
|
v
OIDC Login
|
v
Temporary Key
|
v
Package Push
|
v
Verify NuGet Package
Check:
Authentication succeeds
Package is accepted
Package owner is correct
Version is correct
Workflow is correctly restricted
Environment restrictions work
No API key is stored in repository secrets
Only after successful validation should the old publishing credential be removed.
Advantages and Disadvantages
Advantages
Eliminates long-lived NuGet publishing API keys.
Removes API-key rotation from the CI/CD workflow.
Uses GitHub's OIDC identity.
Produces short-lived publishing credentials.
Allows trust to be restricted to repository and workflow identity.
Can incorporate GitHub environment restrictions.
Reduces the impact of credential leakage.
Fits naturally into automated package publishing.
Disadvantages
Requires initial Trusted Publishing configuration.
Teams must understand OIDC and federated identity.
Existing pipelines need migration.
Organization ownership and membership changes can affect policy activation.
Not every CI/CD environment may have the same level of support.
The publishing workflow itself still needs strong repository protection.
Final Thoughts
NuGet package publishing is moving away from the traditional model of storing reusable API keys in CI/CD systems.
The change is especially important now that new NuGet API keys are limited to 30 days and existing older keys are scheduled to expire on November 1, 2026.
Trusted Publishing provides a better architecture for GitHub Actions: the workflow proves its identity through OIDC, NuGet evaluates that identity against a narrowly defined policy, and a temporary credential is issued only when the workflow needs to publish. That temporary key lasts for about one hour rather than becoming another long-lived secret that developers have to rotate.
For new .NET package pipelines, the design goal should therefore be simple: build with normal CI/CD credentials, authenticate through workload identity, obtain a short-lived publishing credential, publish the package, and let the credential disappear.
That is a much cleaner supply-chain security model than continuously protecting and rotating another static API key.

Join the conversation! Your thoughts help the community grow.