In this post I describe how you can use nuget.org's new Trusted Publishing feature to publish NuGet packages from a GitHub Actions workflow, without having to generate and store API keys, while simultaneously benefiting from improved security.
How do you push NuGet packages today?
If you're someone that creates NuGet packages and pushes them to nuget.org today, then you're probably doing so in one of several ways:
- Building the packages locally and manually uploading them to nuget.org.
- Building the packages in CI, downloading them, and manually uploading them to nuget.org.
- Building the packages in CI, and pushing them directly to nuget.org from CI.
Each of these approaches is progressively "better" in that they generally improve the consistency of your builds and reduce the number of manual steps you require. However, pushing your .nupkg files to NuGet from CI is also somewhat "harder" than just uploading them locally, via the nuget.org website.
Instead of just dragging-and-dropping your package onto the web page, you now need to:
- Generate an API key
- Store it securely in GitHub Actions (e.g. as a Secret)
- Pass the secret in your GitHub Actions workflow
- Rotate the secret whenever it changes
- Make sure others in your organisation can do the same (if you're publishing packages for an org)
None of that is insurmountable, but managing the lifecycle of long-lived secrets is notoriously difficult. And that's where Trusted Publishing steps in.
What is Trusted Publishing?
Trusted Publishing is an initiative that's been in place for many ecosystems for a while. For example Python has it for PyPI, Ruby has it for RubyGems.org, and JavaScript has it for npm. And as of last week, .NET has Trusted Publishing for nuget.org 🎉
Trusted Publishing is an approach that uses existing authentication standards (OpenID Connect) to connect CI infrastructure provides (such as GitHub Actions or GitLab Pipelines) with public package repositories (like PyPI and nuget.org). Instead of needing to store and manage API keys so that the two systems can talk to each other, you use OpenID Connect to retrieve short-lived authentication tokens, which you can then use to push packages to the package repository.
Exactly how this works will vary by provider and package repository, but the overall flow is the same. For GitHub actions and nuget.org, it looks something like this:
- The user configures a "trust policy" on nuget.org.
- In a CI workflow, you retrieve an OpenID Connect token for the job from GitHub.
- In this example, GitHub is the Identity Provider (IdP).
- The token is a signed JSON Web Token (JWT), which includes details about the repository and the workflow that's running.
- You send the token to nuget.org, exchanging the token for an API key.
- NuGet.org verifies the contents of the JWT against the configured trust policy.
- If everything matches up, nuget.org issues a short-lived API token that can be used to publish packages.
- The CI workflow uses the API token to push the .nupkg packages to nuget.org.
Because the trust-policy is configured ahead of time, and you're already authenticated with GitHub (due to running in their infrastructure) it's pretty easy to get up and running.
Configuring Trusted Publishing on NuGet.org
Once I saw the blog post, I decided to give it a try for my new sleep-pc tool, seeing as I hadn't set up CI for it yet. I'll start by showing the workflow without NuGet publishing, to provide the baseline, and then show the steps I took to add publishing via Trusted Publishing.
The workflow starting point
The initial workflow, without publishing, is shown below. This is stored in the file .github/workflows/BuildAndPack.yml:
name: BuildAndPack
on:
push:
branches: [main]
tags: ['*']
pull_request:
jobs:
build-and-publish:
permissions:
contents: read
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: 10.0.x
- name: dotnet pack
run: |
dotnet pack
dotnet pack -r win-x64
This just does 3 things:
- Checkout the repo
- Install .NET 10
- Run
dotnet pack
It's worth nothing that I added the permissions entry to limit the permissions to reading from the repository as a security best practice. This isn't required, but I added it mostly to show what needs to change when we use trusted publishing.
Updating the workflow to add Trusted Publishing support
To add trusted publishing, we need to do three things:
- Add the
id-token: writepermission. - Use
NuGet/login@v1to exchange an OIDC token for a NuGet API key. - Use the generated API key to push the packages to NuGet
The following shows the updated workflow, with comments highlighting the differences:
name: BuildAndPack
on:
push:
branches: [main]
tags: ['*']
pull_request:
jobs:
build-and-publish:
permissions:
contents: read
id-token: write # enable GitHub OIDC token issuance for this job
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: 10.0.x
- name: dotnet pack
run: |
dotnet pack
dotnet pack -r win-x64
# Use the ambient GitHub token to login to NuGet and retrieve an API key
- name: NuGet login (OIDC → temp API key)
uses: NuGet/login@v1
id: login
with:
# Secret is your NuGet username, e.g. andrewlock
user: ${{ secrets.NUGET_USER }}
- name: push to NuGet
# Only push to NuGet if we're building a tag (optional)
if: startsWith(github.ref, 'refs/tags/')
shell: pwsh
# Loop through all the packages in the output folder and push them to
# nuget.org, using the NUGET_API_KEY generated by the previous login step
run: |
Get-ChildItem artifacts/package/release -Filter *.nupkg | ForEach-Object {
dotnet nuget push $_.FullName `
--api-key "${{ steps.login.outputs.NUGET_API_KEY }}" `
--source https://api.nuget.org/v3/index.json
}
In this workflow, the only secret you need to configure is the NUGET_USER secret, which should be set to your nuget.org username (not your email address). Given that it's public information anyway, storing it in a secret seems a tad overkill, but why not 😄

If you run this workflow now, the NuGet login step will fail with an error like the following:
Requesting GitHub OIDC token from: https://run-actions-1-azure-eastus.actions.githubusercontent.com/100//idtoken/aae150a5-757c-411a-b2b0-f82a9b26401f/1943e299-9d47-50e7-8d56-90681c654f24?api-version=2.0&audience=https%3A%2F%2Fwww.nuget.org
Error: Token exchange failed (401): No matching trust policy owned by user '***' was found.
As you can see from the message, the stage failed because we haven't yet configured a trust policy on nuget.org, so that's our next job.
Configuring a trust policy on NuGet.org
Configuring a trust policy is very easy. We start by signing in at nuget.org and navigating to the Trusted Publishing page:

Click Create to create a new policy, and fill in all the required fields:
- A name for the policy. I think it makes sense to be the name of the package/github repo.
- The package owner (whether it's an individual account or an organisation)
- The GitHub repository details (owner and repository)
- The workflow file that will be pushing to Nuget
You can see how I completed this for my sleep-pc tool below:

Once you've created the trust policy, it exists in a "partially" active state until you use the policy

Now if you run the workflow again the login step is successful:
Requesting GitHub OIDC token from: https://run-actions-1-azure-eastus.actions.githubusercontent.com/73//idtoken/21d84c5d-b6a5-49e0-bc04-e46694e083df/b3fac26f-8847-5e96-b7ae-47d656a98f51?api-version=2.0&audience=https%3A%2F%2Fwww.nuget.org
Successfully exchanged OIDC token for NuGet API key.
and the policy has changed to be fully active:

Assuming everything else goes to plan, in the next step your package will be published to NuGet, no long-lived API keys required 🎉
Wrapping up
As far as I can tell, as of today, there's no additional benefits from using trusted publishing on nuget.org, aside from the ease of publishing, and the lack of long-lived credentials. I can certainly foresee additional benefits in the future, with packages pushed via trusted publishing having additional verification marks. For example, maybe there will be some tie-in to provenance attestations in the future? I guess we'll wait and see!
Summary
In this post I discussed Trusted Publishing and how it can help by avoiding the need to store long-lived credentials in your GitHub repositories. I then showed how I configured publishing of my sleep-pc package from my GitHub repository to nuget.org using Trusted Publishing.
