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:
Company.Core -> Key A
Company.Data -> Key B
Company.Web -> Key C
Company.AI -> Key D
A compromise of one key then has a smaller blast radius.
NuGet's scoped-key model supports package-specific and pattern-based package scopes.
A Better GitHub Actions Release Workflow
A production package workflow can look like:
name: Publish NuGet Package
on:
workflow_dispatch:
jobs:
publish:
runs-on: ubuntu-latest
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: Publish
run: >
dotnet nuget push ./artifacts/*.nupkg
--api-key "${{ secrets.NUGET_API_KEY }}"
--source "https://api.nuget.org/v3/index.json"
--skip-duplicate
This creates a clear release pipeline:
Restore
|
v
Build
|
v
Test
|
v
Pack
|
v
Publish
Credential rotation remains a separate concern.
What Happens If Rotation Fails?
Design for failure.
Scenario 1: New Key Creation Fails
Keep the old key active.
Old Key -> Still Active
New Key -> Not Created
The next rotation attempt can retry.
Scenario 2: GitHub Secret Update Fails
Do not revoke the old key.
Old Key -> Active
GitHub Secret -> Old Value
New Key -> Unused
Investigate before continuing.
Scenario 3: Validation Fails
Keep the old credential.
New Key -> Invalid
Old Key -> Active
Scenario 4: Revocation Fails
The release workflow can continue using the new key, but the old key should be flagged for cleanup.
New Key -> Active
Old Key -> Cleanup Required
This is much safer than treating every rotation step as an all-or-nothing operation.
Monitor Rotation Events
Credential rotation should produce observable events.
Record:
Rotation Started
New Credential Created
GitHub Secret Updated
Validation Passed
Old Credential Revoked
Do not log:
API Key Value
A useful notification might be:
NuGet credential rotation completed.
Package scope: Company.*
Credential: nuget-release
Status: Success
Next rotation: 30 days
This gives the team operational confidence without exposing sensitive information.
Why 30 Days Is a Useful Policy
A 30-day credential lifetime provides a relatively small exposure window.
If a key is accidentally leaked:
Day 1
|
v
Credential exposed
|
v
Maximum planned lifetime
|
v
Day 30
|
v
Credential replaced
However, the exact lifetime should depend on the organization's risk model.
Shorter credentials reduce exposure but increase operational overhead.
Longer credentials reduce rotation frequency but increase the potential lifetime of a compromised secret.
The important point is to make expiration intentional rather than accidental.
The Better Option: Trusted Publishing
There is now a more significant alternative to automated API-key rotation.
NuGet Trusted Publishing uses OIDC between the CI/CD system and nuget.org.
The workflow becomes:
GitHub Actions
|
v
GitHub OIDC Token
|
v
NuGet Trusted Publishing
|
v
Short-Lived API Key
|
v
Package Push
NuGet documents that the temporary API key issued through Trusted Publishing is valid for one hour and should be requested shortly before publishing. Each OIDC token can be exchanged only once for a temporary key.
This dramatically changes the security model.
Instead of:
Long-Lived Secret
|
v
Rotation
you get:
Short-Lived Identity
|
v
Temporary Credential
|
v
Publish
|
v
Credential Expires
There is no 30-day API key to rotate.
Trusted Publishing with GitHub Actions
NuGet's current documentation shows a GitHub Actions workflow using the id-token: write permission and the NuGet/login@v1 action to obtain a temporary API key.
A simplified example is:
jobs:
publish:
permissions:
id-token: write
steps:
- 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"
The trusted publishing policy can be restricted to a specific repository, workflow file, and optionally a GitHub Actions environment.
That provides an important security boundary:
Repository
+
Workflow
+
Environment
|
v
Trusted Publisher
|
v
NuGet Package
API Keys vs Trusted Publishing
| Capability | Rotating API Key | Trusted Publishing |
|---|
| Long-lived secret | Yes | No |
| Manual secret storage | Required | Not for package credential |
| Rotation required | Yes | No |
| OIDC | No | Yes |
| Temporary credential | No | Yes |
| Package scoping | Yes | Policy-based |
| GitHub Actions support | Yes | Yes |
| Operational complexity | Higher | Lower after setup |
| Recommended for new CI/CD | Acceptable | Preferred |
NuGet describes Trusted Publishing as a way to publish packages without managing long-lived API keys.
Migration Strategy
If your organization currently uses API keys, do not change everything during a release.
A safer migration looks like:
Existing API Key
|
v
Implement Trusted Publishing
|
v
Test in Non-Production
|
v
Publish Test Package
|
v
Validate
|
v
Production Release
|
v
Remove Old API Key
This provides a controlled transition.
Common Mistakes
Rotating After Expiration
If you rotate only after a release fails, the rotation process itself becomes an outage dependency.
Using an Account-Wide Key
Broad credentials increase the blast radius of compromise.
Deleting the Old Key Too Early
Always validate the replacement before revocation.
Logging Secrets
Never print API keys in workflow logs.
Combining Rotation and Release
Keep credential lifecycle separate from package lifecycle.
Creating Too Many Keys
Unused credentials become security debt.
Ignoring Trusted Publishing
For GitHub Actions-based publishing, OIDC-based Trusted Publishing can remove much of the credential-management burden entirely.
Best Practices
Use this checklist when designing a NuGet publishing system:
[ ] Use scoped API keys
[ ] Restrict package permissions
[ ] Store secrets in GitHub Secrets
[ ] Use production environments
[ ] Rotate before expiration
[ ] Validate replacement credentials
[ ] Revoke old credentials
[ ] Never log key values
[ ] Monitor rotation events
[ ] Keep credential count low
[ ] Separate rotation from release
[ ] Test failed-rotation scenarios
[ ] Prefer OIDC Trusted Publishing where available
Advantages and Disadvantages
Advantages
Reduces the lifetime of publishing credentials.
Limits the impact of leaked keys.
Makes credential lifecycle explicit.
Supports automated CI/CD operations.
Encourages least-privilege package publishing.
Disadvantages
Traditional key rotation requires account-management automation.
Rotation workflows themselves need secure administrative credentials.
Poorly designed rotation can interrupt package publishing.
Multiple packages can make key management complicated.
Migration to Trusted Publishing requires initial configuration and testing.
Conclusion
Automating NuGet API key rotation is a useful security improvement for existing GitHub Actions pipelines, especially when scoped credentials are used and the rotation process validates a replacement before revoking the previous key.
But the bigger architectural lesson is that credential rotation is often a symptom of long-lived authentication.
NuGet Trusted Publishing provides a stronger model for GitHub Actions by using OIDC to obtain short-lived credentials for individual publishing workflows. The temporary NuGet credential is valid for only one hour, eliminating the need to maintain a 30-day publishing secret.
For an existing system that cannot immediately migrate, a 30-day rotation policy is a practical improvement. For new .NET package publishing pipelines, however, the better target is a keyless or short-lived identity model where GitHub Actions proves who is publishing rather than continuously carrying a reusable publishing secret.