Service-to-Service Authentication using mTLS

In the microservices world it may be necessary for one microservice to call another microservice. However, the calls between the microservices must be authenticated in order to achieve Zero Trust (ZT). That is, no implicit trust is granted, ever. Just because the microservices are on the same network or is not externally exposed does not mean that a service is trusted - continuous verification is the core principle.

How do we authenticate calls between microservices?

There are several mechanisms that can be used to authenticate between services, for example, Mutual Transport Layer Security (mTLS), API Keys, Single Sign-On Gateways, etc.  In this post, I'll show you how to implement mTLS in a .NET 6 application.

What is mutual TLS?

mTLS requires the client to verify the server's identity and server to verify the client's identity. Generally, a TLS certificate is only issued for server authentication. However, a client certificate should have the client authentication extended key usage attribute (EKU) set. That is, Client Authentication (OID 1.3.6.1.5.5.7.3.2).

Certificate validation

Before the server accepts the client certificate it should validate it to confirm the following:

  1. That the certificate extended key usage allows for client authentication;
  2. Ensure that the certificate is not expired;
  3. Ensure that the certificate chain is valid;
  4. Ensure that the certificate has not been revoked by checking against a certificate revocation list (CRL) or an online certificate status protocol (OCSP) service.
  5. Ensure that the client certificate is the expected one by checking, for example, the certificate's thumbprint.

Example

To attach a client certificate to an HTTP request, we should setup an HttpClientHandler, as shown below, where cert.pfx and password should match the certificate's filename and password:

var handler = new HttpClientHandler();
var certificate = new X509Certificate2(@"cert.pfx", "password");
handler.ClientCertificates.Add(certificate);

using var client = new HttpClient(handler);

To validate the certificate on the server side, we need to install the microsoft.aspnetcore.authentication.certificate NuGet package and configure it as shown below:

builder.Services.AddAuthentication(
  CertificateAuthenticationDefaults.AuthenticationScheme
).AddCertificate(options =>
   {
        options.ValidateCertificateUse = true;
        options.AllowedCertificateTypes = CertificateTypes.Chained;
        options.Events = new CertificateAuthenticationEvents
        {
            OnAuthenticationFailed = context =>
            {
                context.Fail("Certificate authentication failed");
                return Task.CompletedTask;
            },
            OnCertificateValidated = context =>
            {
                if (ValidCertificate(context.ClientCertificate))
                    context.Success();
                else
                    context.Fail("Certificate authentication failed");

                return Task.CompletedTask;
            }
        };
   });
   
   ...
   app.UseAuthentication();

The client certificate is now being validated as part of the request pipeline. However, we need tell Kestrel that we will accept client certificates otherwise the client certificates will not be attached.

builder.Services.Configure<KestrelServerOptions>(options =>
{
    options.ConfigureHttpsDefaults(opt => 
      opt.ClientCertificateMode = ClientCertificateMode.AllowCertificate);
});

The final step is to ensure that the endpoint is authorized with a client certificate and can be done by applying the AuthorizeAttribute to the controller action:

[Authorize(AuthenticationSchemes = CertificateAuthenticationDefaults.AuthenticationScheme)]

A working example can be found in the Straypaper GitHub repository.