HTTPS isn't optional any more. Browsers flag HTTP sites as insecure, search engines penalise them, and modern web APIs like service workers refuse to work without it. ASP.NET Core makes HTTPS straightforward in development and offers several options for production certificate management.

Development Certificates

The .NET SDK ships with a development certificate tool. If you've ever seen browser warnings about untrusted certificates on localhost, this fixes it:

dotnet dev-certs https --trust

This generates a self-signed certificate and adds it to your operating system's trusted certificate store. Kestrel picks it up automatically when you launch with HTTPS.

To check if you already have a trusted dev cert:

dotnet dev-certs https --check --trust

Enforcing HTTPS

ASP.NET Core provides middleware to redirect HTTP requests to HTTPS and to send HSTS headers:

Program.cs
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseHsts();
}

app.UseHttpsRedirection();

UseHttpsRedirection issues a 307 redirect from HTTP to HTTPS. UseHsts sends the Strict-Transport-Security header, which tells browsers to always use HTTPS for this domain.

Configure the HSTS options for production:

Program.cs
builder.Services.AddHsts(options =>
{
    options.MaxAge = TimeSpan.FromDays(365);
    options.IncludeSubDomains = true;
    options.Preload = true;
});

Setting Preload to true adds the preload directive, which allows your domain to be included in browser preload lists. Once you're on that list, browsers will never attempt an HTTP connection to your domain — even on the first visit.

Configuring Kestrel with Custom Certificates

For production deployments where you're terminating TLS at the application level, configure Kestrel directly:

Program.cs
builder.WebHost.ConfigureKestrel(serverOptions =>
{
    serverOptions.ListenAnyIP(5001, listenOptions =>
    {
        listenOptions.UseHttps(httpsOptions =>
        {
            httpsOptions.ServerCertificate = X509CertificateLoader
                .LoadPkcs12FromFile(
                    "cert.pfx",
                    builder.Configuration["Certificate:Password"]);
        });
    });
});

You can also configure certificates via appsettings.json:

appsettings.json
{
  "Kestrel": {
    "Endpoints": {
      "Https": {
        "Url": "https://*:5001",
        "Certificate": {
          "Path": "/certs/cert.pfx",
          "Password": ""
        }
      }
    }
  }
}

Loading Certificates from Azure Key Vault

For cloud deployments, storing certificates in a vault is the preferred approach:

Program.cs
var client = new CertificateClient(
    new Uri("https://my-vault.vault.azure.net/"),
    new DefaultAzureCredential());

var certificate = await client.DownloadCertificateAsync("my-cert");

builder.WebHost.ConfigureKestrel(serverOptions =>
{
    serverOptions.ListenAnyIP(443, listenOptions =>
    {
        listenOptions.UseHttps(certificate.Value);
    });
});

Certificate Rotation with SNI

For applications serving multiple domains, use Server Name Indication (SNI) to select certificates dynamically:

Program.cs
builder.WebHost.ConfigureKestrel(serverOptions =>
{
    serverOptions.ListenAnyIP(443, listenOptions =>
    {
        listenOptions.UseHttps(httpsOptions =>
        {
            httpsOptions.ServerCertificateSelector = (context, host) =>
            {
                return host switch
                {
                    "api.example.com" => LoadCertificate("api-cert"),
                    "admin.example.com" => LoadCertificate("admin-cert"),
                    _ => LoadCertificate("default-cert")
                };
            };
        });
    });
});

The ServerCertificateSelector is called on every connection, so you can implement dynamic certificate loading — pull from a cache, fetch from Key Vault, or reload from disk when the file changes.

Making Outbound HTTPS Calls

When calling external APIs, HttpClient validates server certificates by default. For development environments with self-signed certificates, you might need to customise the handler:

Program.cs
builder.Services.AddHttpClient("InternalApi", client =>
{
    client.BaseAddress = new Uri("https://internal-service:5001");
})
.ConfigurePrimaryHttpMessageHandler(() =>
{
    var handler = new HttpClientHandler();

    if (builder.Environment.IsDevelopment())
    {
        // Only bypass validation in development
        handler.ServerCertificateCustomValidationCallback =
            HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;
    }

    return handler;
});

The name DangerousAcceptAnyServerCertificateValidator is deliberate. Never use this in production. It completely disables certificate validation, making connections vulnerable to man-in-the-middle attacks.

Reverse Proxy Considerations

If you're behind a reverse proxy (Nginx, Azure App Service, AWS ALB), the proxy typically handles TLS termination. Your application receives plain HTTP but needs to know the original scheme for generating correct URLs:

Program.cs
builder.Services.Configure<ForwardedHeadersOptions>(options =>
{
    options.ForwardedHeaders =
        ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
});

app.UseForwardedHeaders();
app.UseHttpsRedirection();

Without UseForwardedHeaders, your application thinks every request is HTTP and issues endless redirects.

Wrapping Up

For most production deployments, terminate TLS at the reverse proxy or load balancer and let your application focus on business logic. When you need application-level TLS, Kestrel's certificate configuration is flexible enough to handle everything from static PFX files to dynamic Key Vault-backed certificate selection.