DEV Community

Cover image for Setting up mTLS and Kestrel
Russ Hammett
Russ Hammett

Posted on • Originally published at blog.kritner.com on

Setting up mTLS and Kestrel

Pretty sure everyone at this point knows what TLS is, but what about mTLS? How is it different from TLS, what’s it used for?

TLS

What is TLS? TLS, or Transport Layer Security, is the successor to SSL; both of which are means of secure communication. There have been several versions of TLS, each subsequent version being more secure, easier to use, or a combination of the two. We’re up to TLS v1.3.

You can read a lot more about TLS here.

The basic idea of TLS is to secure communications between multiple parties, you’re probably very used to “seeing” it when you visit websites like this one.

TLS on a website

There’s a lot of “magic” going on when connecting to a website, in the form of back and forth between client and server. The link I posted about goes into greater detail regarding this, but we can also pretty easily see some of it using a cURL command.

We’re going to use a testing web api project for this post, I’ll start it with:

mkdir Kritner.Mtls
cd Kritner.Mtls
dotnet new webapi
Enter fullscreen mode Exit fullscreen mode

Now run the project with dotnet run, and submit a cURL command to the default WeatherForecast controller:

curl --insecure -v https://localhost:5001/weatherForecast
Enter fullscreen mode Exit fullscreen mode

TLS establishment via cURL

You’ll notice in the above that we’re using the --insecure flag in our cURL command as we’re using a “development” certificate through the web api to establish secure connections.

mTLS

So now that we’ve established a very high level of what TLS is and what it looks like, what is mTLS?

From Wikipedia:

Mutual authentication or two-way authentication refers to two parties authenticating each other at the same time, being a default mode of authentication in some protocols (IKE, SSH) and optional in others (TLS).

By default the TLS protocol only proves the identity of the server to the client using X.509 certificate and the authentication of the client to the server is left to the application layer. TLS also offers client-to-server authentication using client-side X.509 authentication.[1] As it requires provisioning of the certificates to the clients and involves less user-friendly experience, it’s rarely used in end-user applications.

Mutual TLS authentication (mTLS) is much more widespread in business-to-business (B2B) applications, where a limited number of programmatic and homogeneous clients are connecting to specific web services, the operational burden is limited, and security requirements are usually much higher as compared to consumer environments.

There’s a fair amount of information in the above, but the tdlr in my opinion is:

  • both parties provide their identity by some means
  • often used in business to business applications

What this means is that application access can be controlled to our system through our system generating “passwords” for our users to use, in the form of certificates signed by our CA, that we provide back to them.

Note (I’m going to make it several times throughout the post) that the code is not set up in a way to verify that the client provided cert was signed by our CA, just that it is signed. This is not desired behavior, but I will try to handle the additional auth in another post. Additionally, you will often want to set up another layer of security than just the cert, dual auth of some sort provided by a one time password or something similar. This will help protect your system in an instance where a client’s cert/private key has made it out into the wild; without that “second factor” users won’t be able to get in (also not covered in this post).

mTLS Setup

mTLS, at least in the way we’re going to set it up in this post, has a few steps, many of which are outside the bounds of “coding”. A high level list of steps includes:

  • Create a local CA
  • Import the CA as a trusted root CA for our “server” (our local machine in this case)
  • Create a certificate for use by the “client” which is signed by the CA
  • Enable/enforce client certificates in our .net core application
  • run a cURL command against our code again, without providing a cert, see our request is denied
  • run a cURL command against our code, this time providing our client cert, see our request gets through

Create a local CA

I followed this tutorial: https://deliciousbrains.com/ssl-certificate-authority-for-local-https-development/

# Generate a key
openssl genrsa -aes256 -out myCA.key 2048

# Generate root certificate
openssl req -x509 -new -nodes -key myCA.key -sha256 -days 10240 -out myCA.pem

# Create a .crt file so it can be installed on yucky windows (can *probably* just out in this format from the step above, but i don't know much about openssl)
openssl x509 -ou
Enter fullscreen mode Exit fullscreen mode

Import the CA cert as a trusted root CA

Now install the crt as a trusted root authority by double clicking it and “install cert”:

Install cert as trusted root CA

Create a certificate signed by the CA to be used by the client

create a file client.ext with the following information:

authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE
keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
Enter fullscreen mode Exit fullscreen mode
# Generate a key for the "client" to use
openssl genrsa -out client.key 2048

# Generate a Certificate Signing Request (csr)
openssl req -new -key client.key -out client.csr

# Using the CA, create client cert based on the CSR
openssl x509 -req -in client.csr -CA myCA.pem -CAkey myCA.key -CAcreateserial -out client.crt -days 1024 -sha256 -extfile client.ext
Enter fullscreen mode Exit fullscreen mode

You should now have a client.crt available, and when viewing, you should be able to see the “full certificate chain” in that the certificate was signed by the myCa (kritnerCa in my case):

Signed Certificate

Enable mTLS from Kestrel/.net core code

It’s pretty straight forward getting mTLS working with Kestrel, a bit more involved with IIS (which I may cover in another post…?)

Add to the project file a NuGet package that allows for client certificate authentication:

<ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.Authentication.Certificate" Version="3.1.0" />
</ItemGroup>
Enter fullscreen mode Exit fullscreen mode

We’ll be adding “Require Client Certificate” to our application bootstrapping in the Program.cs:

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<Startup>();
            // vvv requires client certificate when connecting vvv 
            webBuilder.ConfigureKestrel(options =>
            {
                options.ConfigureHttpsDefaults(configureOptions =>
                {
                    configureOptions.ClientCertificateMode = ClientCertificateMode.RequireCertificate;
                });
            });
            // ^^^ requires client certificate when connecting ^^^
        });
Enter fullscreen mode Exit fullscreen mode

Then in the Startup.cs, we’ll need to update ConfigureServices and Configure to set up the authentication and register the authentication middleware.

ConfigureServices:

public void ConfigureServices(IServiceCollection services)
{
    services
        .AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme)
        .AddCertificate(options =>
        {
            // Only allow chained certs, no self signed
            options.AllowedCertificateTypes = CertificateTypes.Chained;
            // Don't perform the check if a certificate has been revoked - requires an "online CA", which was not set up in our case.
            options.RevocationMode = X509RevocationMode.NoCheck;
            options.Events = new CertificateAuthenticationEvents()
            {
                OnAuthenticationFailed = context =>
                {
                    var logger = context.HttpContext.RequestServices.GetService<ILogger<Startup>>();

                    logger.LogError(context.Exception, "Failed auth.");

                    return Task.CompletedTask;
                },
                OnCertificateValidated = context =>
                {
                    var logger = context.HttpContext.RequestServices.GetService<ILogger<Startup>>();

                    // You should implement a service that confirms the certificate passed in
                    // was signed by the root CA.

                    // Otherwise, a certificate that is valid to one of the other trusted CAs on the webserver,
                    // would be valid in this case as well.

                    logger.LogInformation("You did it my dudes!");

                    return Task.CompletedTask;
                } 
            };
        });

    services.AddControllers();
}
Enter fullscreen mode Exit fullscreen mode

Please be aware of the comments in the above code block. If you do not implement your own validation to go on top of the normal cert validation, then any valid certificate passed in from the client will be allowed, regardless of whether or not it was signed by the CA we created earlier in the post. I’m not going to cover writing such a validator in this post, but I’ll try to remember to do so in another; this post is taking me more time than I had intended already!

Configure:

app.UseAuthentication();
Enter fullscreen mode Exit fullscreen mode

Note the above app.UseAuthentication should be done after app.UseRouting(); and before app.UseAuthorization();. The whole Configure method now looks like this:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    app.UseHttpsRedirection();

    app.UseRouting();

    // vvv Order is important vvv
    app.UseAuthentication();
    // ^^^ Order is important ^^^
    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
    });
}
Enter fullscreen mode Exit fullscreen mode

Testing it out

Now we have mTLS set up in regards to our system, and our code. Let’s give it a run!

First, start the web application.

Next, let’s try our same curl command we used in the beginning of the post:

curl --insecure -v https://localhost:5001/weatherForecast
Enter fullscreen mode Exit fullscreen mode

which looks like:

cURL command no cert provided

The above makes sense, we haven’t provided a certificate to the web application, so we are being rejected.

Now let’s make sure we can actually get in with our signed cert, using the following command:

curl --insecure -v --key client.key --cert client.crt https://localhost:5001/weatherForecast
Enter fullscreen mode Exit fullscreen mode

which looks like:

cURL command with cert provided

it works!

Self Notes / future posts

  • Cover setting up mTLS on IIS - there are registry settings that need to be updated in some cases (yuck!)
  • Setting up a custom certificate validator, right now we’re just letting in any cert that is not self signed, rather than checking that the signed cert was signed by our CA.
  • Multi factor auth

References

Top comments (1)

Collapse
 
thejoezack profile image
Joe Zack

This stuff can be such a nightmare when you don't know what you are doing. Thanks for the great straight-forward write up!