Introduction
Publishing a .NET package from GitHub Actions often starts with a simple setup: create a NuGet API key, save it as a repository secret, and use it during dotnet nuget push.
The problem appears later.
The key expires. A package release starts. The workflow reaches the publishing step and fails with an authentication error.
Now someone has to create another key, update the secret, verify the workflow, and repeat the process later.
A better design is to make credential lifetime part of the CI/CD architecture.
NuGet supports scoped API keys with package and permission restrictions, while its newer Trusted Publishing capability can eliminate long-lived API keys entirely by using GitHub Actions OIDC and short-lived credentials.
This article focuses on the traditional API-key approach and shows how to automate a 30-day rotation process in GitHub Actions, while also explaining why Trusted Publishing is a better long-term option when it is available.
Why API Key Rotation Matters
A NuGet API key is effectively a publishing credential.
If that credential is exposed, someone may be able to publish packages using the permissions assigned to it. NuGet explicitly recommends treating API keys as secrets and deleting or regenerating them if they are accidentally exposed.
A typical workflow looks like this:
Developer
|
v
Git Push / Release
|
v
GitHub Actions
|
v
NuGet API Key
|
v
dotnet nuget push
|
v
nuget.org
The weak point is the static credential:
GitHub Secret
|
v
Long-lived API Key
A rotation strategy reduces the lifetime of that credential.
Current Key
|
v
Rotate
|
v
New Key
|
v
Update GitHub Secret
|
v
Delete Old Key
Scoped Keys Should Be the Baseline
Before automating rotation, reduce the blast radius of the key.
NuGet scoped API keys can be restricted by:
Package
Glob pattern
Operation
Expiration
For publishing automation, the key should generally have only the permissions required by the release workflow.
For example:
Package scope:
MyCompany.*
Permission:
Push
Expiration:
30 days
This is much safer than using a broad account-wide credential.
A 30-Day Rotation Architecture
A practical design separates package publishing from credential rotation.
+------------------+
| GitHub Actions |
+---------+--------+
|
+------------+------------+
| |
v v
Package Release Key Rotation
| |
v v
Read GitHub Secret Create New Key
| |
v v
dotnet nuget push Update Secret
| |
v v
NuGet.org Delete Old Key
The important principle is:
Do not wait for the publishing workflow to discover that a credential has expired.
Rotation should happen before expiration.
Store the Key in GitHub Secrets
The API key should never be committed to source control.
A release workflow can consume a repository or environment secret:
- name: Publish package
run: >
dotnet nuget push ./artifacts/*.nupkg
--api-key "${{ secrets.NUGET_API_KEY }}"
--source "https://api.nuget.org/v3/index.json"
The secret remains outside the repository.
NuGet's documentation also supports supplying an API key through the NUGET_API_KEY environment variable rather than storing it in NuGet.Config.
For CI/CD, keeping credentials in the workflow environment is generally preferable to writing them into configuration files.
Use an Environment for Production Publishing
For production packages, consider using a GitHub environment such as:
release
Then store:
NUGET_API_KEY
as an environment secret.
The workflow becomes:
jobs:
publish:
environment: release
steps:
- name: Publish package
run: >
dotnet nuget push ./artifacts/*.nupkg
--api-key "${{ secrets.NUGET_API_KEY }}"
--source "https://api.nuget.org/v3/index.json"
This separates ordinary repository automation from the production publishing credential.
The Rotation Workflow
The rotation workflow needs to perform four logical operations:
1. Create replacement key
2. Update GitHub secret
3. Verify the new credential
4. Revoke old key
The sequence matters.
Do not delete the old key first.
Instead:
Old Key
|
+------------------+
| |
v v
Create New Key Old Key remains valid
|
v
Update Secret
|
v
Test New Key
|
v
Delete Old Key
This provides a recovery path if the new credential does not work.
Automating the Rotation
There is an important limitation with the traditional approach: creating and managing NuGet API keys is an account-level operation, so the rotation workflow itself needs a secure administrative authentication mechanism.
Do not solve this by placing a permanent administrator credential directly into the workflow.
Instead, use a dedicated credential with the minimum possible permissions, or migrate the publishing workflow to Trusted Publishing.
The automation concept can be represented as:
name: Rotate NuGet Credential
on:
schedule:
- cron: "0 3 1 * *"
workflow_dispatch:
jobs:
rotate:
runs-on: ubuntu-latest
steps:
- name: Generate replacement credential
run: ./scripts/create-nuget-key.sh
- name: Update GitHub secret
run: ./scripts/update-github-secret.sh
- name: Validate publishing credential
run: ./scripts/test-nuget-key.sh
- name: Revoke previous credential
run: ./scripts/revoke-old-key.sh
The scripts are intentionally abstract here.
The important design is the order of operations, not a hard-coded implementation of undocumented account-management APIs.
Why You Should Not Put Key Creation Logic in the Package Job
Avoid combining rotation and publishing:
Build
|
v
Create Key
|
v
Publish
|
v
Delete Key
This creates unnecessary coupling.
If key creation fails, the package release fails.
If package publishing fails, credential lifecycle becomes difficult to reason about.
A cleaner architecture is:
Rotation Workflow
|
v
Credential Lifecycle
Release Workflow
|
v
Package Lifecycle
They can operate independently.
Validate Before Revoking
The most important safety check is verifying the new credential before removing the old one.
A validation workflow can publish a package to a controlled test feed or perform another permitted non-production validation.
For example:
New Key
|
v
Authentication Test
|
+---- FAIL ---> Keep Old Key
|
+---- PASS ---> Continue
|
v
Update Secret
|
v
Revoke Old
This prevents a rotation mistake from immediately breaking package releases.
Use a Rotation Metadata File
It can also be useful to track metadata without storing the secret itself.
For example:
{
"credentialName": "nuget-release",
"createdAt": "2026-08-01",
"expiresAt": "2026-08-31",
"scope": "MyCompany.*",
"permission": "push"
}
Never store the actual API key in this file.
The purpose is operational visibility.
You can then calculate:
Days Remaining
Expiration Date
Package Scope
Rotation Status
Add an Expiration Check
Even with scheduled rotation, the release workflow should detect a missing or invalid credential.
For example:
Publish
|
v
Credential available?
|
+---- No ----> Fail clearly
|
+---- Yes
|
v
Push package
A clear failure is better than allowing a generic authentication error to obscure the real problem.
NuGet documentation notes that an invalid or expired API key can result in a 403 response indicating that the key is invalid, expired, or lacks permission for the package.
Make Rotation Idempotent
A good rotation workflow should be safe to rerun.
Suppose the workflow runs twice.
You do not want:
Run 1 -> New Key A
Run 2 -> New Key B
Run 3 -> New Key C
...
with dozens of stale credentials.
Instead, maintain a clear ownership model:
Active Credential
|
v
Create Replacement
|
v
Validate
|
v
Promote Replacement
|
v
Revoke Previous
Keep the number of active publishing keys intentionally small.
NuGet notes that although there is no limit on the number of API keys, keeping the number manageable helps avoid stale credentials.
Use Separate Keys for Separate Packages
Suppose an organization publishes:
Company.Core
Company.Data
Company.Web
Company.AI
Avoid one unrestricted credential when separate scopes are practical.
Instead:

Join the conversation! Your thoughts help the community grow.