From 482d44d24553d329ed7f85cb620762800d5ffb7a Mon Sep 17 00:00:00 2001 From: Eli Belash Date: Sun, 6 Sep 2026 17:08:54 +0300 Subject: [PATCH] ci: nuget publish via Trusted Publishing (OIDC), drop long-lived API key MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The publish-nuget job in .github/workflows/build-and-release.yml no longer authenticates with the NUGETAPIKEY secret. It now uses NuGet.org Trusted Publishing: GitHub mints a short-lived, signed OIDC token (new job permission id-token: write), NuGet/login@v1 exchanges it at nuget.org's token endpoint for a temporary API key (valid 1 hour), and `dotnet nuget push` uses that key. nuget.org validates the token's repo-owner / repo / workflow-file claims against the Trusted Publishing policy registered under the SciSharp owner (workflow file .github/workflows/build-and-release.yml, no environment, scope "Push new packages and package versions", glob *NumSharp*), then returns the key. Why: removes the long-lived, rotate-by-hand credential (the source of the new-package-first-publish authorization risk and the manual key rotation just performed). Keyless publishing is the OpenSSF-recommended posture and cannot leak a standing secret. Details: - A job-level permissions block REPLACES the top-level `write-all` for publish-nuget, so id-token:write is granted explicitly (write-all's coverage of id-token is not something to depend on); contents:read (unused here, no checkout) and actions:read (download-artifact reads this run's nuget-packages artifact) keep the job least-privilege. - `user` is the nuget.org account username (profile name, NOT an email) that owns the policy, read from the NUGET_USER repo secret — no username hardcoded. - Login is requested immediately before the push so the 1-hour temp key cannot expire; the push loop, source URL and --skip-duplicate are unchanged. Operational follow-ups (NOT in this commit): - Add the NUGET_USER repo secret (the nuget.org profile name of the policy owner) before the next release, or the login step fails with an empty user. - The NUGETAPIKEY secret is now unused by this workflow; delete it once a trusted publish is verified end to end. --- .github/workflows/build-and-release.yml | 28 ++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index 23ec66c2..541f147a 100644 --- a/.github/workflows/build-and-release.yml +++ b/.github/workflows/build-and-release.yml @@ -685,6 +685,22 @@ jobs: if: needs.validate-release.outputs.is_valid == 'true' runs-on: ubuntu-latest + # Trusted Publishing (OIDC) — no long-lived API key. GitHub mints a short-lived, signed OIDC + # token (id-token: write); NuGet/login POSTs it to nuget.org's token endpoint, which validates + # the token's repo-owner / repo / workflow-file claims against the Trusted Publishing policy + # registered under the SciSharp owner (workflow file .github/workflows/build-and-release.yml, no + # environment, scope "Push new packages and package versions", glob *NumSharp*) and returns a + # temporary API key valid for 1 hour. The push then uses that key. + # + # A job-level permissions block REPLACES the top-level `write-all` for THIS job, so id-token is + # granted explicitly (write-all's coverage of id-token is not something to rely on) and the rest + # is least-privilege: contents:read is unused here (no checkout) but harmless; actions:read lets + # download-artifact read this run's nuget-packages artifact. + permissions: + id-token: write + contents: read + actions: read + steps: - name: Download NuGet Packages uses: actions/download-artifact@v7 @@ -699,12 +715,22 @@ jobs: 8.0.x 10.0.x + # Exchange the GitHub OIDC token for a short-lived nuget.org API key. `user` is the nuget.org + # account USERNAME (profile name, NOT an email) that owns the Trusted Publishing policy; set it as + # the NUGET_USER repo secret. Requested here, immediately before the push, so the 1-hour temp key + # cannot expire before use. Output: steps.nuget-login.outputs.NUGET_API_KEY. + - name: NuGet login (OIDC trusted publishing) + uses: NuGet/login@v1 + id: nuget-login + with: + user: ${{ secrets.NUGET_USER }} + - name: Push to NuGet run: | for package in artifacts/*.nupkg; do echo "Pushing $package..." dotnet nuget push "$package" \ - --api-key ${{ secrets.NUGETAPIKEY }} \ + --api-key ${{ steps.nuget-login.outputs.NUGET_API_KEY }} \ --source https://api.nuget.org/v3/index.json \ --skip-duplicate done