I recently had to run an Umbraco CMS site locally inside a GitHub Actions workflow. The workflow needed to:
- Start the Umbraco site with HTTPS on
https://localhost:44340 - Make API calls to the Umbraco Management API
Sounds simple, right? Well, it turned into a bit of a nightmare involving Windows certificate management, PowerShell scripting, and GitHub Actions limitations.
The core issue: ASP.NET Core development sites use self-signed HTTPS certificates, and Windows won’t trust them without user interaction — something you can’t do in a CI/CD environment.
My first thought was: “I’ll just add a step in the workflow to create and trust the dev certificate programmatically.” Here’s what I tried:
- name: Create and trust dev certificate
run: |
dotnet dev-certs https --clean
dotnet dev-certs https --export-path $env:TEMP\dev-cert.pfx --password "password123"
$cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2
$cert.Import("$env:TEMP\dev-cert.pfx", "password123", "MachineKeySet")
$store = New-Object System.Security.Cryptography.X509Certificates.X509Store("Root","LocalMachine")
$store.Open("ReadWrite")
$store.Add($cert)
$store.Close()
Create and trust dev certificate
Result: The workflow hung indefinitely. The certificate import to the Root store was waiting for some kind of confirmation that never comes in an automated environment.
Learning: Even “programmatic” certificate operations can have hidden interactive requirements on Windows.
I thought maybe dotnet dev-certs was the problem, so I switched to PowerShell’s New-SelfSignedCertificate:
$cert = New-SelfSignedCertificate -DnsName "localhost" -CertStoreLocation "cert:\LocalMachine\My" -FriendlyName "ASP.NET Core HTTPS development certificate"
$certPath = "C:\temp\dev-cert.pfx"
$password = ConvertTo-SecureString -String "password123" -Force -AsPlainText
Export-PfxCertificate -Cert $cert -FilePath $certPath -Password $password
Import-PfxCertificate -FilePath $certPath -CertStoreLocation Cert:\LocalMachine\Root -Password $password
New-SelfSignedCertificate
Result: Same issue. The import to the Root store hung for 3+ minutes before timing out.
Learning: The problem wasn’t the tool; it was Windows requiring admin consent to modify the trusted root store.
Maybe I didn’t need the certificate in the Root store? I tried removing that step and just keeping it in LocalMachine\My, then configuring Kestrel to allow invalid certs:
$env:ASPNETCORE_Kestrel__Certificates__Default__AllowInvalid = "true"
Start-Process -FilePath "dotnet" -ArgumentList "run --project Clean.Blog.csproj"
Skip the root store
Result: The site started fine, but when my PowerShell script tried to make HTTPS calls to the Management API, they failed with certificate validation errors.
Learning: The server accepting its own certificate is different from the client (PowerShell) trusting it.
I found a blog post suggesting this:
- name: Install SSL Certificates
run: |
dotnet dev-certs https --trust
Result: Hung again. The --trust flag launches a modal dialog asking “Do you want to install this certificate?” — which you can’t click in a headless runner.
Learning: The --trust flag requires interactive consent. Great for local dev, useless for CI/CD.
After all these failed attempts, I asked around. My friend https://mattou07.net/ shared a GitHub Action that gave me an idea:
Why try to trust the certificate at all?
The site and the PowerShell script are both running on the same machine in a controlled build environment. There’s no security risk in bypassing certificate validation for these localhost calls.
Here’s what worked:
No more trying to install or trust certificates. Just let ASP.NET Core create its default dev cert:
- name: Setup .NET 10
uses: actions/setup-dotnet@v4
with:
dotnet-version: '10.0.x'
# That’s it — no certificate trust step needed!
Remove the Certificate Trust Step
# Skip SSL certificate validation for localhost calls (needed for CI/CD with self-signed certs)
if ($PSVersionTable.PSVersion.Major -ge 6) {
# PowerShell Core 6+ approach
Write-Verbose "Configuring certificate validation bypass for PowerShell Core"
} else {
# Windows PowerShell 5.x approach
Write-Verbose "Configuring certificate validation bypass for Windows PowerShell"
add-type @"
using System.Net;
using System.Security.Cryptography.X509Certificates;
public class TrustAllCertsPolicy : ICertificatePolicy {
public bool CheckValidationResult(
ServicePoint svcPoint, X509Certificate certificate,
WebRequest request, int certificateProblem) {
return true;
}
}
"@
[System.Net.ServicePointManager]::CertificatePolicy = New-Object TrustAllCertsPolicy
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12
}
Configure PowerShell to Skip Certificate Validation
# Health check
$webRequestParams = @{
Uri = "https://localhost:44340/umbraco"
UseBasicParsing = $true
ErrorAction = "Stop"
}
if ($PSVersionTable.PSVersion.Major -ge 6) {
$webRequestParams['SkipCertificateCheck'] = $true
}
$response = Invoke-WebRequest @webRequestParams
# API calls
$tokenParams = @{
Method = "Post"
Uri = $tokenUrl
Body = $tokenBody
Headers = $tokenHeaders
ErrorAction = "Stop"
}
if ($PSVersionTable.PSVersion.Major -ge 6) {
$tokenParams['SkipCertificateCheck'] = $true
}
$response = Invoke-RestMethod @tokenParams
Update HTTP Calls to Use Certificate Bypass
Result: Success! The site starts with its self-signed HTTPS cert, and the PowerShell script can make API calls without SSL errors.
- Don’t fight the system — Instead of trying to trust a cert non-interactively (which is hard for security reasons), bypass validation in a controlled way.
- Different approaches for different PowerShell versions — Windows PowerShell 5.x uses
ServicePointManager, PowerShell Core 6+ has-SkipCertificateCheck. - Security context matters — Skipping validation is safe when:
- Both client and server are on the same machine
- You control both
- It’s a temporary build environment
- You’re only calling localhost
Here’s the final GitHub Actions workflow:
name: PR - Build NuGet Packages
on:
pull_request:
branches:
- main
jobs:
build-packages:
runs-on: windows-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup .NET 10
uses: actions/setup-dotnet@v4
with:
dotnet-version: '10.0.x'
- name: Run CreateNuGetPackages script
shell: pwsh
run: |
./CreateNuGetPackages.ps1 -Version "1.0.0.${{ github.run_number }}"
- name: Upload NuGet packages as artifacts
uses: actions/upload-artifact@v4
with:
name: nuget-packages
path: .artifacts/nuget/*.nupkg
Clean, simple, and it works. No certificate gymnastics required.
Sometimes the best solution isn’t forcing the “right” way but finding a simpler alternative. In this case, bypassing certificate validation for localhost was far easier than trying to trust certs on Windows.
If you’re struggling with HTTPS dev certs in GitHub Actions, I hope this saves you the time I spent figuring it out.
Related Resources:
- https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/invoke-webrequest
- https://docs.microsoft.com/en-us/aspnet/core/security/enforcing-ssl#trust-the-aspnet-core-https-development-certificate-on-windows-and-macos
- https://docs.microsoft.com/en-us/dotnet/api/system.net.servicepointmanager.certificatepolicy