.NET Certificate Management
Everything you need for working with certificates in .NET: dev certs, loading certs in code, Kestrel HTTPS, client certificates for mTLS, Azure Key Vault integration, and debugging common errors.
dotnet dev-certs
The dotnet dev-certs tool manages the ASP.NET Core HTTPS development certificate.
Generate and trust the dev certificate:
dotnet dev-certs https --trustClean (remove) existing dev certificates:
dotnet dev-certs https --cleanExport the dev certificate as PFX:
dotnet dev-certs https --export-path ./dev-cert.pfx --password "MyPassword123" --format PfxExport as PEM (useful for Docker/Nginx):
dotnet dev-certs https --export-path ./dev-cert.pem --format Pem --no-passwordCheck if the dev certificate is already trusted:
dotnet dev-certs https --check --trustX509Certificate2
The X509Certificate2 class is the primary way to work with certificates in .NET code.
Load from a PFX file
Load a certificate with its private key from a PFX:
using System.Security.Cryptography.X509Certificates;
var cert = new X509Certificate2(
"server.pfx",
"password",
X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet
);
Console.WriteLine($"Subject: {cert.Subject}");
Console.WriteLine($"Thumbprint: {cert.Thumbprint}");
Console.WriteLine($"Expires: {cert.NotAfter}");
Console.WriteLine($"Has private key: {cert.HasPrivateKey}");Load from a PEM file
Load a certificate and key from PEM files (.NET 6+):
var cert = X509Certificate2.CreateFromPemFile(
"server.crt",
"server.key"
);Load from an embedded resource
Load a certificate embedded in the assembly:
var assembly = typeof(Program).Assembly;
using var stream = assembly.GetManifestResourceStream("MyApp.certs.server.pfx");
using var ms = new MemoryStream();
stream!.CopyTo(ms);
var cert = new X509Certificate2(ms.ToArray(), "password");X509Store
Use X509Store to interact with the Windows certificate store or the platform equivalent.
Find a certificate by thumbprint:
using var store = new X509Store(StoreName.My, StoreLocation.LocalMachine);
store.Open(OpenFlags.ReadOnly);
var certs = store.Certificates.Find(
X509FindType.FindByThumbprint,
"A1B2C3D4E5F6...",
validOnly: false
);
if (certs.Count > 0)
{
var cert = certs[0];
Console.WriteLine($"Found: {cert.Subject}");
}
store.Close();Add a certificate to the store programmatically:
using var store = new X509Store(StoreName.Root, StoreLocation.LocalMachine);
store.Open(OpenFlags.ReadWrite);
var cert = new X509Certificate2("myca.crt");
store.Add(cert);
store.Close();Warning
Adding to StoreName.Root with StoreLocation.LocalMachine requires elevated privileges. On Windows this means running as admin; on Linux the store is per-user.
Kestrel HTTPS Configuration
Via appsettings.json
Configure Kestrel HTTPS endpoints in appsettings.json:
{
"Kestrel": {
"Endpoints": {
"Https": {
"Url": "https://0.0.0.0:5001",
"Certificate": {
"Path": "/app/certs/server.pfx",
"Password": "password"
}
}
}
}
}Via code (Program.cs)
Configure HTTPS in code with full control:
var builder = WebApplication.CreateBuilder(args);
builder.WebHost.ConfigureKestrel(options =>
{
options.ListenAnyIP(5001, listenOptions =>
{
listenOptions.UseHttps(httpsOptions =>
{
var cert = new X509Certificate2("server.pfx", "password");
httpsOptions.ServerCertificate = cert;
});
});
});Load from certificate store
Load the Kestrel certificate from the Windows certificate store:
{
"Kestrel": {
"Endpoints": {
"Https": {
"Url": "https://0.0.0.0:5001",
"Certificate": {
"Subject": "myapp.local",
"Store": "My",
"Location": "LocalMachine",
"AllowInvalid": false
}
}
}
}
}Client Certificates (mTLS)
Send a client certificate with HttpClient for mutual TLS authentication.
Configure HttpClient with a client certificate:
var clientCert = new X509Certificate2("client.pfx", "password");
var handler = new HttpClientHandler();
handler.ClientCertificates.Add(clientCert);
// Optional: accept self-signed server certs in development
// handler.ServerCertificateCustomValidationCallback =
// HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;
var client = new HttpClient(handler);
var response = await client.GetAsync("https://api.internal:5001/health");Require client certificates on the server side (Kestrel):
builder.WebHost.ConfigureKestrel(options =>
{
options.ListenAnyIP(5001, listenOptions =>
{
listenOptions.UseHttps(httpsOptions =>
{
httpsOptions.ServerCertificate = new X509Certificate2("server.pfx", "password");
httpsOptions.ClientCertificateMode = ClientCertificateMode.RequireCertificate;
httpsOptions.ClientCertificateValidation = (cert, chain, errors) =>
{
// Custom validation logic
return cert.Thumbprint == "EXPECTED_THUMBPRINT";
};
});
});
});Azure Key Vault
Load certificates from Azure Key Vault using the Azure.Security.KeyVault.Certificates package.
Install the NuGet packages:
dotnet add package Azure.Security.KeyVault.Certificates
dotnet add package Azure.IdentityDownload a certificate with its private key from Key Vault:
using Azure.Identity;
using Azure.Security.KeyVault.Certificates;
using Azure.Security.KeyVault.Secrets;
var vaultUri = new Uri("https://mykeyvault.vault.azure.net/");
var credential = new DefaultAzureCredential();
// Get the certificate (public part only)
var certClient = new CertificateClient(vaultUri, credential);
var certResponse = await certClient.GetCertificateAsync("my-cert");
// To get the private key, download via the Secrets API
var secretClient = new SecretClient(vaultUri, credential);
var secret = await secretClient.GetSecretAsync(certResponse.Value.Name);
// The secret value is a Base64-encoded PFX
var pfxBytes = Convert.FromBase64String(secret.Value.Value);
var cert = new X509Certificate2(pfxBytes);
Console.WriteLine($"Subject: {cert.Subject}");
Console.WriteLine($"Has key: {cert.HasPrivateKey}");Tip
Use DefaultAzureCredential for seamless local-to-Azure auth. It tries Managed Identity in Azure, Visual Studio credentials locally, then Azure CLI auth as a fallback.
Load from Environment Variable
A common pattern for CI/CD: store the certificate as a base64-encoded PEM or PFX in an environment variable.
Set the environment variable (bash):
export CERT_PFX_BASE64=$(base64 -w0 server.pfx)
export CERT_PASSWORD="password"Load in .NET code:
var certBase64 = Environment.GetEnvironmentVariable("CERT_PFX_BASE64")
?? throw new InvalidOperationException("CERT_PFX_BASE64 not set");
var certPassword = Environment.GetEnvironmentVariable("CERT_PASSWORD") ?? "";
var certBytes = Convert.FromBase64String(certBase64);
var cert = new X509Certificate2(certBytes, certPassword);Certificate Validation in Middleware
Add certificate authentication middleware:
// Program.cs
builder.Services.AddAuthentication(
CertificateAuthenticationDefaults.AuthenticationScheme)
.AddCertificate(options =>
{
options.AllowedCertificateTypes = CertificateTypes.All;
options.RevocationMode = X509RevocationMode.NoCheck; // dev only
options.Events = new CertificateAuthenticationEvents
{
OnCertificateValidated = context =>
{
var thumbprint = context.ClientCertificate.Thumbprint;
// Validate against allowed list, database, etc.
context.Success();
return Task.CompletedTask;
},
OnAuthenticationFailed = context =>
{
context.Fail("Certificate validation failed");
return Task.CompletedTask;
}
};
});
// Don't forget to add the middleware
app.UseAuthentication();
app.UseAuthorization();Install the required package:
dotnet add package Microsoft.AspNetCore.Authentication.CertificateCommon .NET Certificate Errors
“The remote certificate is invalid according to the validation procedure”
This is the most common .NET TLS error. It means the server certificate failed validation. Common causes:
Self-signed cert not trusted— Add the CA to the trust store or use a custom validation callback in development.
Hostname mismatch— The URL hostname does not match the certificate's CN or SAN.
Expired certificate— The certificate's NotAfter date has passed.
Incomplete chain— The server is not sending the intermediate certificates.
Debug the exact error with a custom callback:
var handler = new HttpClientHandler
{
ServerCertificateCustomValidationCallback = (message, cert, chain, errors) =>
{
Console.WriteLine($"SSL Errors: {errors}");
Console.WriteLine($"Certificate: {cert?.Subject}");
Console.WriteLine($"Issuer: {cert?.Issuer}");
Console.WriteLine($"Valid: {cert?.NotBefore} - {cert?.NotAfter}");
if (chain != null)
{
foreach (var status in chain.ChainStatus)
{
Console.WriteLine($"Chain status: {status.StatusInformation}");
}
}
return false; // Still reject — this is for debugging only
}
};Warning
Never use DangerousAcceptAnyServerCertificateValidator in production. It completely disables certificate validation, making the connection vulnerable to man-in-the-middle attacks.
“No credentials are available in the security package”
Usually means the PFX was loaded without the private key flag. Ensure you pass the correct X509KeyStorageFlags:
Load PFX with correct key storage flags:
var cert = new X509Certificate2(
"server.pfx",
"password",
X509KeyStorageFlags.MachineKeySet
| X509KeyStorageFlags.PersistKeySet
| X509KeyStorageFlags.Exportable
);“The certificate chain was issued by an authority that is not trusted”
The issuing CA is not in the machine's trust store. Fix by adding the root CA to the trusted roots:
Trust the CA on Windows:
Import-Certificate -FilePath myca.crt -CertStoreLocation Cert:\LocalMachine\RootTrust the CA on Linux:
sudo cp myca.crt /usr/local/share/ca-certificates/
sudo update-ca-certificates