Coding Stephan

Publish to nuget without a token

Ever got a message from nuget.org stating that your API key is expiring or has expired? Well, I sure did. Most of the 20 packages I maintain are published using GitHub Actions. I created separate API keys for each package, and stored them in the respective github repository secrets. So I have 20 keys to manage and rotate every year.

Fast-forward to September 2025, when NuGet announced support for Trusted Publishing from GitHub. And today I show you how to convert your existing GitHub Actions workflow to use trusted publishing instead of an API key.

How it works

Basically trusted publishing is using the GitHub Actions OIDC token to get a short-lived access token from nuget.org. They trust the GitHub token service to hand out the right token. That token is validated against the policies you create under your account. And if it matches one of the policies, you get a token that allows publishing and managing packages.

Never rolling over secrets for nuget again, sounds good right? Go ahead and read on. We will come to the downsides later.

Create policy in nuget.org

Before you start changing the pipeline, you need to configure a trusted publishing policy under your account.

  1. Go to nuget.org and log in
  2. Click on your username in the top right corner and select Trusted publishing
  3. Click the Create button to show the form.
  4. Fill in the form
    • Name: A name for your policy, e.g. GitHub Actions {repository name} {workflow name}
    • Package Owner: Select your personal account or an organization you own
    • Repository Owner: The owner of your GitHub repository, e.g. {username} or {organization}
    • Repository: The name of your GitHub repository, e.g. {repository}
    • Workflow: The name of the workflow file, e.g. publish.yml
  5. Click Create to create the policy

This creates a policy that is tied to that exact workflow file, make sure it exists prior to creating the policy.

Update GitHub Actions workflow

Next we are going to update the GitHub Actions workflow (see commit). I made some mistakes the first time I moved a package to trusted publishing so you don’t have to.

1. Add permissions

As said, trusted publishing used the OIDC token from GitHub Actions. By default, this won’t be available to your workflow, so when I made this commit, I was presented with this error. You must explicitly set the permission for the workflow to get the OIDC token.

jobs:
...
  publish:
    name: 📦 Publish to nuget
    runs-on: ubuntu-latest
    if: startsWith(github.ref, 'refs/tags/')
    # 👇 Add the correct permissions to your workflow job
    permissions:
      id-token: write # Enables OIDC token generation

2. Add nuget login step

Right above where you’re publishing the package, you’ll need to add a new special NuGet/login login step. This step will use the OIDC token to get a short-lived access token from nuget.org.

jobs:
  publish:
    ...
    steps:
      ...

      # 👇 Add the NuGet/login@v1 step
    - name: 🔑 Nuget trusted publishing
      uses: NuGet/login@v1
      id: nuget_login # Assign an ID to this step so you can use the output in the next step
      with:
        user: svrooij # Replace with the configured nuget.org username or organization name

3. Update nuget publish step

Finally, you need to update the dotnet nuget push command to use the token generated in the previous step. You can get the token using the expression ${{ steps.nuget_login.outputs.token }}.

jobs:
  publish:
    ...
    steps:
      ...
        # 👇 Modify the `api-key` argument to use the output from the nuget step
      - name: 📦 Publish package
        run: |
          dotnet nuget push ./path/to/your/package.nupkg \
            --source https://api.nuget.org/v3/index.json \
            --api-key ${{ steps.nuget_login.outputs.token }}          

That is it!

With this final step, you are done. Well all most done. After testing the workflow by packaging a new version of your package, you should remove the old api key from the repository secrets and revoke the key from nuget.org. There are however a few downsides to this approach.

The downsides

  • Only works from GitHub Actions
  • You get a short-lived token, that can manage ALL packages owned by either the user or the organization you selected when creating the policy.

Not being able to limit the scope of the token is a giant oversight by the nuget team. It means that it requires only a single repository to be compromised to gain access to all packages. There is an open issue about this, so please go and up-vote it.

With these downsides in mind, I’m not sure if I would recommend this approach for everyone. I just don’t like rotating keys, because I often forget and then have to renew the api key and re-run the job. So I’m switching all my packages to this as a convenience. In larger organizations you might not want to give every repository the ability to publish all the packages. So think carefully before switching to trusted publishing. This is the current state at the time of writing, things might change in the future. At which point this opinions might change as well.