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

AreaTraditional API KeyTrusted Publishing
Permanent publishing secretYesNo
GitHub Secret for API keyRequiredNot required
OIDCNoYes
Credential lifetimeLimited and increasingly shortAbout 1 hour
RotationRequiredNot required
Workflow identityIndirectExplicit
Repository restrictionLimited to key scopePolicy-based
Environment restrictionSeparate GitHub controlCan be included in policy
Secret exposure riskHigherLower
CI/CD suitabilityLegacy-compatiblePreferred

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

Disadvantages

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.