Loading custom SSL certificates into Windows Containers


As part of securing development processes and communications internally, it is common for companies and teams to enforce the usage of custom SSL certificates with the enterprise acting as CA root. This is great, because it helps simulate better the production systems within the development stages. But sometimes, this can lead to some challenges with our tooling of choice; for example, when using Windows containers that must connect against internal URL that are running using https with company certificates.

I ran into this situation at work recently where I was trying to run a system tests suite that connects to an internal API system using https. A base windosservercore image only ships with the default bundle of certificates provided by Microsoft, and it can’t, as expected, automatically load certificates from the host machine where the container is running in. In Linux environment, this is usually easier to solve, because you can just mount the certificates as a volume into the container. Unfortunately, the same solution does not apply for Windows, at least not to this point, since there is no way to mount paths under the Certificate Provider from the host into the container.

What can we do then? Well, after some googling and reading, I found out / realize that I could export the SSL certificates from the host as .cer files and then import them either as part of the docker build process. Sounds good? Let’s explore then this approach in more detail below, using an imaginary company named Fabrikam as example for our custom certificate. Otherwise if you just want the final code, just head to the example repo.

Self signed certificate for Fabrikam

First thing we need is to have some certificates issued by our imaginary Fabrikam company into our host machine. For that, we can use self signed certificates using the New-SelfSignedCertificate cmdlet.

$fabrikamCerts = Get-ChildItem -Path cert: -Recurse | Where-Object {
    $_ -is [System.Security.Cryptography.X509Certificates.X509Certificate2] -and `
    $_.Issuer -like '*fabrikam*'
}

if (-not $fabrikamCerts) {
    New-SelfSignedCertificate -DnsName "www.fabrikam.com", "www.contoso.com" `
    -CertStoreLocation "cert:\LocalMachine\My"
}

To avoid polluting our host with multiple instances of these self signed certificates, as part of the code we check whether there are Fabrikam certificates already present in the Certificate store before creating a new one.

Extract SSL certificates into certs

The first step is to produce an exporter script capable of finding the desired certificates in the host and exporting them as files using the Cert format, so they can be loaded into the container later on. One approach could be similar to what is shown below.

# exporter.ps1

$certificatesPath = Join-Path -Path $PSScriptRoot -ChildPath 'Certs'
New-Item -Path $certificatesPath -ItemType Directory -Force | Out-Null
Get-ChildItem -Path $certificatesPath | Remove-Item -Force

Get-ChildItem -Path cert: -Recurse | Where-Object {
        $_ -is [System.Security.Cryptography.X509Certificates.X509Certificate2] -and `
        $_.Issuer -like '*fabrikam*'
    } | ForEach-Object {
        $certPath = Join-Path $certificatesPath -ChildPath "cert_$($_.Thumbprint).cer"
        Export-Certificate -Cert $_ -FilePath $certPath -Force | Out-Null
}

Let’s break down the essential steps in the script:

  1. Get all items starting from the root path of the Certificate Provider.
  2. Filter the items to only process actual certificates using the X509Certificate2 class and that match your expected issuer (Fabrikam in this example).
  3. Save the certificates to files using Export-Certificate cmdlet.

By always cleaning the Certs folder before exporting the certificates, we can be sure that we are getting the most up-to-date versions of the certificate to be imported into the container.

Import SSL certificates from files

Now that we have the certificates stored in files, we need to be able to load them into the container. To accomplish that, the following script could be useful.

# importer.ps1

$CertsPath = 'Your path here'
Get-ChildItem -Path $CertsPath -Filter *.cer | ForEach-Object {
    Import-Certificate -FilePath $_.FullName -CertStoreLocation Cert:\LocalMachine\Root\
}

Let’s break down what is going on in this script:

  1. Get all the certificates files from our Certs folder.
  2. Load them into a Certificate Store, in this case LocalMachine store, using the Import-Certificate cmdlet.

Dockerfile

Since this post is about running on Windows containers :), let’s show a simple Dockerfile definition that we can use to convey our approach.

FROM mcr.microsoft.com/windows/servercore

SHELL ["powershell", "-Command", "$ErrorActionPreference = 'Stop'; $ProgressPreference = 'SilentlyContinue';"]

WORKDIR C:/example

COPY . .

RUN ./importer.ps1

Let’s highlight some key points in the dockerfile:

  1. We are using servercore as our base image
  2. The COPY layer brings into the image the scripts previously mentioned, as well as the certificates already exported to file.
  3. The RUN layer imports the certificates into the image.

Tying everything with a build.ps1

Now that we have all the moving parts to address our initial problem, we don’t want to be manually calling each of the pieces separately each time we need to add/update our certificates. For that, a simple build.ps1 script that integrate all the scripts is usually nice.

# generate self signed certificate if needed
& ./generateSelfCertificate.ps1

# exporting the Certificates
& ./exporter.ps1

# build docker image
docker build -t certs-example $PSScriptRoot

To run the build script, just issue ./build.ps1 at the root of the location where the script resides.

Limitations

The solution described here has a couple of restrictions to account for:

  1. As of now it cannot be use with PowerShell Core, since the *-Certificate cmdlets are not available yet. That is supposed to change once PowerShell 7 comes out.
  2. We are using servercore image as base, which has Windows PowerShell baked in. nanoserver images, on the contrary, ship with PowerShell Core, so it will require some initial work to install/enable Windows Powershell to use the *-Certificate cmdlets.

Conclusion

This was a fun exercise! And it was a good challenge to come up with this solution since it helped me address a temporary blocker in some changes that we were doing in our build pipeline process at work.

To conclude, thank you so much for reading this post. Hope you liked reading it as much as I did writing it. See you soon and stay tuned for more!!

Disclaimer: The opinions expressed herein are my own personal opinions and do not represent my employer’s view in any way.