aspnetcore icon indicating copy to clipboard operation
aspnetcore copied to clipboard

Kestrel config behaves differently when set using env vars vs. in-code

Open qui8t opened this issue 2 years ago • 4 comments

I have a Web API application that I want to configure it to listen on an HTTPS port. The app is containerized and orchestrated using Docker compose.

I can configure it by setting the following env variable in docker-compose, which works as expected.

services:
  webapi:
    environment:
      - ASPNETCORE_URLS=https://+:443

However, when I use the following approach as an alternative to setting the environment variable, it fails with the following error message.

builder.WebHost.ConfigureKestrel(options =>
{
    options.Listen(
    address: IPAddress.Any,
        port: 443,
        configure: listenOptions =>
        {
            listenOptions.Protocols = HttpProtocols.Http1AndHttp2;
            listenOptions.UseHttps();
        });
});

Unable to configure HTTPS endpoint. No server certificate was specified, and the default developer certificate could not be found or is out of date.

When I provide the filename and password to the SSL certificate file as the following, it works as expected.

builder.WebHost.ConfigureKestrel(options =>
{
    options.Listen(
    address: IPAddress.Any,
        port: 443,
        configure: listenOptions =>
        {
            listenOptions.Protocols = HttpProtocols.Http1AndHttp2;
            listenOptions.UseHttps("/root/.aspnet/https/cert.pfx", "Password");
        });
});

I assume setting the environment variable ASPNETCORE_URLS or calling ConfigureKestrel are equivalent, though, I am not sure what I am missing in the second approach.

Update

I was previously getting the following error, which was related to mapping the port 443 to an incorrect port of the container.

Detected a TLS handshake to an endpoint that does not have TLS enabled.

qui8t avatar Dec 29 '22 16:12 qui8t

The app is containerized and orchestrated using Docker compose.

I think this explains the difference. If you try environment variables vs code outside of the container they should work the same. Inside the container there are extra tooling steps to make the dev certificate available. I don't know if that is happening when you switch to the code version.

Detected a TLS handshake to an endpoint that does not have TLS enabled.

That's a weird error, you should only get that if you left out UseHttps. Even if it couldn't load the cert, it should fail to start instead.

Tratcher avatar Dec 29 '22 23:12 Tratcher

Inside the container there are extra tooling steps to make the dev certificate available. I don't know if that is happening when you switch to the code version.

Is mounting the certs onto the container sufficient?! I guess so, because when the certificate file and its password are explicity provided, it works as expected.

volumes:
  - ${APPDATA}/Microsoft/UserSecrets:/root/.microsoft/usersecrets:ro
  - ${APPDATA}/ASP.NET/Https:/root/.aspnet/https:ro

Detected a TLS handshake to an endpoint that does not have TLS enabled.

That's a weird error, you should only get that if you left out UseHttps. Even if it couldn't load the cert, it should fail to start instead.

This was caused by an error on my part mapping the port 443 to incorrect container port. My apologies for the confusion; I updated my question.

qui8t avatar Dec 30 '22 02:12 qui8t

@NCarlsonMSFT Can you take a look at the original post in this issue and see if there's something missing from the workflow here (why does it not work when the certificate path isn't specified)?

adityamandaleeka avatar Jan 04 '23 23:01 adityamandaleeka

@qui8t for clarification, are you using the VS tools for running your compose app? If so, I believe we key off the value for ASPNETCORE_URLS to know whether to do our magic to enable HTTPS so that may be what is causing your strange behavior. If you don't want the value in your main compose file, you can specify it in docker-compose.override.yml located next your main docker-compose.yml file. As of now there is no other way to let the tools know that the service needs HTTPS support.

Alternatively you can add the volumes from your above comment, and export the dev certificate to %AppData%/ASP.NET/Https.pfx and store the password as a user secret named "Kestrel:Certificates:Development:Password". Having that user secret (or correspondingg environment variable etc.) tells kestrel to look for the file in the well known location.

NCarlsonMSFT avatar Jan 05 '23 00:01 NCarlsonMSFT

Yes, I am using VS tools.

I already have the setup in docker-compose.override.yml that mounts the directory containing the certificates onto the container. Though it seems it only works if I explicitly provide the certificate names and password.

qui8t avatar Jan 06 '23 03:01 qui8t

@adityamandaleeka I've replicated the scenario and can confirm that despite setting the same user secret and providing the same .pfx file as the compose tooling, attempting to configure HTTPs using the above method results in an error. Looking at the code it appears that the UseHttps extension method does not attempt to call FindDeveloperCertificateFile so it can't use this fall-back mechanism.

@qui8t as a work-around you can at least update what you have to also use the user secret (and only be used in development):

if (builder.Environment.IsDevelopment())
{
    listenOptions.UseHttps("/root/.aspnet/https/WebApplication1.pfx", builder.Configuration["Kestrel:Certificates:Development:Password"]);
}

I also modified my launch profile to manually use https as the scheme:

"Docker Compose": {
  "commandName": "DockerCompose",
  "commandVersion": "1.0",
  "composeLaunchAction": "LaunchBrowser",
  "composeLaunchServiceName": "webapplication1",
  "composeLaunchUrl": "https://localhost:{ServicePort}",
  "serviceActions": {
    "webapplication1": "StartDebugging"
  }
}

Bonus: The container tools won't regenerate the cert/secret for you w/o the Environment variable, but here is a script that when run in the Web project's folder will take care of doing the same steps

$password = [Guid]::NewGuid().ToString("N")
$projectName = [System.IO.Path]::GetFileNameWithoutExtension((Get-Item *.csproj)[0])

dotnet dev-certs https --trust --export-path "$Env:APPDATA\ASP.NET\Https\$projectName.pfx" -p $password
dotnet user-secrets init
dotnet user-secrets set "Kestrel:Certificates:Development:Password" "$password"

NCarlsonMSFT avatar Jan 07 '23 00:01 NCarlsonMSFT

@NCarlsonMSFT Thank you for looking into this.

qui8t avatar Jan 07 '23 00:01 qui8t

It appears that we have two pieces of code that try to load the development cert. There's the following which doesn't look at config at all.

https://github.com/dotnet/aspnetcore/blob/d2074d7181221021861de7492bc03449d5a424f2/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs#L283-L324

The above ends up being the only thing that tries to set the DefaultCertificate if KestrelConfigurationLoader.Load() never gets called, which would mean the following logic couldn't set it first:

https://github.com/dotnet/aspnetcore/blob/d2074d7181221021861de7492bc03449d5a424f2/src/Servers/Kestrel/Core/src/KestrelConfigurationLoader.cs#L399-L451

I'm surprised KestrelConfigurationLoader.Load() isn't being called here in a WebApplicationBuilder-based app:

https://github.com/dotnet/aspnetcore/blob/d2074d7181221021861de7492bc03449d5a424f2/src/Servers/Kestrel/Core/src/Internal/KestrelServerImpl.cs#L315

And `Options.ConfigurationLoader should always be set in WebApplicationBuilder-based apps by the following: https://github.com/dotnet/aspnetcore/blob/d2074d7181221021861de7492bc03449d5a424f2/src/DefaultBuilder/src/WebHost.cs#L225-L228

There are scenarios where we know KestrelServerOptions.ConfigurationLoader is not set though. I bet the logic for reading the development certificate from config was put into KestrelConfigurationLoader because that's about the only place Kestrel has access to the IConfiguration, and if it's not set, that means that the user didn't configure a "Kestrel" config section and therefore might be surprised to see something trying to read "Kestrel:Certificates:Development:Password".

It's unfortunate that our tooling tooling requires Kestrel to bind to the "Kestrel" section of config for the dev cert to work on docker. It might make sense to fix Kestrel to look there for the dev cert regardless of whether KestrelServerOptions.Configure(IConfiguration) was ever called if otherwise it would fail.

halter73 avatar Jan 09 '23 23:01 halter73

It looks like UseHttps() is probably calling KestrelServerOptions.ApplyDefaultCert() too early before KestrelConfigurationLoader.Load().

halter73 avatar Jan 09 '23 23:01 halter73

There are scenarios where we know KestrelServerOptions.ConfigurationLoader is not set though.

Such as?

Tratcher avatar Jan 09 '23 23:01 Tratcher

There are scenarios where we know KestrelServerOptions.ConfigurationLoader is not set though.

Such as?

When you use a non-default builder. So ConfigureWebHost instead of ConfigureWebHostDefaults. It can also be overridden even if you are using the defaults.

This seems related to #28120 and #26258.

halter73 avatar Jan 25 '23 21:01 halter73

Even if we don't end up reviving #46296, it's probably worth salvaging the tests.

amcasey avatar Feb 24 '23 23:02 amcasey

Possible path forward: can dotnet-monitor query for the existence of the cert(s) they care about and use the result to determine whether or not they attempt to use https?

amcasey avatar Feb 27 '23 23:02 amcasey