Publish NuGet packages with Trusted Publishing and GitHub Actions

6 minute read

Publishing a NuGet package from GitHub Actions has traditionally required a NuGet API key stored as a repository or organization secret.

That works, but it means keeping a reusable publishing credential outside NuGet.org and rotating it whenever it expires.

NuGet Trusted Publishing replaces that stored secret with an identity relationship between NuGet.org and a specific GitHub repository and workflow. The workflow authenticates through OpenID Connect, receives a temporary API key, and uses it for the final dotnet nuget push operation.

Microsoft has announced that newly created NuGet API keys will be limited to 30 days starting August 17, 2026. Existing keys created before that date will expire on November 1, 2026. These dates create some urgency around the migration, but the Trusted Publishing setup described in this article remains useful after those deadlines have passed.

In this article, I will migrate my NuGet package GitHub Actions sample publishing workflow from a stored API key to Trusted Publishing.

The old publishing flow

A typical publishing step retrieves a reusable API key from GitHub Secrets and passes it to dotnet nuget push.

- name: Publish to NuGet.org
  run: >-
    dotnet nuget push
    "./artifacts/packages/*.nupkg"
    --source https://api.nuget.org/v3/index.json
    --api-key "$"

The workflow does not know how the key was created or who created it. Possession of the secret is enough to publish packages within the permissions assigned to that key.

The setup also creates an operational dependency: somebody has to create the key, copy it into GitHub, and replace it before it expires.

What Trusted Publishing changes

Trusted Publishing changes what NuGet.org trusts.

Instead of trusting any process that possesses a reusable API key, NuGet.org trusts a specific GitHub identity composed of:

  • the repository owner;
  • the repository;
  • the workflow file;
  • optionally, a GitHub Actions environment.

When the workflow runs, GitHub issues an OIDC token describing the job identity. NuGet.org validates that token against the configured Trusted Publishing policy.

If the identity matches, NuGet.org returns a temporary API key. The workflow then passes that key to dotnet nuget push just as it did before.

There is still an API key involved in the final publishing operation. The difference is that it is created for the workflow run instead of being stored as a reusable secret.

Create the Trusted Publishing policy

Sign in to NuGet.org and open Trusted Publishing from the account menu.

Create a new policy and select the NuGet owner whose packages the workflow should be allowed to publish. The owner can be an individual account or a NuGet organization.

For the sample repository, the GitHub policy uses these values:

Repository owner: Kralizek
Repository: NuGetPackageGitHubActions
Workflow file: ci.yml
Environment: optional

The workflow value is only the filename. For a workflow stored at:

.github/workflows/ci.yml

enter:

ci.yml

Do not include the .github/workflows/ path.

A policy applies to packages owned by the selected NuGet owner. It is not limited to a single package identifier. This is worth considering when an account or organization owns several packages: the configured repository and workflow receive publishing authority for that owner.

A GitHub environment can narrow the identity further. When the policy specifies an environment, the publishing job must use the same environment for its OIDC identity to match.

Allow the job to request an OIDC token

The publishing job needs permission to request an OIDC token from GitHub:

publish:
  permissions:
    contents: read
    id-token: write

The relevant permission is:

id-token: write

This permission does not allow the workflow to publish NuGet packages by itself. It only allows the job to request an OIDC token.

NuGet.org still decides whether to accept the identity by comparing the token with the Trusted Publishing policy.

The permission can therefore remain limited to the publishing job. Build, test, and package-validation jobs do not need it.

Exchange the GitHub identity for a temporary key

Add the NuGet/login action shortly before publishing:

- name: Log in to NuGet.org
  id: nuget-login
  uses: nuget/login@v1
  with:
    user: Kralizek

The action requests an OIDC token from GitHub and exchanges it with NuGet.org.

When the identity matches the policy, the action exposes the temporary API key through its output:

$

The user value is the NuGet.org profile name, not the email address used to sign in.

It is not a secret, although storing it in a GitHub variable can be useful when the same workflow is reused across repositories or package owners.

Use the temporary key to publish

The push command itself changes very little:

- name: Publish to NuGet.org
  run: >-
    dotnet nuget push
    "./artifacts/packages/*.nupkg"
    --source https://api.nuget.org/v3/index.json
    --api-key "$"
    --skip-duplicate

The important change is the source of the key.

Before:

--api-key "$"

After:

--api-key "$"

The workflow now requests the credential when it needs it and discards it when the job ends.

There is no NuGet publishing key to copy into GitHub, preserve between runs, or rotate every month.

Keep authentication close to publishing

The temporary key is valid for approximately one hour, so there is little reason to request it at the beginning of a job and then spend time building and testing the solution.

In the sample workflow, compilation and packaging happen in a separate job. The publishing job downloads the resulting artifacts, obtains the temporary credential, and pushes the package.

A simplified version looks like this:

jobs:
  validate-and-pack:
    permissions:
      contents: read

    steps:
      - name: Build
        run: dotnet build --configuration Release

      - name: Test
        run: dotnet test --configuration Release --no-build

      - name: Pack
        run: >-
          dotnet pack
          --configuration Release
          --no-build
          --output ./artifacts/packages

      - name: Upload packages
        uses: actions/upload-artifact@v7
        with:
          name: nuget-packages
          path: ./artifacts/packages/*.nupkg

  publish:
    needs: validate-and-pack
    if: github.event_name == 'release'
    permissions:
      contents: read
      id-token: write

    steps:
      - name: Download packages
        uses: actions/download-artifact@v8
        with:
          name: nuget-packages
          path: ./artifacts/packages

      - name: Log in to NuGet.org
        id: nuget-login
        uses: nuget/login@v1
        with:
          user: Kralizek

      - name: Publish to NuGet.org
        run: >-
          dotnet nuget push
          "./artifacts/packages/*.nupkg"
          --source https://api.nuget.org/v3/index.json
          --api-key "$"

This keeps authentication close to the operation that needs it.

The separation also means that the job holding publishing permissions does not compile arbitrary repository code. It receives packages already produced by the validation job and only performs the release-specific work.

Remove the old secret

After a successful release through Trusted Publishing, remove the old NuGet API key from the repository or organization secrets.

Keeping it around as an undocumented fallback defeats part of the migration. It remains a reusable publishing credential even though the workflow no longer needs it.

You can also revoke the corresponding key from NuGet.org after confirming that no other release process depends on it.

Check organization-level secrets and reusable workflows as well. A key may be shared by more repositories than the one currently being migrated.

Limitations and operational details

Trusted Publishing authenticates supported CI/CD workloads such as GitHub Actions. A local dotnet nuget push still needs a conventional NuGet API key.

The policy also depends on the configured identity remaining consistent. Renaming the repository, renaming or moving the workflow file, or changing the GitHub environment can cause the exchange to fail until the NuGet policy is updated.

Trusted Publishing authenticates the publishing workflow; it does not replace the rest of the release process. The package should still be built, tested, validated, and produced from a controlled revision before the publishing job starts.

Finally, consider the scope of the selected NuGet owner. The policy is broader than one package, so the repository and workflow should be protected accordingly. A GitHub environment with required reviewers can be useful when publishing needs an explicit approval step.

Recap

Traditional NuGet publishing relies on a reusable API key stored in GitHub Secrets.

Trusted Publishing replaces that secret with an identity relationship between NuGet.org and a specific GitHub repository and workflow.

The migration requires three changes:

  1. Create a Trusted Publishing policy on NuGet.org.
  2. Grant the publishing job id-token: write.
  3. Use NuGet/login to obtain a temporary key immediately before dotnet nuget push.

The push command remains familiar.

The long-lived publishing secret disappears.

Support this blog

If you liked this article, consider supporting this blog by buying me a pizza!