Skip to content
Introducing the next era of Duende IdentityServer. Read our CEO’s announcement

mTLS Setup Guide

Mutual TLS support in Duende IdentityServer enables two distinct security features:

  1. Client authentication — Clients present an X.509 certificate to authenticate to IdentityServer endpoints (instead of or in addition to client secrets)
  2. Sender-constrained tokens — Access tokens are cryptographically bound to the client’s certificate, preventing token theft and replay

Both features are defined in RFC 8705: OAuth 2.0 Mutual-TLS Client Authentication and Certificate-Bound Access Tokens.

This guide covers the infrastructure side: generating certificates for local development, configuring reverse proxies in production, and loading certificates from secure stores. For IdentityServer client registration and options, see Client Authentication. For API-side token validation, see Proof-of-Possession.

The recommended tool for local mTLS development is mkcert, a cross-platform utility that creates locally trusted certificates backed by a private CA installed on your machine. Unlike the certificates bundled with samples (which use publicly known private keys), mkcert generates certificates whose private keys never leave your machine.

  1. Install mkcert

    Install via WinGet:

    Terminal window
    winget install FiloSottile.mkcert

    Or via Chocolatey:

    Terminal window
    choco install mkcert
  2. Create a Local Certificate Authority

    Install a local CA that will sign your development certificates:

    Terminal window
    mkcert -install

    This adds the CA to your OS trust store (and browser trust stores where supported). Certificates signed by this CA are treated as trusted on your machine.

  3. Generate Certificates

    You will need a server certificate for the IdentityServer and API hostnames:

    Terminal window
    mkcert localhost mtls.localhost api.localhost

    This produces localhost+2.pem and localhost+2-key.pem in the current directory. Reference these files in your Kestrel HTTPS configuration.

    Next, generate a client certificate in PKCS#12 format (used by the client application):

    Terminal window
    mkcert -client -pkcs12 localhost

    This produces localhost-client.p12. Reference this file path when constructing the X509Certificate2 for the .NET client.

  4. Configure the Hosts File

    The mTLS sample uses mtls.localhost and api.localhost as hostnames. Add them to your hosts file so they resolve to 127.0.0.1:

    Open C:\Windows\System32\drivers\etc\hosts as Administrator and add:

    127.0.0.1 mtls.localhost
    127.0.0.1 api.localhost
  5. Configure Kestrel

    Kestrel must be configured to accept client certificates before ASP.NET Core’s certificate authentication middleware can validate them. Use AllowCertificate mode so that client certificates are accepted but not required on every connection — IdentityServer’s mTLS middleware enforces the requirement on the mTLS endpoint specifically.

    Program.cs
    builder.WebHost.ConfigureKestrel(serverOptions =>
    {
    serverOptions.ConfigureHttpsDefaults(httpsOptions =>
    {
    httpsOptions.ClientCertificateMode = ClientCertificateMode.AllowCertificate;
    });
    });

    Then configure the IdentityServer mTLS options and ASP.NET Core certificate authentication:

    Program.cs
    builder.Services.AddIdentityServer(options =>
    {
    options.MutualTls.Enabled = true;
    options.MutualTls.DomainName = "mtls"; // serves mTLS endpoints at mtls.localhost
    options.MutualTls.ClientCertificateAuthenticationScheme = "Certificate";
    })
    .AddMutualTlsSecretValidators();
    builder.Services.AddAuthentication()
    .AddCertificate("Certificate", options =>
    {
    // Use CertificateTypes.All or CertificateTypes.Chained in production
    options.AllowedCertificateTypes = CertificateTypes.SelfSigned;
    });

In production, a reverse proxy (IIS, Nginx, or Apache) handles TLS termination and forwards the client certificate to Kestrel via a request header. This separates certificate negotiation from the application tier and supports load-balanced deployments.

When running behind a reverse proxy, configure Kestrel to skip TLS-level certificate negotiation and instead read the forwarded certificate from a request header:

Program.cs
// Tell Kestrel not to negotiate a client cert at the TLS layer
builder.WebHost.ConfigureKestrel(serverOptions =>
{
serverOptions.ConfigureHttpsDefaults(httpsOptions =>
{
httpsOptions.ClientCertificateMode = ClientCertificateMode.NoCertificate;
});
});
// Read the client certificate from the forwarded header instead
builder.Services.AddCertificateForwarding(options =>
{
// Must match the header name your reverse proxy sends (see configs below)
options.CertificateHeader = "X-SSL-CERT";
options.HeaderConverter = headerValue =>
{
if (string.IsNullOrWhiteSpace(headerValue))
return null;
// Reverse proxies typically URL-encode the PEM data
var certPem = Uri.UnescapeDataString(headerValue);
return X509Certificate2.CreateFromPem(certPem);
};
});

Register the middleware in the pipeline before UseAuthentication:

app.UseCertificateForwarding();
app.UseAuthentication();
app.UseAuthorization();

Also update AddCertificate to accept chained certificates from a real CA in production:

builder.Services.AddAuthentication()
.AddCertificate("Certificate", options =>
{
options.AllowedCertificateTypes = CertificateTypes.Chained;
});
  1. Open IIS Manager → select the mTLS site → SSL Settings.
  2. Set Client Certificates to Require.
  3. Install the Application Request Routing (ARR) module to enable IIS as a reverse proxy.
  4. Configure ARR to forward requests to Kestrel. ARR automatically includes the client certificate in the X-ARR-ClientCert header when proxying to the backend.
  5. Update the CertificateHeader in your ASP.NET Core certificate forwarding configuration to match:
builder.Services.AddCertificateForwarding(options =>
{
options.CertificateHeader = "X-ARR-ClientCert";
options.HeaderConverter = headerValue =>
{
if (string.IsNullOrWhiteSpace(headerValue))
return null;
// ARR sends the certificate as a base64-encoded DER blob
byte[] certBytes = Convert.FromBase64String(headerValue);
return new X509Certificate2(certBytes);
};
});

For full IIS hosting guidance, see Host ASP.NET Core on Windows with IIS and Configure certificate authentication on Microsoft Learn.

In production, certificates must be loaded from a secure store, not from disk paths in application configuration. The following examples show how to load a certificate programmatically for use with Kestrel or an outbound HttpClient.

Use X509Store to load a certificate by thumbprint from the local machine store:

X509Certificate2 LoadFromStore(string thumbprint)
{
using var store = new X509Store(StoreName.My, StoreLocation.LocalMachine);
store.Open(OpenFlags.ReadOnly);
var results = store.Certificates.Find(
X509FindType.FindByThumbprint,
thumbprint,
validOnly: true);
if (results.Count == 0)
throw new InvalidOperationException(
$"Certificate with thumbprint '{thumbprint}' not found in LocalMachine\\My.");
return results[0];
}

Import the certificate using certlm.msc (Local Machine Certificates MMC snap-in) or PowerShell:

Terminal window
Import-PfxCertificate `
-FilePath client.p12 `
-CertStoreLocation Cert:\LocalMachine\My `
-Password (Read-Host -Prompt "PFX password (leave blank if none)" -AsSecureString)
Then wire it into Kestrel or an outbound HTTP handler:
```csharp
var cert = LoadFromStore(configuration["Mtls:Thumbprint"]);
// Kestrel server certificate
builder.WebHost.ConfigureKestrel(o =>
o.ConfigureHttpsDefaults(h => h.ServerCertificate = cert));
// Outbound mTLS client
var handler = new SocketsHttpHandler();
handler.SslOptions.ClientCertificates = new X509CertificateCollection { cert };
var httpClient = new HttpClient(handler);

IdentityServer can expose mTLS endpoints using three different URL strategies. Configure the strategy via MutualTls.DomainName:

StrategyDomainName ValueExample Endpoint
Path-basednull or emptyhttps://identity.example.com/connect/mtls/token
Sub-domain"mtls"https://mtls.identity.example.com/connect/token
Separate domain"identity-mtls.example.com"https://identity-mtls.example.com/connect/token
// Path-based (default) — mTLS endpoints under /connect/mtls/*
options.MutualTls.DomainName = null;
// Sub-domain — mTLS endpoints on mtls.{yourdomain}
options.MutualTls.DomainName = "mtls";
// Separate domain — mTLS endpoints on a completely different domain
options.MutualTls.DomainName = "identity-mtls.example.com";

IdentityServer publishes the mTLS endpoint URLs in the discovery document under mtls_endpoint_aliases. Clients should use these aliases when mTLS is required:

var disco = await client.GetDiscoveryDocumentAsync("https://identity.example.com");
// Use the mTLS token endpoint alias
var tokenEndpoint = disco.MtlsEndpointAliases?.TokenEndpoint
?? disco.TokenEndpoint;

The discovery document will include an mtls_endpoint_aliases section like:

{
"issuer": "https://identity.example.com",
"token_endpoint": "https://identity.example.com/connect/token",
"mtls_endpoint_aliases": {
"token_endpoint": "https://mtls.identity.example.com/connect/token",
"revocation_endpoint": "https://mtls.identity.example.com/connect/revocation",
"introspection_endpoint": "https://mtls.identity.example.com/connect/introspect",
"device_authorization_endpoint": "https://mtls.identity.example.com/connect/deviceauthorization"
}
}