În articolul anterior am văzut cum funcționează autentificarea cu certificat X.509 — clientul prezintă un certificat, serverul îl validează. Acesta este TLS unilateral: serverul se autentifică față de client (prin certificatul HTTPS), dar clientul se autentifică opțional.
mTLS (mutual TLS) face ambele direcții obligatorii — atât clientul cât și serverul prezintă și validează certificate. Nicio conexiune nu e acceptată fără identitate verificată criptografic de ambele părți.
Este mecanismul standard în arhitecturi de microservicii, service mesh (Istio, Linkerd), și orice scenariu în care zero-trust înseamnă literalmente zero: nicio componentă nu are încredere implicită în alta.
1. TLS vs. mTLS — diferența vizuală
TLS standard (one-way):
- Clientul se conectează la server
- Serverul prezintă certificatul său → clientul îl validează
- Conexiune criptată stabilită — serverul nu știe cine e clientul
mTLS (mutual TLS):
- Clientul se conectează la server
- Serverul prezintă certificatul său → clientul îl validează
- Serverul cere certificatul clientului → serverul îl validează
- Conexiune criptată stabilită — ambele identități sunt verificate criptografic
Tot handshake-ul se întâmplă la nivel TLS, înainte ca primul byte de date aplicație să fie trimis. Dacă validarea eșuează, conexiunea TCP e închisă — aplicația nici nu ajunge să proceseze request-ul.
2. Când folosești mTLS
mTLS nu este soluția pentru orice scenariu. Overhead-ul de gestionare a certificatelor este real. Îl alegi când:
- Microservicii interne — ServiceA apelează ServiceB fără să treacă prin internet; vrei să garantezi că ServiceB acceptă numai apeluri de la componente cunoscute
- API-uri B2B — un partener extern apelează API-ul tău; vrei să elimini complet parolele și API keys din ecuație
- Zero-trust networking — nicio componentă nu e considerată de încredere implicit, indiferent că e în același datacenter
- Conformitate — anumite standarde (PCI-DSS, HIPAA) cer autentificare mutuală pentru transferuri de date sensibile
- Service mesh — Istio / Linkerd pot impune mTLS automat între toate serviciile, transparent față de aplicație
Pentru aplicații web publice cu utilizatori umani, mTLS nu e potrivit — browserele nu gestionează ușor certificate client.
3. Arhitecturi posibile
Există două scenarii de deployment cu implicații diferite pentru ASP.NET Core:
Scenariul A — Kestrel expus direct
ASP.NET Core (Kestrel) termină TLS direct. Certificatul client ajunge la aplicație nativ, prin HttpContext.Connection.ClientCertificate. Cel mai simplu de implementat.
[Client cu cert] ──mTLS──▶ [Kestrel / ASP.NET Core]
Scenariul B — Reverse proxy în față
Nginx, Azure Application Gateway, sau IIS termină TLS. ASP.NET Core primește conexiuni HTTP(S) interne fără certificat client direct. Proxy-ul trebuie să transmită certificatul prin header.
[Client cu cert] ──mTLS──▶ [Nginx / App Gateway] ──HTTP──▶ [ASP.NET Core]
Acesta este scenariul cel mai frecvent în producție — și sursa majorității problemelor de configurare.
4. Scenariul A — mTLS direct cu Kestrel
Dacă Kestrel e expus direct (containere interne, servicii fără proxy), configurarea e directă — am văzut baza în articolul anterior, acum o extindem pentru mTLS complet.
4.1 Configurare Kestrel cu RequireCertificate
builder.WebHost.ConfigureKestrel(options =>
{
options.ListenAnyIP(5001, listenOptions =>
{
listenOptions.UseHttps(https =>
{
// Certificatul serverului
https.ServerCertificate = new X509Certificate2(
"server.pfx", "server-password");
// Obligă clientul să prezinte certificat
https.ClientCertificateMode = ClientCertificateMode.RequireCertificate;
// Validare TLS-level (înainte de middleware)
https.ClientCertificateValidation = (cert, chain, errors) =>
{
// Acceptăm certificate semnate de CA-ul nostru intern
var trustedCa = new X509Certificate2("ca.crt");
chain!.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust;
chain.ChainPolicy.CustomTrustStore.Add(trustedCa);
chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
return chain.Build(cert);
};
});
});
});
4.2 Middleware de autentificare
builder.Services
.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme)
.AddCertificate(options =>
{
options.AllowedCertificateTypes = CertificateTypes.Chained;
options.RevocationMode = X509RevocationMode.NoCheck; // Online în producție
options.Events = new CertificateAuthenticationEvents
{
OnCertificateValidated = context =>
{
var cert = context.ClientCertificate;
var cn = cert.GetNameInfo(X509NameType.SimpleName, false);
// Mapăm CN-ul la un serviciu cunoscut
var knownServices = new HashSet<string>
{
"billing-service",
"notification-service",
"gateway-service"
};
if (!knownServices.Contains(cn))
{
context.Fail($"Unknown service identity: {cn}");
return Task.CompletedTask;
}
var claims = new[]
{
new Claim(ClaimTypes.NameIdentifier, cert.Thumbprint),
new Claim(ClaimTypes.Name, cn!),
new Claim("service", cn!),
new Claim("cert-expiry",
cert.NotAfter.ToString("yyyy-MM-dd"))
};
context.Principal = new ClaimsPrincipal(
new ClaimsIdentity(claims, context.Scheme.Name));
context.Success();
return Task.CompletedTask;
}
};
});
app.UseAuthentication();
app.UseAuthorization();
5. Scenariul B — mTLS prin reverse proxy
Acesta e scenariul cel mai comun în producție și cel mai des configurat greșit. Proxy-ul termină TLS — ASP.NET Core nu vede certificatul client decât dacă proxy-ul îl transmite explicit prin header HTTP.
5.1 Nginx — configurare
server {
listen 443 ssl;
server_name api.myservice.com;
# Certificatul serverului
ssl_certificate /etc/nginx/certs/server.crt;
ssl_certificate_key /etc/nginx/certs/server.key;
# CA intern de încredere pentru certificate client
ssl_client_certificate /etc/nginx/certs/ca.crt;
# on = obligatoriu, optional = acceptă și fără certificat
ssl_verify_client on;
ssl_verify_depth 3;
location / {
proxy_pass http://aspnetcore-app:5000;
# Transmite certificatul client codificat URL spre ASP.NET Core
proxy_set_header X-ARR-ClientCert $ssl_client_escaped_cert;
# Headers standard de proxy
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $host;
}
}
5.2 Azure Application Gateway — configurare
Azure Application Gateway suportă mTLS nativ. Pașii în portal:
- SSL Settings → Client Authentication → Add — uploadezi certificatul CA (format
.cer, DER encoded) - Asociezi profilul SSL cu listener-ul HTTPS
- Application Gateway transmite automat certificatul client prin header-ul
X-ARR-ClientCert(URL-encoded, PEM format)
Cu Azure CLI:
# Creare profil SSL cu client authentication
az network application-gateway ssl-profile add \
--gateway-name myAppGateway \
--resource-group myRG \
--name mTLSProfile \
--client-auth-configuration requireClientCert
# Upload CA certificate
az network application-gateway client-cert add \
--gateway-name myAppGateway \
--resource-group myRG \
--name myCA \
--data ca.cer
5.3 ASP.NET Core — Certificate Forwarding
Acum că proxy-ul transmite certificatul prin header, ASP.NET Core trebuie configurat să îl citească de acolo:
// Program.cs
builder.Services.AddCertificateForwarding(options =>
{
// Header-ul setat de Nginx / Application Gateway
options.CertificateHeader = "X-ARR-ClientCert";
// Nginx trimite URL-encoded PEM — trebuie decodat
options.HeaderConverter = headerValue =>
{
if (string.IsNullOrEmpty(headerValue))
return null;
// URL decode
var decoded = Uri.UnescapeDataString(headerValue);
// Convertim PEM → X509Certificate2
var bytes = Encoding.UTF8.GetBytes(decoded);
return new X509Certificate2(bytes);
};
});
// ORDINEA CONTEAZĂ — CertificateForwarding ÎNAINTE de Authentication
app.UseCertificateForwarding();
app.UseAuthentication();
app.UseAuthorization();
Atenție la securitate: Dacă activezi
CertificateForwarding, oricine poate injecta header-ulX-ARR-ClientCertcu un certificat arbitrar. Asigură-te că numai proxy-ul de încredere poate ajunge la ASP.NET Core — prin network policy, firewall, sau acceptând conexiuni doar de la IP-ul proxy-ului.
6. Client .NET — cum trimiți certificatul în mTLS
Clientul mTLS în .NET este identic cu ce am văzut în articolul anterior — configurezi HttpClientHandler cu certificatul client. Diferența e că în mTLS serverul va valida și el certificatul, nu doar invers.
// ServiceA apelează ServiceB cu mTLS
var clientCert = new X509Certificate2(
"service-a-client.pfx",
Environment.GetEnvironmentVariable("CERT_PASSWORD"),
X509KeyStorageFlags.EphemeralKeySet); // EphemeralKeySet pe Linux/containere
var handler = new HttpClientHandler();
handler.ClientCertificates.Add(clientCert);
// Validare certificat server — în producție validezi CA-ul
handler.ServerCertificateCustomValidationCallback = (message, cert, chain, errors) =>
{
if (errors == SslPolicyErrors.None)
return true;
// Acceptăm certificate semnate de CA-ul nostru intern
var trustedCa = new X509Certificate2("ca.crt");
chain!.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust;
chain.ChainPolicy.CustomTrustStore.Add(trustedCa);
chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
return chain.Build(new X509Certificate2(cert!));
};
var client = new HttpClient(handler)
{
BaseAddress = new Uri("https://service-b:5001")
};
6.1 Înregistrare în DI cu IHttpClientFactory
builder.Services.AddHttpClient("ServiceBClient", client =>
{
client.BaseAddress = new Uri(
builder.Configuration["Services:ServiceB:BaseUrl"]!);
})
.ConfigurePrimaryHttpMessageHandler(sp =>
{
var certPath = builder.Configuration["Certificates:ClientCertPath"]!;
var certPassword = builder.Configuration["Certificates:ClientCertPassword"]!;
var cert = new X509Certificate2(certPath, certPassword,
X509KeyStorageFlags.EphemeralKeySet);
var caCert = new X509Certificate2(
builder.Configuration["Certificates:CACertPath"]!);
var handler = new HttpClientHandler();
handler.ClientCertificates.Add(cert);
handler.ServerCertificateCustomValidationCallback = (_, serverCert, chain, errors) =>
{
if (errors == SslPolicyErrors.None) return true;
chain!.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust;
chain.ChainPolicy.CustomTrustStore.Add(caCert);
chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
return chain.Build(new X509Certificate2(serverCert!));
};
return handler;
});
7. mTLS în arhitecturi de microservicii
Când ai zeci de servicii, gestionarea manuală a certificatelor devine rapid o problemă. Există două abordări:
7.1 Gestiune manuală (PKI intern)
Potrivit pentru un număr mic de servicii (sub 10). Menții un CA intern, emiți certificate per serviciu, automatizezi reînnoirea prin scripturi sau Azure Key Vault. Am detaliat această abordare în articolul anterior.
7.2 Service Mesh (Istio / Linkerd)
Pentru arhitecturi mari, un service mesh gestionează mTLS transparent față de aplicație. Nu scrii niciun cod de certificate în serviciile .NET — mesh-ul injectează un sidecar proxy (Envoy în Istio, Linkerd2-proxy în Linkerd) care face tot handshake-ul mTLS automat.
# Kubernetes: activare mTLS strict în Istio pentru un namespace
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
name: default
namespace: production
spec:
mtls:
mode: STRICT # refuză orice conexiune non-mTLS
Cu mode: STRICT, Istio garantează că nicio comunicare inter-servicii în namespace-ul production nu se face fără mTLS, indiferent ce scrie codul aplicației.
7.3 Certificate per serviciu — naming convention
Indiferent de abordare, convenția recomandată pentru CN în arhitecturi de microservicii:
# Format: <service-name>.<namespace>.<environment>
CN=billing-service.internal.production
CN=notification-service.internal.production
CN=gateway-service.internal.production
Naming consistent permite validarea automată fără whitelist manual — accepti orice certificat cu pattern *.internal.production semnat de CA-ul tău.
8. Testing mTLS cu curl și .http files
8.1 curl
# Test mTLS complet
curl --cert client.crt \
--key client.key \
--cacert ca.crt \
https://localhost:5001/api/data
# Cu fișier PFX
curl --cert-type P12 \
--cert client.pfx:dev-password \
--cacert ca.crt \
https://localhost:5001/api/data
# Verbose — vezi tot handshake-ul TLS
curl -v --cert client.crt --key client.key --cacert ca.crt \
https://localhost:5001/api/data
8.2 Test în cod — WebApplicationFactory
public class MtlsIntegrationTests : IClassFixture<WebApplicationFactory<Program>>
{
private readonly WebApplicationFactory<Program> _factory;
public MtlsIntegrationTests(WebApplicationFactory<Program> factory)
{
_factory = factory;
}
[Test]
public async Task Request_WithValidClientCert_Returns200()
{
var cert = new X509Certificate2("test-client.pfx", "test-password");
var client = _factory.WithWebHostBuilder(b =>
{
b.ConfigureKestrel(k =>
k.ConfigureHttpsDefaults(h =>
{
h.ClientCertificateMode = ClientCertificateMode.RequireCertificate;
h.ClientCertificateValidation = (c, _, _) => true;
}));
}).CreateClient();
// Injectăm certificatul direct în HttpContext pentru teste
// (WebApplicationFactory nu suportă TLS complet —
// folosim CertificateForwarding cu header mock)
var pemBytes = cert.Export(X509ContentType.Cert);
var pem = "-----BEGIN CERTIFICATE-----\n"
+ Convert.ToBase64String(pemBytes, Base64FormattingOptions.InsertLineBreaks)
+ "\n-----END CERTIFICATE-----";
client.DefaultRequestHeaders.Add(
"X-ARR-ClientCert", Uri.EscapeDataString(pem));
var response = await client.GetAsync("/api/data");
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK));
}
}
9. Probleme comune și soluțiile lor
| Problemă | Cauza probabilă | Soluție |
|---|---|---|
| Certificat client ignorat în spatele proxy-ului | AddCertificateForwarding nu e configurat sau ordinea middleware e greșită |
UseCertificateForwarding() înainte de UseAuthentication() |
Header X-ARR-ClientCert gol |
Nginx nu e configurat cu ssl_client_certificate și ssl_verify_client on |
Verifică configurarea Nginx; testează cu curl -v direct pe Nginx |
CryptographicException pe Linux |
Flag-uri de stocare incompatibile cu Linux | Folosește X509KeyStorageFlags.EphemeralKeySet în loc de MachineKeySet |
| Injectare header de oricine | ASP.NET Core acceptă X-ARR-ClientCert de la orice sursă |
Restricționează accesul la ASP.NET Core prin firewall / network policy la IP-ul proxy-ului |
| Chain validation eșuează în container | CA-ul nu e în trusted store al containerului | Folosește CustomRootTrust cu CA explicit sau adaugă CA în /etc/ssl/certs la build |
| mTLS funcționează local, pică în Azure | Application Gateway transmite cert diferit față de ce testezi local | Loghează header-ul X-ARR-ClientCert brut și verifică formatul (URL-encoded PEM vs Base64 DER) |
10. Checklist de producție
- ✅ ASP.NET Core accesibil numai de la proxy (network policy / firewall) — previne injectare header
- ✅
UseCertificateForwarding()înainteaUseAuthentication() - ✅
EphemeralKeySetpentru certificate încărcate în containere Linux - ✅ Logging pentru
OnAuthenticationFailedcu thumbprint și CN din certificatul respins - ✅ Alertă la expirare certificate (server și client) — minim 30 zile înainte
- ✅ Strategie de rotație fără downtime (whitelist cu două thumbprint-uri în perioadă de tranziție)
- ✅
RevocationMode = Onlineactivat dacă ai CRL / OCSP disponibil - ✅ Testat explicit: request fără certificat →
400/403, nu500 - ✅ Certificate servicii cu naming consistent (
<service>.internal.<env>) - ✅ CA intern stocat în Azure Key Vault, acces prin Managed Identity
Concluzie
mTLS adaugă un strat de autentificare la nivelul transportului — înainte ca aplicația să proceseze ceva. Combinat cu autorizarea la nivel de aplicație (claims, policies), creezi un sistem în care fiecare componentă știe cu certitudine cu cine vorbește.
Complexitatea reală nu e în configurarea ASP.NET Core, care e relativ compactă. E în infrastructură: cum gestionezi CA-ul intern, cum automatizezi emiterea și reînnoirea certificatelor, cum te asiguri că proxy-ul transmite corect certificatul clientului. Odată rezolvate acestea, mTLS devine transparent pentru codul aplicației.
Seria de securitate continuă cu un subiect diferit: semnarea documentelor XML și e-Factura — unde certificatele X.509 joacă din nou un rol central, dar într-un context complet diferit.