Using a Signum Agent, a GitHub Actions signing workflow can run on any of the standard runners or their self-hosted equivalents.
This integration guide explains how to set up a GitHub Actions workflow by building a .NET project on a Windows runner, authenticating the user and publishing certificates, and signing the resulting binaries with SignTool. The guide is for the Windows Agent, but it is possible to adapt it to the other Agents as well.
Signing keys stay in Signum's HSM. The Agent exposes the certificates your account is entitled to through the Windows certificate store, so SignTool selects one by thumbprint exactly as if it were a local certificate. The private key operation is forwarded to Signum and the key never reaches the GitHub runner.
For setup with the legacy Windows Agent instead, see Using the Legacy Windows Agent.
Prerequisites
In Signum
-
A user account on the Signum instance, LocalUsers or Certificate domain type. See Domains.
-
A code-signing certificate in the Signum instance.
-
A Signum policy granting the user account the code-signing certificate for the SignTool app.
-
Thumbprint of the signing certificate.
-
Your Signum server URL (Deployment URL, without the
<https://> prefix).
Certificate-based Authentication Option: As an alternative to using the CI user’s password as a GitHub Secret, Signum supports authenticating to an Agent with a client certificate instead. To use this, you need Set Up Certificate-based Authentication.
In GitHub
-
A GitHub repository with a buildable project (this guide uses .NET, but the signing steps are language-agnostic).
-
A Windows runner. Signing is a Windows operation: the Agent is a Windows service and SignTool is a Windows SDK tool.
-
Admin permissions on the repository to configure Secrets and Variables.
Runner Requirements
GitHub-hosted windows-latest runner meet Signum's baseline OS requirements out of the box. If you are using a self-hosted runner instead, match the OS-specific prerequisites for whichever OS you are targeting (Windows Server 2019+/Windows 11).
GitHub-hosted Runner (windows-latest)
Self-hosted Runner
Signum Server requirement:
Reachable from the public internet
Reachable from your network
Signum network policy must accept:
GitHub's runner IP ranges, which are broad and change
Your runner's address
Agent installation:
Every run, from a URL the runner can reach
Once, ahead of time
Agent installer distribution:
You must solve this (see below)
Not a workflow concern
Runner lifetime:
Ephemeral, clean state each run
Persistent, Agent session outlives the job
Install Agent onto the Hosted Runner
The .msi is distributed through the Signum SaaS portal, which requires authentication, so a workflow cannot download it directly. Either:
-
Republish the .msi to a location your runner can reach: an internal artifact repository, or blob storage. If the URL must carry a credential (a SAS token, a signed URL), store it as a secret, not a variable.
Or
-
Pre-install the Agent on a self-hosted runner, and have the workflow detect it and skip installation.
Use specific Agent version that matches your Signum server version rather the latest one.
Resources
This guide includes an example YAML pipeline configuration:
|
Resource |
Link |
|---|---|
|
YAML pipeline |
Set Up .NET Project
Before writing your workflow YAML, store all sensitive values as GitHub Secrets and non-sensitive values as GitHub Variables.
Step 1 - Configure GitHub Secrets and Variables
-
Navigate to your repository Settings > Secrets and variables > Actions.
-
Create the following secrets:
It is not recommended to put credentials directly in the YAML file. Use GitHub Secrets for storing sensitive information.
|
Secret |
Description |
Example |
Required |
|---|---|---|---|
|
|
Password for the Signum service account |
|
|
|
|
URL to the Agent |
|
* |
-
Create the following variables:
|
Variable |
Purpose |
Example |
Required |
|---|---|---|---|
|
|
Signum deployment URL with no https:// prefix |
|
|
|
|
Signum service account, in the form username@domain, where the domain is the Local User Domain Alias |
|
|
|
|
SHA-1 thumbprint of the code-signing certificate |
|
|
|
|
URL to the Agent |
|
* |
|
|
Runner label for the |
|
|
|
|
|
|
|
|
|
Port the connectivity check probes |
|
|
* Set SIGNUM_AGENT_MSI_URL as either a secret or variable, depending on if the URL contains a token or not.
Step 2 - Adjust the Trigger and Global Environment
Triggers shown in the following workflow (running on v* tags and manual dispatch) are only an example of when to run signing. Choose the right triggers for your pipeline, such as branches, tags, events, or environments should invoke signing.
Adjust the on: section to match your project's release process and your organization's policy on when signing should occur:
# Only on release tags
on:
push:
tags: ['v*']
# Manual only
on:
workflow_dispatch:
# Push to main + manual
on:
push:
branches: [main]
workflow_dispatch:
defaults:
run:
# Explicit so the scripts below behave the same on GitHub-hosted and
# self-hosted Windows runners (PowerShell 7, not Windows PowerShell 5.1).
shell: pwsh
env:
DOTNET_VERSION: "8.0.x"
CONFIGURATION: Release
PUBLISH_DIR: artifacts/publish
DOTNET_NOLOGO: "true"
DOTNET_CLI_TELEMETRY_OPTOUT: "true"
Step 3 - Build .NET Project
The workflow follows the standard .NET steps: checkout, setup SDK, restore, build, and publish. This phase is independent of Signum and can be adapted to any technology.
The dotnet build step shown in the workflow is only an example, based on a simple .NET project, to illustrate where the build fits in the pipeline:
jobs:
build:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: "8.0.x"
- run: dotnet publish src/SignumSample.App/SignumSample.App.csproj -c Release -o $env:PUBLISH_DIR
- uses: actions/upload-artifact@v4
with:
name: unsigned
path: ${{ env.PUBLISH_DIR }}
Step 4 - Set Environment Variables
If you did not install the Agent on the runner, some environment variables must be set. This is related to the .NET configuration and depends on the build environment:
sign:
needs: build
runs-on: ${{ vars.SIGNING_RUNNER || 'windows-latest' }}
env:
SIGNUM_UTIL: C:\Program Files\Keyfactor\Signum\setup\signum-util.exe
SIGNUM_SERVICE: C:\Program Files\Keyfactor\Signum\service\SignumService.exe
SIGNUM_AGENT_MSI_URL: ${{ vars.SIGNUM_AGENT_MSI_URL }}
SIGNUM_PRIMARY_SERVER: ${{ vars.SIGNUM_PRIMARY_SERVER }}
SIGNUM_USERNAME: ${{ vars.SIGNUM_USERNAME }}
SIGNUM_PASSWORD: ${{ secrets.SIGNUM_PASSWORD }}
SIGNUM_CERT_THUMBPRINT: ${{ vars.SIGNUM_CERT_THUMBPRINT }}
steps:
- uses: actions/download-artifact@v4
with:
name: unsigned
path: ${{ env.PUBLISH_DIR }}
Authenticate and Publish the Certificates
After setting up the .NET project, configure your Signum deployment with user authentication and certificate storage.
If you did not install the Agent on the runner, install the Agent now. For instructions, see Install Windows Agent.
Step 5 - Authenticate User
Run the following to authenticate the user and store the session:
signum-util setup -h $env:SIGNUM_PRIMARY_SERVER -u $env:SIGNUM_USERNAME -p $env:SIGNUM_PASSWORD
|
Setup Flag |
Meaning |
|---|---|
|
|
Signum deployment URL. Do not include the |
|
|
Username as username@domain, where the domain is the Local User Domain Alias |
|
|
Password. Omit it to be prompted; a workflow has to pass it. The |
|
|
HTTPS proxy, when the runner reaches Signum through one |
|
|
CLI log level: |
Step 6 - Publish Certificates
Run the following to add the entitled certificates to the Windows certificate store, bound to the Signum KSP:
signum-util cs -a -t My
|
Certificate Store Flag |
Meaning |
|---|---|
|
|
Add the available certificates to the Windows certificate store |
|
|
Remove the Signum certificates from the store |
|
|
Target store: |
Sign Artifacts
With the certificate(s) visible in the store, native tools can sign binaries transparently. The signing command depends on the artifact type, not the CI platform.
Step 7 - Sign Artifacts with SignTool
With the certificates visible in the store, SignTool signs the binaries transparently through the Signum KSP:
- name: Sign, verify and run
run: |
$signtool = (Get-ChildItem "C:\Program Files (x86)\Windows Kits\10\bin\*\x64\signtool.exe" | Sort-Object FullName)[-1].FullName
$files = "$env:PUBLISH_DIR\SignumSample.App.exe", "$env:PUBLISH_DIR\SignumSample.Library.dll"
& $signtool sign /fd SHA256 /sha1 $env:SIGNUM_CERT_THUMBPRINT /tr http://timestamp.digicert.com /td SHA256 /v @files
|
SignTool Flag |
Description |
|---|---|
|
|
Use the LocalMachine certificate store. Required only when the certificates were published with |
|
|
Select certificate by subject name (resilient to renewals). |
|
|
Select certificate by thumbprint (alternative to |
|
|
Hash algorithm for the signature . |
|
|
Hash algorithm for the timestamp . |
|
|
RFC 3161 timestamp authority. Timestamping keeps signatures valid after the certificate expires. |
|
|
Verbose output . |
Step 8 - Verify Signature (Optional)
Verify each signature. For SignTool, use signtool verify:
signtool.exe verify /pa /v @files
Step 9 - Upload Artifact
Upload the signed binaries as a GitHub Actions artifact:
- name: Upload signed artifacts
uses: actions/upload-artifact@v4
with:
name: signed
path: ${{ env.PUBLISH_DIR }}
if-no-files-found: error
Step 10 - Cleanup
Log out of the Signum session and remove temporary files. This step uses if: always() so it runs even if a previous step fails:
- name: Log the agent out
if: always()
run: |
& $env:SIGNUM_UTIL logout
exit 0
GitHub Actions appends exit to every pwsh step, so the explicit exit 0 prevents a failed logout from masking the real failure of the job.
Troubleshooting
The following solutions are for common issues that may occur. To find the logs for the Agents, see Logging.
Certificate Does Not Appear After Login
-
Verify that the user has a certificate assigned through a Signum policy.
-
Wait a few seconds after login. Add
Start-Sleep -Seconds 10in the workflow. -
Check which store the certificates were installed into:
# Search in LocalMachine
Get-ChildItem Cert:\LocalMachine\My | Select-Object Subject, Thumbprint
# Search in CurrentUser
Get-ChildItem Cert:\CurrentUser\My | Select-Object Subject, Thumbprint
SignumService IPC endpoint is not reachable
Every signum-util command fails this way when no agent is serving the named pipe. In order of likelihood:
-
The Agent was never started. A silent install does not launch it and the per-user service cannot start without an interactive logon.
-
The Agent is running as a different Windows account than signum-util. The pipe is Keyfactor_Signum_<SID>; check that the account that started the Agent is the account running the CLI.
-
The Agent started and exited. Check the agent log, and the Windows Application log for an Application Error or Windows Error Reporting record naming SignumService.
-
The Agent is still starting. The example waits 10 seconds after launching it; a slow runner may need more.
-
To confirm what is being served, list the pipes and look for the one named after your SID:
[System.IO.Directory]::GetFiles("\\.\pipe\") | Where-Object { $_ -match "Signum" }
Test-Path "\\.\pipe\Keyfactor_Signum_$(([System.Security.Principal.WindowsIdentity]::GetCurrent()).User.Value)"
SignTool: "No certificates were found that met all the given criteria"
-
If using Server Mode, make sure you include the
/smflag in the SignTool command. -
Verify that the
/nvalue matches the certificate's CN exactly. -
Check that the certificate has the Code Signing EKU (Enhanced Key Usage):
Get-ChildItem Cert:\LocalMachine\My | Where-Object {
$_.EnhancedKeyUsageList.FriendlyName -contains "Code Signing"
} | Select-Object Subject, Thumbprint
Connection Error to the Signum Server
-
Verify that
RTPRIMARYdoes not include the<https://> prefix. -
If the runner is behind a proxy, configure the
WEBPROXY_URIparameter in the MSI installation. -
Review the installation log (
signum-install.log) and RTService logs.
MSI Installation Fails
-
Review the log:
Get-Content signum-install.log -Tail 80 -
Exit code 1603: generic failure, usually a permissions or prerequisites issue.
-
Exit code 3010: installation succeeded but requires a reboot (can be ignored in CI).
-
Ensure the runner has administrator privileges.
Signing is Slow
-
Each signature requires a network call to the remote HSM. This is expected to be slower than local signing.
-
If you are signing many files, consider signing only the main executables rather than all third-party DLLs.
Using the Legacy Windows Agent
To integrate your legacy Windows Agent with GitHub Actions, use Server Mode, which is a headless installation controlled entirely through the command line. The following information covers the installation and authentication of the legacy Windows Agent.
This guide includes a complete YAML pipeline configuration for the legacy Windows Agent:
|
Resource Description |
Link |
|---|---|
|
Legacy Windows Agent YAML pipeline |
When configuring the .NET project, in addition to the other GitHub secrets and variables, create the following:
|
Secret |
Purpose |
Example |
Required |
|---|---|---|---|
|
|
Use the Client ID from the Signum SaaS portal |
|
|
|
Variable |
Purpose |
Example |
Required |
|
|
Signum deployment URL ( |
|
|
|
|
|
|
|
|
|
Port the connectivity check probes |
|
|
Check Configuration (Optional)
Before continuing the GitHub action execution, it is recommended to perform a check. In this example, these tasks are performed by scripts that are in the same repository, but can be performed directly in the pipeline.
-
Check out the workflow scripts:
- name: Check out the workflow scripts
uses: actions/checkout@v4
with:
sparse-checkout: .github/scripts
sparse-checkout-cone-mode: false
-
Check required configuration:
# -----------------------------------------------------------------------
# Fail fast with a readable message when the repo is not configured yet,
# instead of a cryptic msiexec or rtsetup error some steps later.
# -----------------------------------------------------------------------
- name: Check required configuration
run: |
$required = @(
"SIGNUM_PRIMARY_SERVER",
"SIGNUM_CLIENT_ID",
"SIGNUM_USERNAME",
"SIGNUM_PASSWORD",
"SIGNUM_CERT_THUMBPRINT"
)
$missing = $required | Where-Object {
[string]::IsNullOrWhiteSpace((Get-Item "env:$_" -ErrorAction SilentlyContinue).Value)
}
if ($missing) {
throw "Missing repository configuration: $($missing -join ', '). " +
"See README.md for which are variables and which are secrets."
}
Write-Host "Configuration present. Target certificate store: $env:SIGNUM_TARGET_STORE"
-
Test connectivity to check that the runner can actually reach the Signum server before installing the Agent:
.github/scripts/Test-SignumConnectivity.ps1
-Server "$env:SIGNUM_PRIMARY_SERVER"
-Port $env:SIGNUM_SERVICE_PORT
-TimeoutSeconds 10
Installation
For installation instructions, see Windows Agent - Server Mode (CLI Interface).
Wait for the Signum Agent Service
The msiexec returns before RTService finishes starting. Running rtsetup.exe too soon races the service, showing up as a confusing connection or login error, including on a pre-installed self-hosted runner, where the install step is skipped but the service could still be stopped.
Run the following:
# -----------------------------------------------------------------------
# Wait for the agent service to come up.
# -----------------------------------------------------------------------
- name: Wait for the Signum agent service
run: >
.github/scripts/Wait-AgentService.ps1
-ServiceName RTService
-TimeoutSeconds 120
-AllowStart
Authenticate
Once the Agent is installed, use rtsetup.exe (located at C:\\Program Files\\KeyFactor\\) to authenticate the user. This starts the RTService and makes the assigned certificates appear in the Windows certificate store.
- name: Add Signum to PATH
run: |
$env:PATH = "C:\Program Files\KeyFactor\;" + $env:PATH
echo "PATH=$env:PATH" >> $env:GITHUB_ENV
- name: Authenticate with Signum
run: |
echo "${{ secrets.SIGNUM_PASSWORD }}" | rtsetup.exe `
-authMode=LocalUsers `
-username="${{ secrets.SIGNUM_USERNAME }}" `
-targetStore=LocalMachine
Confirm Certificate Visibility
# -----------------------------------------------------------------------
# Confirm the agent actually surfaced the expected certificate.
# -----------------------------------------------------------------------
- name: Verify the signing certificate is available
run: >
.github/scripts/Test-SigningCertificate.ps1
-Thumbprint "$env:SIGNUM_CERT_THUMBPRINT"
-TargetStore "$env:SIGNUM_TARGET_STORE"
Verify
- name: Verify certificate is present in store
run: |
$certs = Get-ChildItem -Path Cert:\LocalMachine\My | Where-Object {
$_.Subject -like "${{ secrets.CERT_SUBJECT_NAME }}"
}
if (-not $certs) {
Write-Host "::error::Certificate '${{ secrets.CERT_SUBJECT_NAME }}' not found in LocalMachine\\My"
Write-Host "Available certificates in LocalMachine\\My:"
Get-ChildItem -Path Cert:\\LocalMachine\\My | ForEach-Object {
Write-Host " Subject: $($_.Subject) | Thumbprint: $($_.Thumbprint)"
}
exit 1
}
foreach ($cert in $certs) {
Write-Host "Certificate found:"
Write-Host " Subject: $($cert.Subject)"
Write-Host " Thumbprint: $($cert.Thumbprint)"
Write-Host " Valid: $($cert.NotBefore) - $($cert.NotAfter)"
}