DotNetMTlsLayer 0.1.1
dotnet add package DotNetMTlsLayer --version 0.1.1
NuGet\Install-Package DotNetMTlsLayer -Version 0.1.1
<PackageReference Include="DotNetMTlsLayer" Version="0.1.1" />
<PackageVersion Include="DotNetMTlsLayer" Version="0.1.1" />
<PackageReference Include="DotNetMTlsLayer" />
paket add DotNetMTlsLayer --version 0.1.1
#r "nuget: DotNetMTlsLayer, 0.1.1"
#:package DotNetMTlsLayer@0.1.1
#addin nuget:?package=DotNetMTlsLayer&version=0.1.1
#tool nuget:?package=DotNetMTlsLayer&version=0.1.1
DotNetMTlsLayer
DotNetMTlsLayer is a .NET 8 library for mutual TLS between two APIs. It handles both sides of the exchange:
- Kestrel HTTPS configuration for inbound client certificates
- ASP.NET Core certificate authentication and authorization
HttpClientregistration for outbound mTLS calls- shared certificate trust validation logic
Contents
- What the library gives you
- Project layout
- Certificate model
- Using the library in your own APIs
- Configuration walkthrough
- Example server setup
- Example client setup
- Self-signed or pinned peers
- Runnable sample APIs
- GitHub CI/CD
- Troubleshooting
- Running the tests
What the library gives you
The library does not issue certificates for you. It assumes you already have:
- a server certificate for each API
- a client certificate for each API when it calls the other API
- either a shared/internal CA or explicit thumbprint pinning
What it does provide:
- Kestrel helpers to require or allow client certificates during the TLS handshake
- certificate-auth registration so a trusted client certificate becomes an authenticated ASP.NET Core user
- outbound
HttpClientregistration that automatically sends a client certificate - shared trust evaluation for inbound and outbound peer validation
- certificate loading helpers for
.pfx,.pem, Base64 PFX, and Windows certificate store lookup
Project layout
src/DotNetMTlsLayer: reusable library codetests/DotNetMTlsLayer.Tests: unit testssamples/ApiA: sample API Asamples/ApiB: sample API Bsamples/certs: generated sample certificatestools/SampleCertificatesGenerator: local certificate generator for the samples
Certificate model
In a typical two-API setup:
- API A hosts HTTPS with its server certificate.
- API B hosts HTTPS with its server certificate.
- API A presents its client certificate when calling API B.
- API B presents its client certificate when calling API A.
- Both APIs trust either:
- the same internal root CA, or
- each other's exact certificate thumbprints.
The library supports both CA-based trust and direct pinning.
Flow diagram
sequenceDiagram
participant ApiA as API A
participant ApiB as API B
ApiA->>ApiB: HTTPS request with client certificate
ApiB->>ApiA: Server certificate during TLS handshake
ApiA->>ApiA: Validate API B server certificate
ApiB->>ApiB: Validate API A client certificate
ApiB->>ApiB: Build ClaimsPrincipal from certificate
ApiB-->>ApiA: Protected response
Using the library in your own APIs
1. Reference the library
If the library is in the same solution:
dotnet add YourApi.csproj reference src/DotNetMTlsLayer/DotNetMTlsLayer.csproj
2. Load certificates at startup
Most API-to-API mTLS setups need:
- one server certificate for the current API
- one client certificate for outbound calls from the current API
- one trusted root certificate, or explicit thumbprints/subject names for peers
Example:
var serverCertificate = CertificateLoader.LoadFromPfxFile("certs/api-a-server.pfx", "password");
var clientCertificate = CertificateLoader.LoadFromPfxFile("certs/api-a-client.pfx", "password");
var trustedRoot = CertificateLoader.LoadPublicCertificate("certs/internal-root-ca.cer");
3. Configure inbound mTLS on Kestrel
Use UseMutualTls on the listener. This configures HTTPS and validates incoming client certificates.
builder.WebHost.ConfigureKestrel(kestrel =>
{
kestrel.ListenAnyIP(5001, listen =>
{
listen.UseMutualTls(options =>
{
options.ServerCertificate = serverCertificate;
options.ClientCertificateTrust.TrustedRoots.Add(trustedRoot);
options.ClientCertificateTrust.AllowSubjectName("api-b-client");
});
});
});
4. Configure ASP.NET Core certificate authentication
This turns a validated client certificate into an authenticated user that can be used by authorization.
builder.Services.AddMutualTlsAuthentication(options =>
{
options.ClientCertificateTrust.TrustedRoots.Add(trustedRoot);
options.ClientCertificateTrust.AllowSubjectName("api-b-client");
});
builder.Services.AddAuthorization();
5. Protect endpoints
The library creates a default policy named MutualTlsDefaults.AuthorizationPolicyName.
app.UseAuthentication();
app.UseAuthorization();
app.MapGet("/secure/data", () => Results.Ok("secure"))
.RequireAuthorization(MutualTlsDefaults.AuthorizationPolicyName);
6. Configure outbound mTLS for peer API calls
Use AddMutualTlsHttpClient so the outgoing HttpClient sends your client certificate and validates the peer server certificate.
builder.Services.AddMutualTlsHttpClient("ApiB", options =>
{
options.BaseAddress = new Uri("https://api-b.internal:5002");
options.ClientCertificate = clientCertificate;
options.ServerCertificateTrust.TrustedRoots.Add(trustedRoot);
options.ServerCertificateTrust.AllowSubjectName("api-b-server");
});
7. Call the peer API
app.MapGet("/call-peer", async (IHttpClientFactory httpClientFactory) =>
{
var client = httpClientFactory.CreateClient("ApiB");
var response = await client.GetStringAsync("/secure/data");
return Results.Ok(response);
});
Configuration walkthrough
These are the main types you will use:
CertificateLoaderLoads certificates from.pfx,.pem, Base64 PFX data, or the Windows certificate store.MutualTlsTrustOptionsDefines what you trust:TrustedRootsIntermediateCertificates- allowed thumbprints
- allowed subject names
- revocation and chain policy options
MutualTlsServerTlsOptionsControls the HTTPS listener:ServerCertificateClientCertificateMode- inbound trust rules
MutualTlsServerAuthenticationOptionsControls certificate authentication and the default authorization policy.MutualTlsHttpClientOptionsControls outbound mTLS:BaseAddressClientCertificate- peer server trust options
Common trust setups:
- Internal CA:
Add the CA certificate to
TrustedRootsand optionally restrict accepted peers by subject name. - Direct pinning:
Call
AllowThumbprint(...)and setAllowThumbprintPinningWithoutChainValidation = true. - Hybrid: Trust a CA but still pin a subject or thumbprint for a tighter allow-list.
Example server setup
using DotNetMTlsLayer;
var builder = WebApplication.CreateBuilder(args);
var serverCertificate = CertificateLoader.LoadFromPfxFile("certs/api-a-server.pfx", "password");
var trustedClientRoot = CertificateLoader.LoadPublicCertificate("certs/internal-root-ca.cer");
builder.WebHost.ConfigureKestrel(kestrel =>
{
kestrel.ListenAnyIP(5001, listen =>
{
listen.UseMutualTls(options =>
{
options.ServerCertificate = serverCertificate;
options.ClientCertificateTrust.TrustedRoots.Add(trustedClientRoot);
});
});
});
builder.Services.AddMutualTlsAuthentication(options =>
{
options.ClientCertificateTrust.TrustedRoots.Add(trustedClientRoot);
options.ClientCertificateTrust.AllowSubjectName("api-b-client");
});
builder.Services.AddAuthorization();
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.MapGet("/secure", () => Results.Ok("mTLS OK"))
.RequireAuthorization(MutualTlsDefaults.AuthorizationPolicyName);
app.Run();
Example client setup
using DotNetMTlsLayer;
var builder = WebApplication.CreateBuilder(args);
var clientCertificate = CertificateLoader.LoadFromPfxFile("certs/api-b-client.pfx", "password");
var trustedServerRoot = CertificateLoader.LoadPublicCertificate("certs/internal-root-ca.cer");
builder.Services.AddMutualTlsHttpClient("ApiA", options =>
{
options.BaseAddress = new Uri("https://api-a.internal:5001");
options.ClientCertificate = clientCertificate;
options.ServerCertificateTrust.TrustedRoots.Add(trustedServerRoot);
options.ServerCertificateTrust.AllowSubjectName("api-a-server");
});
Self-signed or pinned peers
If both APIs pin each other by thumbprint instead of using a CA, enable thumbprint pinning explicitly:
options.ServerCertificateTrust.AllowThumbprint("7A3C...");
options.ServerCertificateTrust.AllowThumbprintPinningWithoutChainValidation = true;
The same option exists on ClientCertificateTrust for the server side. Thumbprint pinning works, but it shifts trust management to certificate distribution and rotation.
Runnable sample APIs
The repo includes two runnable sample APIs in samples/ApiA and samples/ApiB, plus a certificate generator in tools/SampleCertificatesGenerator.
1. Generate sample certificates
dotnet run --project tools/SampleCertificatesGenerator
This creates:
samples/certs/mtls-root.cersamples/certs/api-a-server.pfxsamples/certs/api-a-client.pfxsamples/certs/api-b-server.pfxsamples/certs/api-b-client.pfxsamples/certs/operator-client.pfx
All generated PFX files use the password sample-password.
2. Run the sample APIs
Open two terminals from the repository root:
dotnet run --project samples/ApiA
dotnet run --project samples/ApiB
API addresses:
ApiA:https://localhost:5001ApiB:https://localhost:5002
3. Trigger API-to-API mTLS
Once both APIs are running, you can trigger an API-to-API mTLS call without presenting a browser certificate yourself:
curl -k https://localhost:5001/demo/call-peer
curl -k https://localhost:5002/demo/call-peer
Each API will:
- create an outbound
HttpClientconfigured with its own client certificate - call the peer API's
/secure/whoamiendpoint - validate the peer server certificate against the generated root CA
- present its client certificate to the peer
- return the authenticated identity details from the peer
4. Call a protected endpoint directly
Use the generated operator certificate to call /secure/whoami yourself.
PowerShell example:
$cert = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new(
"samples/certs/operator-client.pfx",
"sample-password")
Invoke-RestMethod `
-Uri "https://localhost:5001/secure/whoami" `
-Certificate $cert `
-SkipCertificateCheck
Without a client certificate, the /secure/* endpoints should return unauthorized or forbidden depending on the request path and auth state.
More sample-specific detail is in samples/README.md.
GitHub CI/CD
The repository now includes two GitHub Actions workflows:
ci.yml behavior:
- runs on every push
- runs on every pull request
- can also be started manually
- restores, builds, and runs the solution tests
release.yml behavior:
- runs when a git tag matching
v*is pushed - can also be started manually with:
versionpublish_to_nuget
- restores, builds, and tests the solution
- packs
src/DotNetMTlsLayer - uploads the
.nupkgand.snupkgas workflow artifacts - creates a GitHub release
- optionally pushes the package and symbol package to NuGet.org
Release versioning:
- tag-driven releases use the tag version, for example
v1.2.3becomes package version1.2.3 - manual releases use the
versioninput exactly as provided
Required GitHub repository secret:
NUGET_API_KEY: NuGet.org API key with push permission for the package ID
The release workflow automatically injects the active GitHub repository URL into the package metadata during CI publishing.
If you also plan to pack locally outside GitHub Actions, replace these placeholder values in src/DotNetMTlsLayer/DotNetMTlsLayer.csproj:
RepositoryUrlPackageProjectUrl
Example release commands:
git tag v1.0.0
git push origin v1.0.0
Or run release.yml manually from GitHub Actions and provide the version in the workflow form.
Troubleshooting
The certificate did not match any configured thumbprint or subject name.Your allow-list does not match the presented certificate. Check the configured subject names or thumbprints.TLS policy errors: RemoteCertificateNameMismatchThe hostname you are calling does not match the server certificate. Fix DNS/URL usage or issue the server certificate with the correct subject alternative name.UntrustedRootThe presented certificate chain does not build to a trusted root. Add the root CA toTrustedRootsor switch to explicit pinning.- Protected endpoints return
401or403Make sure the client presented a certificate and thatUseAuthentication()andUseAuthorization()are in the middleware pipeline. - Sample APIs fail at startup because certificate files are missing
Run
dotnet run --project tools/SampleCertificatesGeneratorfirst.
Running the tests
dotnet test DotNetMTlsLayer.sln
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net8.0
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.1.1 | 109 | 3/12/2026 |