Configuring HTTPS in ASP.NET Core across different platforms

Daniel Roth

Daniel

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.

9 comments

Comments are closed. Login to edit/delete your existing comments

  • Avatar
    Michael Crook

    Hey DanielThe Linux section is missing the https.config config file info! Could you get that added, would love to develop with https on Pop_OS! (Ubuntu)

    • Avatar
      AustEcon SV

      Did you manage to get it working? I am also using Pop_OS (Ubuntu)… I am using .net core 3.0 and spent an entire day (8 hours) trying to just get a basic template mvc or any kind of basic website working with https – no success!

      It really shouldn’t be this hard… Please please let me know if you have some template code to copy-paste to make this work!

  • Avatar
    Sérgio Damasceno

    Amazing post! Has it already exist a MS way (easy) to do it?

  • Avatar
    john holton

    Hi  Good article. I tried implementing you system, but when it tries to execute this line
    var endpoints = configuration.GetSection("HttpServer:Endpoints")it doesn’t find anything. suspect it doesn’t find appsettings.Development.json. Am i missing something?

      • Avatar
        john holton

        Hi Thanks for your quick response. In the meantime I did manage to that file to load, but still have an issue sending data to the site (it works in postman) but not from a xamarin forms app. I’ve tried implementing this “ServicePointManager.ServerCertificateValidationCallback += (sender, certificate, chain, sslPolicyErrors) =>{if (certificate.Issuer.Equals(“CN=localhost”))return true;return sslPolicyErrors == System.Net.Security.SslPolicyErrors.None;};” but is doesn’t even get called if I call localhost:8011 where 8011 is the https port that I’ve defined. however if I call https://google.com it does. The thing I find so frustrating about .netcore is that there are so many configuration options and if it fails no errors seem to be generated

        • Daniel Roth
          Daniel RothMicrosoft employee

          A Xamarin app typically runs in an device emulator that acts like a different machine from the host machine running the emulator. “Localhost” in the Xamarin will therefore refer to the emulated device, not the host machine running the ASP.NET Core app. You typically need to configure the emulator to allow for connections from the Xamarin app to the host machine. You’ll need to check the Xamarin docs for how to set this up.

  • Avatar
    Andrew Jonkers

    Thank you for a short concise practical summary of how to make this all work.
    One historical problem I have with the Microsoft way is there are usually 30 ways to achieve the same thing and it is not always clear which path to take, which is current, which is NOW best practice and so on. So thanks!