dockerasp.net-coresslssl-certificatedocker-container

How to configure SSL certificate in Docker container


How to configure a SSL certificate for ASP.NET Core in a Docker container?

I have a Visual Studio solution with multiple projects that I run on Docker Desktop; this is a "sandbox" environment for the purpose of evaluating code. This is not and never will be a "production" environment. The intent is to keep the configuration as simple as possible so I can focus on "real" work (FWIW at some point I will need to redo this for production... but that is a different problem for another time).

The basic Visual Studio Docker container has a functioning SSL certificate: eg open https://localhost:10433 in a web browser, the web browser reports the connection as secure with a valid certificate.

Running two docker containers

with container service02 accessing service01 over https.

The problem:

from host
> ping service01
pinging service01.local [172.26.13.76]
reply from 172.26.13.76 
etc

from service02
> ping service01
pinging service01.local [172.26.13.76]
reply from 172.26.13.76 
etc

from service02
> curl https://service01.local
curl: (60) schannel: SEC_E_UNTRUSTED_ROOT (0x80090325) - The certificate chain was issued by an authority that is not trusted.

This all makes sense:

Basically, I need to get a new self signed certificate with CN=service01.local onto the service01 container.

I assume I need to do the following:

As I noted: this is for a "sandbox" non-production environment, so something quick and dirty is fine (ie I want to avoid compose, kubernetics, reverse proxy, lets encrypt approaches; those tools just add another layer of complexity that I don't want to deal with right now).

Some of the relevant stackoverflow posts that I have reviewed so far

UPDATE 1: one of the SO posts uses a PowerShell script to create and register a self-signed cert. Which looks great, except the base ASP.NET Core images don't include PowerShell from what I can tell. Installing PowerShell image over ASP.NET Core image is rabbit hole I don't need to go down.

UPDATE 2: using a HttpClientHandler to ignore SSL errors works for the HttpClient, but didn't work for IdentityServer (token invalid error because authority must use https). Basically, Identity Server barfs if SSL / HTTPS connection is not valid. So really need a proper "certificate" for the container.

Reviewing the Microsoft doc on config https on docker, was not useful. basically confirmed that is how Visual Studio is configuring the dockerfile to run OOB. I have a valid dev-cert and it is being loaded to the container. I can confirm this by accessing the container with https://localhost:10433; the url is flagged as secure with the correct dev-cert. if I access the container with https://service01.local instead, the connection is NOT secure as "the certificate for this site is not valid".

A "single" docker container does handle https properly using the localhost port mapping (eg https://localhost:10433); it does not handle https property using the container hostname (eg https://service01.local). Using "multiple" docker containers is just an exasperates the problem.

UPDATE 3: I gave up on the docker approach and went back to testing using https server (Kestrel). I switched all URL references to use the https://localhost:portnbr instead of https://containername. At which point all of the SSL functionality worked, as expected.

Basically, this means I cant use docker as part of the dev process, except for the most trivial of applications. But I have learned a lot more about the ins and outs of using docker.

UPDATE 4: this error is off topic for this post, but... the question was asked.

Error communicating from secure web api (service02) to identity service (service01) is really, really weird. this is the code for the webapplication build.

builder.Services.AddAuthentication("Bearer")
    .AddIdentityServerAuthentication("Bearer", options =>
    {
        options.ApiName = "myApi";

// FAIL: using container hostname
        //options.Authority = "https://service01.local";      
        
// FAIL: HTTPS service option
        //options.Authority = "https://localhost:52433";        

// PASS: HTTPS service with http access
// NOTE: if using http then must turn off require https
        options.Authority = "http://localhost:52080";           
        options.RequireHttpsMetadata = false;                   
// NOTE: turn off require https metadata for testing only
    });
builder.Services.AddAuthorization();

now for the weird part. when I call https://localhost:52443/.well-known/openid-configuration from another app (service03) the https connection works. so why does setting the identity server Authority to https fail with "connection actively refused", yet using the same url from a web browser or a different service app or switching to http works?

I ran out of coffee trying to figure this one out. I really gave up, once I read the commercial licensing costs for the supported version Identity Server. Chalk this up as a good learning exercise.

UPDATE 5: creating self signed certs...

As per qiang-fu answer below, I need to create the appropriate certs. here is the PowerShell script I am using to do so...

# Generate Certificate using OpenSSL
# Assume Git is installed, which includes OpenSSL client

write-host "STARTING GEN CERTIFICATE"

$outputFile = (Get-Item .).FullName
write-host "Working Directory: " $outputFile

# NOTE: single '' is used by powershell, versus "" used by OS
$openssl = 'C:\Program Files\Git\usr\bin\openssl.exe'
write-host "OpenSSL Executable: " $openssl

# UPDATE THIS: step options: 1-7, CA, SR, ALL
$step = 7

# UPDATE THIS: hostname to generate certificate for
$hostname = "service02"

# local variables
$caKey = "ca.key"
$caCert = "ca.crt"
$serverKey = "$hostname.key"
$serverCsr = "$hostname.csr"
$serverCert = "$hostname.crt"
$serverPfx = "$hostname.pfx"

# UPDATE THIS: certificate details
$certSubj = '"/C=CA/ST=BritishColumbia/L=Vancouver/O=MyCo/OU=MyCo/CN=MyCo.local"'
$certExt = '"subjectAltName=DNS:myco.local"'

$serverSubj = "/C=CA/ST=BritishColumbia/L=Vancouver/O=MyCo/OU=MyCo/CN=$hostname.local"
$serverExt = "subjectAltName=DNS:$hostname.local"

# show settings
if (($step -eq 0) -or ($step -eq "CA") -or ($step -eq "SR") -or ($step -eq "ALL"))
{
    write-host "Settings..."
    write-host "step $step"
    write-host $hostname
    write-host $caKey $caCert
    write-host $serverKey $serverCsr $serverCert
    write-host $serverExt
    
# TEST command will execute
    #& $openssl
    #& $openssl help
}

# Generate CA private key
if (($step -eq 1) -or ($step -eq "CA") -or ($step -eq "ALL"))
{
    & $openssl genrsa -out $caKey 4096
}

# Generate CA certificate
if (($step -eq 2) -or ($step -eq "CA") -or ($step -eq "ALL"))
{
    & $openssl req -x509 -new -nodes -days 365 -sha256 -key $caKey -out $caCert -subj "$certSubj" -addext "$certExt"
}

# Generate CSR for CA
if (($step -eq 3) -or ($step -eq "CA") -or ($step -eq "ALL"))
{
    #& $openssl req -newkey rsa:4096 -keyout $caprivatekey -out cert.csr -sha256 -days 365 -nodes -subj "$certSubj" -addext "$certExt"
}

# Generate Server Private Key
if (($step -eq 4) -or ($step -eq "SR") -or ($step -eq "ALL"))
{
    & $openssl genrsa -out $serverKey 4096
}

# Generate Server CSR
if (($step -eq 5) -or ($step -eq "SR") -or ($step -eq "ALL"))
{
    & $openssl req -new -key $serverKey -out $serverCsr -sha256 -subj "$serverSubj" -addext "$serverExt"
}

# Generate Server SSL Certificate along with certificates serial
if (($step -eq 6) -or ($step -eq "SR") -or ($step -eq "ALL"))
{
    & $openssl req -x509 -days 365 -sha256 -CA $caCert -CAkey $caKey -in $serverCsr -out $serverCert -addext $serverExt -copy_extensions copyall
}

# Generate Server PFX certificate
if (($step -eq 7) -or ($step -eq "SR") -or ($step -eq "ALL"))
{
    write-host "Generating PFX certificate: you will be prompted to enter a password and confirm it"
    & $openssl pkcs12 -export -in $serverCert -inkey $serverKey -out $serverPfx
    # -certfile root?
}

Running through this script I now have

the launchSettings.json for docker is now

    "Docker": {
      "commandName": "Docker",
      "launchBrowser": true,
      "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}/",
      "environmentVariables": {
        "ASPNETCORE_URLS": "https://+:443;http://+:80",
        "ASPNETCORE_Kestrel__Certificates__Default__Password": "password",
        "ASPNETCORE_Kestrel__Certificates__Default__Path": "/app/service01.pfx"
      },
      "publishAllPorts": true,
      "useSSL": true,
      "httpPort": 52080,
      "sslPort": 52443,
      "commandLineArgs": "",
      "DockerfileRunArguments": "-h service01"

    }

At this point, service01 is running with the self signed certificate with the root CA included.

so now in web browser https://service01.local comes up with the correct self signed certificate. checking the certificate shows this is valid. same for all three service01.local, service02.local, service03.local.

UPDATE 6: still need a way to load the ca cert into the windows container as trusted CA. (lots of linux examples that work, but not windows examples). note that the container does not have PowerShell or certutil or openssl installed.


Solution

  • Seen this problem days before. You could bypass the certificate validation by config the httpclienthandler when using httpclient in service02.

    var httpClientHandler = new HttpClientHandler();
    httpClientHandler.ServerCertificateCustomValidationCallback = (message, cert, chain, sslPolicyErrors) =>{return true;};
    builder.Services.AddScoped(sp => new HttpClient(httpClientHandler));
    

    You can also add some validtion if you want

    httpClientHandler.ServerCertificateCustomValidationCallback = (message, cert, chain, sslPolicyErrors) => {
        if (cert.GetCertHashString() == "A7E2BA992F4F2BE220559B8A5371F05A00A6E750") //FingerPrint of the certificate
        {
            return true;
        }
        return false;
    };
    

    By the way, Below is my working command to run a dockerfile with certificate(.pfx) docker run -it -p 22007:22007 -e ASPNETCORE_URLS="https://+:22007" -e ASPNETCORE_HTTPS_PORT=22007 -e ASPNETCORE_Kestrel__Certificates__Default__Password="123" -e ASPNETCORE_Kestrel__Certificates__Default__Path=/https/aspnetapp.pfx -v ${HOME}/.aspnet/https:/https/ myimagename
    Reference build container with https

    ----------------update of install cert----------
    Steps of using openssl to create server certificate (pfx):
    1.generate ca.key
    2.generate ca.csr from ca.key
    3.generate ca.crt from ca.csr + ca.key
    4.generate server.key
    5.generate server.csr from server.key
    6.generate server.crt from server.csr + ca.crt + ca.key
    7.generate server.pfx from server.crt + server.key

    In step 6,you will be ask the Common Name(CN), you have to fill in the domain of server01 (or ip, depends on what API URL you are calling in service02).If not same the certificate will be invalid.

    Then use the step7 pfx when run service01 docker to host https.
    My docker file

    FROM mcr.microsoft.com/dotnet/aspnet:6.0   
    COPY / /app/    
    WORKDIR /app    
    EXPOSE 22007/tcp   
    ENV ASPNETCORE_URLS=http://+:22007
    ENTRYPOINT ["dotnet", "/app/dockerBack.dll","--server.urls","http://*/22007"]
    

    My working command (I put the server.pfx in linux home/username/app1/composetest folder):
    docker run -it -p 22007:22007 -e ASPNETCORE_URLS="https://+:22007" -e ASPNETCORE_HTTPS_PORT=22007 -e ASPNETCORE_Kestrel__Certificates__Default__Password="123" -e ASPNETCORE_Kestrel__Certificates__Default__Path="/composetest/server.pfx" -v /home/lighthouse/app1/composetest:/composetest imagename

    Then in service02, you could install the certifiate by adding "copy" "run" at begining in dockerfile. (I put the ca.crt in the same folder of docker file)

    FROM mcr.microsoft.com/dotnet/aspnet:6.0 
    COPY ca.crt /usr/local/share/ca-certificates/ca.crt
    RUN chmod 644 /usr/local/share/ca-certificates/ca.crt && update-ca-certificates
    COPY / /app/    
    WORKDIR /app  
    EXPOSE 22008/tcp
    ENV ASPNETCORE_ENVIRONMENT=Development
    ENV ASPNETCORE_URLS=http://+:22008
    ENTRYPOINT ["dotnet", "/app/docker-front.dll","--server.urls","http://*/22008"]
    

    Then there will be no SSL error in my fetching https api test.(Needn't bypass validation)