.NET code & nupkg signing in GitHub Actions
This guide will walk you through using a combination of GitHub actions and Entra managed identities to enable signing code and NuGet packages from within an action without needing to worry about access tokens.
Introduction
This guide will walk you through signing .NET assemblies, EXEs and NuGet packages using a combination of GitHub actions and Entra managed identities.
Using managed identities allows you to stop worrying about expiring access tokens or leaking the secrets that hold them. Code Signing means no more (well, less) Windows Defender complaints about your executable being suspicious.
The process involves creating and a user-assigned managed identity and assigning it a GitHub action. It uses GitHub environments to isolate the signing stage and its configuration secrets from steps that compile your code. Code signing is accomplished using the dotnet sign tool.
Picking your signing solution
If you're worried about forgetting to renew your Authenticode signing certificate, and you're signing executables then Trusted Signing is probably the solution for you. Trusted Signing issues and renews your certificates and is a lot cheaper than buying an annual certificate.
Publishing nupkgs signed with Trusted Signing to nuget.org is currently an unsupported scenario. If you want to use Trusted Signing with nuget.org please react or add your feedback to the "Support Azure Trusted Signing certificate on NuGet.org account settings" issue.
If you're signing NuGet packages and you want to publish them on nuget.org you will need an Authenticode Signing Certificate stored in an Azure Key vault. If you're concerned about forgetting to renew DigiCert supports integration with Key vault and automatically handle certificate renewal and rotation. You will still need to update nuget.org with the new certificate details once a year.
You can go look at the GitHub actions I use for producing my idunno.Authentication NuGet packages. There are two separate actions, prerelease.yml which uses Trusted Signing and publishes to MyGet and GitHub packages, and release.yml which uses a certificate stored in an Azure Key vault and publishes to nuget.org.
Prerequisites
You should already have
- A GitHub account and admin rights on the repository you want to configure the actions on.
- An Azure account and subscription where you have owner rights, and either
- An Authenticode Signing Certificate stored in an Azure Premium Key vault, or
- A Trusted Signing account with an active certificate profile.
- Azure CLI installed and up to date.
Configuration values you will need
| Configuration | Where to find it |
|---|---|
| Azure Subscription ID | Available once you log in to Azure. |
| Azure Tenant Id | Available once you log in to Azure |
| Azure Managed Identity Client ID |
Available when you create a Managed Identity. |
| GitHub Repository Owner | The first path segment in the URI for the repository you want to assign managed identities to. e.g. for https://github.com/contoso/WebSite/ the owner is contoso |
| GitHub Repository Name | The second path segment in the URI for the repository you want to assign managed identities to. e.g. for https://github.com/contoso/WebSite/ the Repository Name is WebSite |
| GitHub Actions Environment | Available when you assign a Managed Identity to a GitHub Action. |
If you are going to sign with a certificate store in an Azure Key vault you will also need
| Configuration | Where to find it |
|---|---|
| Vault URI | On the overview page of your Key Vault |
| Certificate Name | In the Certificates list of your Key Vault |
If you are going to sign with Trusted signing you will also need
| Configuration | Where to find it |
|---|---|
| Account Name | Available when granting permission to Trusted signing |
| Account Uri | Available when granting permission to Trusted signing |
| Certificate Profile | Available when granting permission to Trusted signing |
Authenticating GitHub Actions to Azure
The first step to configuring signing and release in a GitHub action is to configure the action to authenticate to Azure. This requires a User Assigned Managed Identity which is then federated to GitHub Actions.
Creating a User Assigned Managed Identity
First you need to create a User Assigned Managed Identity.
- Open PowerShell and authenticate to Azure, picking the subscription that contains your Trusted Signing account or the Key vault that contains your signing key.
az login - Set the subscription id and tenant id for later use
$subscriptionId = az account show --query id --output tsv
$tenantId = az account show --query tenantId --output tsv - Set the Azure resource group you want the identity to be created in
$resourceGroup = "resourceGroupName" - Set the name of the Managed Identity you want to create, and then create it
$managedIdentityName = "managedIdentityName"
az identity create \--name $managedIdentityName \
--resource-group $resourceGroup
$clientId = az identity show \
--name $managedIdentityName \
--resource-group $resourceGroup \
--query clientId --output tsv
$managedIdentityId = az identity show \
--name $managedIdentityName \
--resource-group $resourceGroup \
--query principalId --output tsv
Assigning a User Assigned Managed Identity to a GitHub Action
Now you have a User Assigned Managed Identity it needs to be assigned to one or more GitHub Actions. This is accomplished by adding a Federated Credential to the managed identity that has the properties necessary to use from a GitHub action.
- Pick the GitHub repo you want to create your signing action in.
- Make a note of the repo owner and name. These are case sensitive.
- Pick an Environment name for the GitHub action. This is used to isolate the secrets needed to login and sign to a signing stage in the action we will create, for example
sign. You will also need this when configuring the action. - Construct a subject string for the federated credential you are about to create in the following format
$subject = "repo:repoOwner/repoName:environment:environmentName"
For example, if we wanted to create a federated credential for theSigning.Examplerepo owned byContoso, with an environment name ofsignyou would use$subject = "repo://contoso/Signing.Example:environment:sign"
Remember everything is case sensitive. - Pick a name for the federated credential.
$credentialName = "credentialName" - Create a new Federated Credential for the User Assigned Managed Identity you created in the previous step
az identity federated-credential create \
--name $credentialName \
--identity-name $managedIdentityName \
--resource-group $resourceGroup \
--issuer https://token.actions.githubusercontent.com \
--subject $subject
Configuring the GitHub Action Environment
Next you need to create and configure the environment for your GitHub actions.
- Log in to GitHub.
- Choose the repository you want to add the signing action to by navigating to it.
- From the repository view select Settings.
- Select Environments from the left-hand menu.
- Select New environment, then give it the same name as you configured the federated credential entity for in Assigning a User Assigned Managed Identity to a GitHub Action, step 3, then select Configure Environment.
- In Environment Secrets add the following secrets and the appropriate values.
These were saved as you created the managed identity and can be shown again in your PowerShell session, with theechocommand listed under the value description
| Secret Name | Secret Value |
|---|---|
| AZURE_CLIENT_ID | The Client ID of the managed identity. echo $clientId |
| AZURE_SUBSCRIPTION_ID | The Subscription ID the managed identity was created in. echo $subscriptionId |
| AZURE_TENANT_ID | The Tenant ID the subscription belongs to. echo $tenantId |
7. Create any deployment and branch protection rules you require.
Signing in a GitHub action
A basic two step GitHub action with a managed identity
Finally, you’re at the point where you can write a GitHub action which can authenticate with Azure. The goal here is to have a two (or three once you include publishing) stage action, where the first stage is the typical build/test, producing unsigned artifacts, the second signs the output of the build/test, producing signed artifacts, and then optionally a publish stage to, for example, NuGet.
To start, let’s define an action with two stages, build and sign. Signing .NET executables or nupkgs requires using a Windows runner and will also use the environment you created in Configuring the GitHub Action Environment.
What follows is a typical workflow for building NuGet packages. Y can easily modify for executables by swapping out dotnet pack for dotnet publish.
I have omitted the action hashes in the example to make things easier to read, but as you write your own workflow you should pin each action to its version hash to avoid the potential harm that could be caused by an attacker gaining control of an action, injecting malicious code and then retagging the version of the action containing the code to an existing version tag.
name: Build, Test, Sign
on:
workflow_dispatch
env:
OUTPUT_DIRECTORY: ${{ github.workspace}}/output
permissions:
contents: read
jobs:
build:
name: Build & Test
permissions:
contents: read
runs-on: ubuntu-latest
steps:
- name: 'Checkout repository'
uses: actions/checkout
persist-credentials: false
- name: 'Setup .NET SDK'
uses: actions/setup-dotnet
with:
dotnet-version: |
10.0.x
- name: 'Build'
run: dotnet build
- name: 'Test'
run: dotnet test --no-build
- name: 'Pack'
run: dotnet pack --no-build --output ${OUTPUT_DIRECTORY}
- name: 'Upload unsigned artifacts'
id: upload
uses: actions/upload-artifact
with:
name: build-artifacts.zip
path: ${{ env.OUTPUT_DIRECTORY }}/*
retention-days: 7
sign:
name: Sign
needs: build
runs-on: windows-latest
environment: signing
permissions:
attestations: write
contents: read
id-token: write
steps:
- name: 'Setup .NET SDK'
uses: actions/setup-dotnet
- name: 'Install Sign CLI'
run: dotnet tool install --tool-path ./sign --prerelease sign
- name: 'Gather unsigned artifacts'
uses: actions/download-artifact
with:
name: build-artifacts.zip
path : ${{ env.OUTPUT_DIRECTORY }}
- name: 'Authenticate to Azure'
uses: azure/login
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
#### Signing happens here
- name: 'Upload signed artifacts'
uses: actions/upload-artifact
id: upload
with:
name: signed-artifacts.zip
path: ${{env.OUTPUT_DIRECTORY}}/*
retention-days: 7
A GitHub action definition to support code and nupkg signing from the action
Make sure to adjust the environment value in the sign step to match the environment you specified when creating the federated credential.
Note the placeholder Signing happens here. We will replace this with the appropriate commands in the next step, depending on whether you are using Azure Trusted Signing or an Azure Key vault.
You can see in the sign step the use of the azure/login with the values produced during the creation of the managed identity. That's all it takes in the action to authenticate but doing anything more requires granting the managed identity the appropriate roles/permissions in Azure.
Code signing with an Authenticode Certificate in Azure Key vault
To use a Key vault hosted certificate to sign your assemblies, executables, or nupkgs you need to assign the correct permissions to the User Assigned Managed Identity, configure the correct secrets for the Key vault and then add the signing command to your GitHub Action.
- List all the Key vaults in your subscription
az keyvault list
Save the name of the of the Key vault you want to use and its resource group and the retrieve its id.$keyVault = "keyVaultName"
$keyVaultResourceGroup = "resourceGroup"
$keyVaultId = az keyvault show \
--name $keyVault \
--resource-group $keyVaultResourceGroup \
--query id --output tsv
$keyVaultUri =az keyvault show \
--name $keyVault \
--resource-group $keyVaultResourceGroup \
--query properties.vaultUri --output tsv - Now we need to assign the appropriate roles to the managed identity to sign with.
az role assignment create \
--role "Key Vault Certificate Reader" \
--assignee-object-id $managedIdentityId \
--scope $keyVaultId
az role assignment create \
--role "Key Vault Crypto User" \
--assignee-object-id $managedIdentityId \
--scope $keyVaultId - Finally, we need to choose the certificate in the Key vault we want to use. First we list all the available certificates in the vault, and then we save the name.
az keyvault certificate list --id $keyVaultUri
$certificate = "name" - Now switch back to GitHub.
- Choose the repository you want to add the signing action to by navigating to it, or by choosing it from the Repositories list.
- From the repository view select Settings.
- Select Environments from the left-hand menu.
- Select the environment you configured the federated credential to apply to.
- In Environment Secrets add the following secrets and the appropriate values. These were saved as you added the signing permission and can be shown again in your PowerShell session, with the
echocommand listed under the value description.
| Secret Name | Secret Value |
|---|---|
| AZURE_KEYVAULT_URL | The name URL of the Key vault to use. echo $keyVaultUri |
| AZURE_KEYVAULT_CERTIFICATE | The certificate to sign with. echo $certificate |
- Switch to your code editor and remove the following from the action yaml.
#### Signing happens here
- Insert the following yaml where you removed the placeholder comment, between the Authenticate to Azure and Upload steps in the Sign stage
- name: Sign
shell: pwsh
run: >
./sign/sign code azure-key-vault *.nupkg
--base-directory ${env:OUTPUT_DIRECTORY}
--azure-key-vault-url "${{ secrets.AZURE_KEY_VAULT_URL }}"
--azure-key-vault-certificate "${{ secrets.AZURE_KEY_VAULT_CERTIFICATE }}"A GitHub action job step for signing with an Azure Key vault stored certificate.
- Run your action and you should now have two sets of artifacts,
build-artifactsandsigned-artifacts.
At the time of writing, with version 0.9.1-beta.25379.1 of the sign tool if you examine the logs for the sign stage may see in your sign stage logs one or more exceptions from Azure.Identity which complain about authentication being unavailable. The stage however says it was successful. You can ignore the exceptions, signing did work. See below on how to validate a file has been signed.Code signing with Trusted Signing
To use Trusted Signing to sign your assemblies, executables, or nupkgs, you need to assign the correct permissions to the User Assigned Managed Identity, configure the correct secrets for Trusted Signing and then add the signing command to your GitHub Action.
- List all the Trusted Signing accounts in your subscription
az trustedsigning list
Save the name of the of the account you want to use. its resource group and retrieve its accountUri.$trustedSigningAccount = "accountName"
$trustedSigningResourceGroup = "resourceGroup"
$accountUri = az trustedsigning show \
--name $trustedSigningAccount \
--resource-group $trustedSigningResourceGroup \
--query accountUri --output tsv - List the certificate signing profiles for the Trusted Signing account and set the name of certificate profile you want to use
az trustedsigning certificate-profile list \
--account-name $trustedSigningAccount \
--resource-group $trustedSigningResourceGroup
$certificateProfile = "name" - Create the scope the signing permission will granted to. You have two choices for the scope; apply it to the Trusted Signing account, which will allow any certificate profile to be used, or apply it to an individual certificate profile.
To grant permissions to the Trusted Signing account use$scope = az trustedsigning show \
--account-name $trustedSigningAccount \
--resource-group $trustedSigningResourceGroup \
--query id --output tsv
To grant permissions to the individual certificate profile use$scope = az trustedsigning certificate-profile show \
--name $certificateProfileName \
--account-name $trustedSigningAccount \
--resource-group $trustedSigningResourceGroup \
--query id --output tsv
Note that you cannot see Certificate Profile scoped role assignments in the Azure portal. - Finally, the last Azure piece is to assign the "Trusted Certificate Profile Signer" role to the managed identity.
az role assignment create \
--role "Trusted Signing Certificate Profile Signer" \
--assignee-object-id $managedIdentityId \
--scope $scope - Now switch back to GitHub.
- Choose the repository you want to add the signing action to by navigating to it, or by choosing it from the Repositories list.
- From the repository view select Settings.
- Select Environments from the left-hand menu.
- Select the environment you configured the federated credential to apply to.
- In Environment Secrets add the following secrets and the appropriate values. These were saved as you added the signing permission and can be shown again in your PowerShell session, with the
echocommand listed under the value description.
| Secret Name | Secret Value |
|---|---|
| AZURE_TRUSTEDSIGNING_ACCOUNT | The name of the Trusted Signing account to use. echo $trustedSigningAccount |
| AZURE_TRUSTEDSIGNING_ACCOUNTURI | The URI of the Trusted Signing Account. echo $accountUri |
| AZURE_TRUSTEDSIGNING_CERTIFICATEPROFILE | The certificate profile to sign with. echo $certificateProfile |
- Switch to your code editor and remove the following from the action yaml.
#### Signing happens here
- Insert the following yaml where you removed the placeholder comment, between the Authenticate to Azure and Upload steps in the Sign stage
- name: Sign
shell: pwsh
run: >
./sign/sign code trusted-signing *.nupkg
--base-directory ${env:OUTPUT_DIRECTORY}
-tsa "${{ secrets.AZURE_TRUSTEDSIGNING_ACCOUNT }}"
-tse "${{ secrets.AZURE_TRUSTEDSIGNING_ACCOUNTURI }}"
-tscp "${{ secrets.AZURE_TRUSTEDSIGNING_CERTIFICATEPROFILE }}"A GitHub action job step for signing with an Azure Trusted Signing account.
- Run your action and you should now have two sets of artifacts,
build-artifactsandsigned-artifacts.
At the time of writing, with version 0.9.1-beta.25379.1 of the sign tool if you examine the logs for the sign stage may see in your sign stage logs one or more exceptions from Azure.Identity which complain about authentication being unavailable. The stage however says it was successful. You can ignore the exceptions, signing did work. See below on how to validate a file has been signed.Reusing managed identities across repos
You can add up to 20 federated credentials to a single User Assigned Managed Identity. This allows you to have multiple repos use the same, existing credentials and permissions. However, you may prefer individual managed identities per repository enabling you, in case of breach, to revoke a single repository's credentials without affecting signing actions in other repositories.
Debugging
Validating assembly and executable signatures
To validate an assembly or executable signature you must use Windows and PowerShell. In PowerShell use
Get-AuthenticodeSignature <filePath> | Select-Object -ExpandProperty SignerCertificate | Format-ListYou should see output something like this:
Subject : CN=Barry Dorrans, O=Barry Dorrans, L=Bothell, S=Washington, C=US
Issuer : CN=DigiCert Trusted G4 Code Signing RSA4096 SHA384 2021 CA1, O="DigiCert, Inc.", C=US
Thumbprint : A75A98B54008714D7B76C253C80C23C52E02266E
FriendlyName :
NotBefore : 7/25/2025 17:00:00
NotAfter : 10/23/2026 16:59:59
Extensions : {System.Security.Cryptography.Oid, System.Security.Cryptography.Oid, System.Security.Cryptography.Oid,
System.Security.Cryptography.Oid…}If the executable or assembly is unsigned nothing will be returned.
Validating nupkg signatures
To validate a NuGet package is signed use
dotnet nuget verify <filePath>
You should see output something like this:
Verifying test
Content hash: M+OIv2sa4SVzuqRGB1V27HgdORcCiFlW3ElJr79SJQPJYx+m8mD2BKby+QEg0iiMvuetczmgXQw12DhYpRfrcg==
Signature type: Author
Subject Name: CN=Barry Dorrans, O=Barry Dorrans, L=Bothell, S=Washington, C=US
SHA256 hash: 8043BF33C42BC87254D9F8213E7A97641490AEF26A11E8D30739DDC83C535D12
Valid from: 11/02/25 07:48:22 to 11/05/25 07:48:22If the nupkg is unsigned you will see an error,
Verifying test
Content hash: M+OIv2sa4SVzuqRGB1V27HgdORcCiFlW3ElJr79SJQPJYx+m8mD2BKby+QEg0iiMvuetczmgXQw12DhYpRfrcg==
error: NU3004: The package is not signed.
Package signature validation failed.In conclusion
Now you have all the tools you need to sign your .NET executables, or your nupkgs from your GitHub actions, increasing security as there are no API keys to leak, and making Windows Defender and other anti-malware software happier as your code is signed, and reliability of signing as there are no API keys to access your signing certificates that expire and need rotating.