Configuring HTTPS in ASP.NET Core across different platforms

Daniel Roth

As the web moves to be more secure by default it’s more important than ever to make sure your websites have HTTPS enabled. And if you’re going to use HTTPS in production its a good idea to develop with HTTPS enabled so that your development environment is as close to your production environment as possible. In this blog post we’re going to go through how to setup an ASP.NET Core app with HTTPS for local development on Windows, Mac, and Linux.

This post is primarily focused on enabling HTTPS in ASP.NET Core during development using Kestrel. When using Visual Studio you can alternatively enable HTTPS in the Debug tab of your app to easily have IIS Express enable HTTPS without it going all the way to Kestrel. This closely mimics what you would have if you’re handling HTTPS connections in production using IIS. However, when running from the command-line or in a non-Windows environment you must instead enable HTTPS directly using Kestrel.

The basic steps we will use for each OS are:

  1. Create a self-signed certificate that Kestrel can use
  2. Optionally trust the certificate so that your browser will not warn you about using a self-signed certificate
  3. Configure Kestrel to use that certificate

Kestrel HTTPS sample app

Create a certificate

Windows

Use the New-SelfSignedCertificate Powershell cmdlet to generate a suitable certificate for development:

New-SelfSignedCertificate -NotBefore (Get-Date) -NotAfter (Get-Date).AddYears(1) -Subject "localhost" -KeyAlgorithm "RSA" -KeyLength 2048 -HashAlgorithm "SHA256" -CertStoreLocation "Cert:\CurrentUser\My" -KeyUsage KeyEncipherment -FriendlyName "HTTPS development certificate" -TextExtension @("2.5.29.19={critical}{text}","2.5.29.37={critical}{text}1.3.6.1.5.5.7.3.1","2.5.29.17={critical}{text}DNS=localhost")

Linux & Mac

For Linux and Mac we will use OpenSSL.

Create a file https.config with the following data:

[ req ]
default_bits       = 2048
default_md         = sha256
default_keyfile    = key.pem
prompt             = no
encrypt_key        = no

distinguished_name = req_distinguished_name
req_extensions     = v3_req
x509_extensions    = v3_req

[ req_distinguished_name ]
commonName             = "localhost"

[ v3_req ]
subjectAltName      = DNS:localhost
basicConstraints    = critical, CA:false
keyUsage            = critical, keyEncipherment
extendedKeyUsage    = critical, 1.3.6.1.5.5.7.3.1

Run the following command to generate a private key and a certificate signing request:

openssl req -config https.config -new -out csr.pem

Run the following command to create a self-signed certificate:

openssl x509 -req -days 365 -extfile https.config -extensions v3_req -in csr.pem -signkey key.pem -out https.crt

Run the following command to generate a pfx file containing the certificate and the private key that you can use with Kestrel.

openssl pkcs12 -export -out https.pfx -inkey key.pem -in https.crt -password pass:<<Password>>

Trust the certificate

This step is optional, but without it the browser will warn you about your site being potentially unsafe. You will see something like the following if you browser doesn’t trust your certificate:

cert_error_chrome

Windows

To trust the generated certificate on Windows you need to add it to the current user’s trusted root store:

  1. Run certmgr.msc
  2. Find the certificate under Personal/Certificates. The “Issued To” field should be localhost and the “Friendly Name” should be HTTPS development certificate

    image

  3. Copy the certificate and paste it under Trusted Root Certification Authorities/Certificates

  4. When Windows presents a security warning dialog to confirm you want to trust the certificate, click on “Yes”.

    image

Linux

There is no centralized way of trusting the a certificate on Linux so you can do one of the following:

  1. Cxclude the URL you are using in your browsers exclude list
  2. Trust all self-signed certificates on localhost
  3. Add the https.crt to the list of trusted certificates in your browser.

How exactly to achieve this depends on your browser/distro, and we aren’t going to through all the options here.

Mac

Option 1: command line

Run the following command

sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain https.crt

Some browsers, such as Chrome, require you to restart them before this trust will take affect.

Option 2: Keychain UI

If you open the “Keychain Access” app you can drag your https.crt into the Login keychain,

Configure Kestrel to use the certificate we generated

The following code will read a set of HTTP server endpoint configurations from your app configuration settings and then apply them to Kestrel including setting up HTTPS using the specified certificate.

Application Code

This code will read a set of HTTP server endpoint configurations from a custom section in your app configuration settings and then apply them to Kestrel. The endpoint configurations include settings for configuring HTTPS, like which certificate to use. Add the code for the ConfigureEndpoints extension method to your application and then call it when setting up Kestrel for your host in Program.cs:

public class Program
{
    public static void Main(string[] args)
    {
        BuildWebHost(args).Run();
    }

    public static IWebHost BuildWebHost(string[] args) =>
        WebHost.CreateDefaultBuilder(args)
            .UseStartup<Startup>()
            .UseKestrel(options => options.ConfigureEndpoints())
            .Build();
}

public static class KestrelServerOptionsExtensions
{
    public static void ConfigureEndpoints(this KestrelServerOptions options)
    {
        var configuration = options.ApplicationServices.GetRequiredService<IConfiguration>();
        var environment = options.ApplicationServices.GetRequiredService<IHostingEnvironment>();

        var endpoints = configuration.GetSection("HttpServer:Endpoints")
            .GetChildren()
            .ToDictionary(section => section.Key, section =>
            {
                var endpoint = new EndpointConfiguration(); 
                section.Bind(endpoint); 
                return endpoint;
            });

        foreach (var endpoint in endpoints)
        {
            var config = endpoint.Value;
            var port = config.Port ?? (config.Scheme == "https" ? 443 : 80);

            var ipAddresses = new List<IPAddress>();
            if (config.Host == "localhost")
            {
                ipAddresses.Add(IPAddress.IPv6Loopback);
                ipAddresses.Add(IPAddress.Loopback);
            }
            else if (IPAddress.TryParse(config.Host, out var address))
            {
                ipAddresses.Add(address);
            }
            else
            {
                ipAddresses.Add(IPAddress.IPv6Any);
            }

            foreach (var address in ipAddresses)
            {
                options.Listen(address, port,
                    listenOptions =>
                    {
                        if (config.Scheme == "https")
                        {
                            var certificate = LoadCertificate(config, environment);
                            listenOptions.UseHttps(certificate);
                        }
                    });
            }
        }
    }

    private static X509Certificate2 LoadCertificate(EndpointConfiguration config, IHostingEnvironment environment)
    {
        if (config.StoreName != null && config.StoreLocation != null)
        {
            using (var store = new X509Store(config.StoreName, Enum.Parse<StoreLocation>(config.StoreLocation)))
            {
                store.Open(OpenFlags.ReadOnly);
                var certificate = store.Certificates.Find(
                    X509FindType.FindBySubjectName,
                    config.Host,
                    validOnly: !environment.IsDevelopment());

                if (certificate.Count == 0)
                {
                    throw new InvalidOperationException($"Certificate not found for {config.Host}.");
                }

                return certificate[0];
            }
        }

        if (config.FilePath != null && config.Password != null)
        {
            return new X509Certificate2(config.FilePath, config.Password);
        }

        throw new InvalidOperationException("No valid certificate configuration found for the current endpoint.");
    }
}

public class EndpointConfiguration
{
    public string Host { get; set; }
    public int? Port { get; set; }
    public string Scheme { get; set; }
    public string StoreName { get; set; }
    public string StoreLocation { get; set; }
    public string FilePath { get; set; }
    public string Password { get; set; }
}

Windows Sample Configuration

To configure your endpoints and HTTPS settings on Windows you could then put the following into your appsettings.Development.json, which configures an HTTPS endpoint for your application using a certificate in a certificate store:

{
    "HttpServer":{
        "Endpoints":{
            "Http":{
                "Host": "localhost",
                "Port": 8080,
                "Scheme": "http"
            },
            "Https":{
                "Host": "localhost",
                "Port": 44340,
                "Scheme": "https",
                "StoreName": "My",
                "StoreLocation": "CurrentUser"
            }
        }
    }
}

Linux and Mac OS Sample Configuration

On Linux or Mac your appsettings.Development.json would look something like this, where your certificate is specified using a file path:

{
    "HttpServer":{
        "Endpoints":{
            "Http":{
                "Host": "localhost",
                "Port": 8080,
                "Scheme": "http"
            },
            "Https":{
                "Host": "localhost",
                "Port": 44340,
                "Scheme": "https",
                "FilePath": "/path/to/certificate"
            }
        }
    }
}

You can then use the user secret manager tool, environment variables, or some secure store such as Azure KeyVault to store the password of your certificate using the HttpServer:Endpoints:Https:Password configuration key instead of storing the password in a file that goes into source control.

For example, to store the certificate password as a user secret during development, run the following command from your project:

dotnet user-secrets set HttpServer:Endpoints:Https:Password <password>

To override the certificate password using an environment variable, create an environment variable named HttpServer:Endpoints:Https:Password (or HttpServer__Endpoints__Https__Password if your system does not allow :) with the value of the certificate password.

Run your application

firefox_ubuntu_https

When running from Visual Studio you can change the default launch URL for your application to use the HTTPS address by modifying the launchSettings.json file:

{
  "iisSettings": {
    "windowsAuthentication": false,
    "anonymousAuthentication": true,
    "iisExpress": {
      "applicationUrl": "http://localhost:2613/",
      "sslPort": 44335
    }
  },
  "profiles": {
    "IIS Express": {
      "commandName": "IISExpress",
      "launchBrowser": true,
      "launchUrl": "https://localhost:44335/",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    },
    "KestrelHttpsTest": {
      "commandName": "Project",
      "launchBrowser": true,
      "launchUrl": "https://localhost:44340",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      },
      "applicationUrl": "http://localhost:8080/"
    }
  }
}

Redirect from HTTP to HTTPS

When you setup your site to use HTTPS by default, you typically want to allow HTTP requests, but have them redirected to the corresponding HTTPS address. In ASP.NET Core this can be accomplished using the URL rewrite middleware.

var httpsSection = Configuration.GetSection("HttpServer:Endpoints:Https");
if (httpsSection.Exists())
{
    var httpsEndpoint = new EndpointConfiguration();
    httpsSection.Bind(httpsEndpoint);
    app.UseRewriter(new RewriteOptions().AddRedirectToHttps(
        statusCode: env.IsDevelopment() ? StatusCodes.Status302Found : StatusCodes.Status301MovedPermanently,
        sslPort: httpsEndpoint.Port));
}

Conclusion

With a little bit of work you can setup your ASP.NET Core 2.0 site to always use HTTPS. In our next release we are working to simplify setting up HTTPS for ASP.NET Core apps and we plan to enable HTTPS in the project templates by default. We will share more details on these improvements as they become publicly available.