Using pure powershell to generate TLS certificates for Docker daemon running on Windows

Steps below will allow you to create necessary PKI infrastructure to secure you docker daemons with no requirement to download any external tools. Snippets below are not part of complete script but tidbits which you can use to procure both CA, server certificate as well as client certificate and reuse pieces for issuing additional certificates down the road.

Docker daemon requires 3 files on server to for secure TLS connection:

  • tlscacert which is Base64 encoded public key of CA certificate
  • tlscert which is Base64 encoded public key of server certificate
  • tlskey which Base64 encoded private key of server certificate

Docker daemon will list those keys in file called daemon.json under  $env:programdata\docker\config

Example of that file is below

{
"group": "Network Service",
"graph": "E:\\images",
"tlscacert": "C:\\ProgramData\\docker\\certs.d\\rootCA.cer",
"tlskey": "C:\\ProgramData\\docker\\certs.d\\privateKey.cer",
"hosts": [
"tcp://0.0.0.0:2376",
"npipe://"
],
"tlscert": "C:\\ProgramData\\docker\\certs.d\\serverCert.cer",
"tlsverify": true
}

Similar files will be required on client to connect to server, difference is certificate which will be used to connect which will have different EKU (Enhanced Key Usage) specified

  • tlscacert which is Base64 encoded public key of CA certificate
  • tlscert which is Base64 encoded public key of client certificate
  • tlskey which is Base64 encoded private key of client certificate
Docker client will use syntax below to connect to TLS secured docker endpoint
& docker --tlsverify --tlscacert=c:\test\rootca.cer --tlscert=c:\test\clientPublicKey.cer --tlskey=c:\test\clientPrivateKey.cer -H=tcp://containerhost1:2376 version

Creating CA certificate

Snippet below creates CA certificate and exports it’s public key to c:\test\rootCA.cer. Private key stays in your Windows Certificate Store and is exportable for your backup purpouses and reissuing new server and client certificates later. The only changeable parameter which you can modify for your environment is Subject.

        $splat = @{
        type = "Custom" ;
        KeyExportPolicy = "Exportable";
        Subject = "CN=Docker TLS Root";
        CertStoreLocation = "Cert:\CurrentUser\My";
        HashAlgorithm = "sha256";
        KeyLength = 4096;
        KeyUsage = @("CertSign", "CRLSign");
        TextExtension = @("2.5.29.19 ={critical} {text}ca=1")
    }
    $rootCert = New-SelfSignedCertificate @splat

After CA certificate is generated we need to export it’s public key to a file, the only changeable part here is Path

 $splat = @{
Path = "c:\test\rootCA.cer";
        Value = "-----BEGIN CERTIFICATE-----`n" + [System.Convert]::ToBase64String($rootCert.RawData, [System.Base64FormattingOptions]::InsertLineBreaks) + "`n-----END CERTIFICATE-----";
        Encoding = "ASCII";
    }
    Set-Content @splat

Creating Server Certificate to secure TLS on container host

Code similar to generation of CA certificate with few notable changes, that is we provide which certificate is used to sign it as well as type of certificate, we export key after cert is generated. Changeable parameters are DNSName and Path

    $splat = @{
        CertStoreLocation = "Cert:\CurrentUser\My";
        DnsName = "swarmmanager1", "localhost", "containerhost1";
        Signer = $rootCert ;
        KeyExportPolicy = "Exportable";
        Provider = "Microsoft Enhanced Cryptographic Provider v1.0";
        Type = "SSLServerAuthentication";
        HashAlgorithm = "sha256";
        TextExtension = @("2.5.29.37= {text}1.3.6.1.5.5.7.3.1");
        KeyLength = 4096;
    }
    $serverCert = New-SelfSignedCertificate @splat
    $splat = @{
       Path = "c:\test\serverCert.cer";
       Value = "-----BEGIN CERTIFICATE-----`n" + [System.Convert]::ToBase64String($serverCert.RawData, [System.Base64FormattingOptions]::InsertLineBreaks) + "`n-----END CERTIFICATE-----";
       Encoding = "Ascii"
}
   Set-Content @splat

Exporting private key for server certificate to a file

Last step for TLS connectivity for docker host to export private key to Base64 encoded file which has been pretty difficult with off the shelf powershell/.NET framework untill version 4.6 which provided method to export that key. Implementation is below

    $privateKeyFromCert = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($serverCert)
    $splat = @{
        Path = "c:\test\privateKey.cer";
        Value = ("-----BEGIN RSA PRIVATE KEY-----`n" + [System.Convert]::ToBase64String($privateKeyFromCert.Key.Export([System.Security.Cryptography.CngKeyBlobFormat]::Pkcs8PrivateBlob), [System.Base64FormattingOptions]::InsertLineBreaks) + "`n-----END RSA PRIVATE KEY-----");
        Encoding = "Ascii";
    }
    Set-Content @splat

Creating client certificate

Code below performs similar tasks in relevant to client certificate like server certificate tasks above

    $splat = @{
        CertStoreLocation = "Cert:\CurrentUser\My";
        Subject = "CN=clientCert";
        Signer = $rootCert ;
        KeyExportPolicy = "Exportable";
        Provider = "Microsoft Enhanced Cryptographic Provider v1.0";
        TextExtension = @("2.5.29.37= {text}1.3.6.1.5.5.7.3.2") ;
        HashAlgorithm = "sha256";
        KeyLength = 4096;
    }
    $clientCert = New-SelfSignedCertificate  @splat
    $splat = @{
        Path = "c:\test\clientPublicKey.cer" ;
        Value = ("-----BEGIN CERTIFICATE-----`n" + [System.Convert]::ToBase64String($clientCert.RawData, [System.Base64FormattingOptions]::InsertLineBreaks) + "`n-----END CERTIFICATE-----");
        Encoding = "Ascii";
    }
    Set-Content  @splat
    $clientprivateKeyFromCert = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($clientCert)
    $splat = @{
        Path = "c:\test\clientPrivateKey.cer";
        Value = ("-----BEGIN RSA PRIVATE KEY-----`n" + [System.Convert]::ToBase64String($clientprivateKeyFromCert.Key.Export([System.Security.Cryptography.CngKeyBlobFormat]::Pkcs8PrivateBlob), [System.Base64FormattingOptions]::InsertLineBreaks) + "`n-----END RSA PRIVATE KEY-----");
        Encoding = "Ascii";
    }
    Set-Content  @splat

If everything worked correctly you are supposed to see 3 certificates on your machine listed below, which are CA certificate, server certificate and client certificate, you also shall have private key for each of those (indicated by key image).

mmc_2017-06-10_16-10-12

You shall also have 5 files created in c:\test folder like below

 Directory: C:\test

Mode                LastWriteTime         Length Name
----                -------------         ------ ----
-a----        6/10/2017   4:09 PM           3314 clientPrivateKey.cer
-a----        6/10/2017   4:09 PM           1858 clientPublicKey.cer
-a----        6/10/2017   4:09 PM           3310 privateKey.cer
-a----        6/10/2017   4:09 PM           1812 rootCA.cer
-a----        6/10/2017   4:09 PM           1936 serverCert.cer

Add your CA root certificate to your trusted root certificate authorities in certmgr.msc. Make sure you copy and paste and not move CA root certificate since if you move you will not be able to sign any more keys. This is not not a requirement but will allow you to use native Windows tools in working with certificates instead of relying on file based store like openSSL does. For example you will be able to use HTTPS to call docker REST API both in browser and via Invoke-WebRequest

Deploy server certificate to docker container host

  • Open file named daemon.json under  $env:programdata\docker\config and paste lines in snippet below into it.
    "tlscacert":  "C:\\ProgramData\\docker\\certs.d\\rootCA.cer",
    "tlskey":  "C:\\ProgramData\\docker\\certs.d\\privateKey.cer",
    "hosts":  [
                  "tcp://0.0.0.0:2376",
                  "npipe://"
              ],
    "tlscert":  "C:\\ProgramData\\docker\\certs.d\\serverCert.cer",
    "tlsverify":  true
  • Copy files rootCA.cer, privateKey.cer, serverCert.cer to $env:programdata\docker\certs.d
  • Restart docker service Restart-Service docker

At this point you shall not be able to connect to daemon via HTTPS without providing a valid certificate. Try following  Invoke-WebRequest https://containerhost1:2376, it shall fail complaining that SSL client certificate is required for connection

The request was aborted: Could not create SSL/TLS secure channel.
At line:1 char:1
+ Invoke-WebRequest https://containerhost1:2376
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidOperation: (System.Net.HttpWebRequest:HttpWebRequest) [Invoke-WebRequest], WebException
+ FullyQualifiedErrorId : WebCmdletWebResponseException,Microsoft.PowerShell.Commands.InvokeWebRequestCommand

Attach client certificate to request by first finding thumbprint of your client cert in certificates manager and then use tab completion to iterate to it in get-item cert:\CurrentUser\My\, assign it to variable which you will attach to REST request $cert = get-item Cert:\CurrentUser\My\350A62B64152D9B85673E902A1F1C2CB6766598E

Issue the same request as above and it will succeed returning information about available images on remote system

PS >(Invoke-WebRequest https://containerhost1:2376/images/json -Certificate $cert -UseBasicParsing).Content | convertfrom-json

Containers : -1
Created : 1494389426
Id : sha256:242b8694ed621610a27746e0075c95e87f1a239e1800a4ea55e753010a49d9d5
Labels :
ParentId :
RepoDigests : {stefanscherer/dockertls-windows@sha256:5fe358a57cb31f18d2d148b0481898d530a5547c4d5d6f9ce5e0334ed8d3de19}
RepoTags : {stefanscherer/dockertls-windows:latest}
SharedSize : -1
Size : 1049291645
VirtualSize : 1049291645

One last thing is to try to use docker CLI to query the same information.

PS C:\admin> docker --tlsverify --tlscacert=c:\test\rootca.cer --tlscert=c:\test\clientPublicKey.cer --tlskey=c:\test\clientPrivateKey.c
er -H=tcp://containerhost1:2376 images

time="2017-06-10T16:46:14-05:00" level=info msg="Unable to use system certificate pool: crypto/x509: system root pool is not available on Windows"
REPOSITORY                        TAG                 IMAGE ID            CREATED             SIZE
stefanscherer/dockertls-windows   latest              242b8694ed62        4 weeks ago         1.05 GB

Full script is below

$ErrorActionPreference = "Stop"
if ([int](Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full"  -Name Release).Release -lt 393295) {
    throw "Your version of .NET framework is not supported for this script, needs at least 4.6+"
}
function GenerateCerts {
    $splat = @{
        type = "Custom" ;
        KeyExportPolicy = "Exportable";
        Subject = "CN=Docker TLS Root";
        CertStoreLocation = "Cert:\CurrentUser\My";
        HashAlgorithm = "sha256";
        KeyLength = 4096;
        KeyUsage = @("CertSign", "CRLSign");
        TextExtension = @("2.5.29.19 ={critical} {text}ca=1")
    }
    $rootCert = New-SelfSignedCertificate @splat
    $splat = @{
        Path = "c:\test\rootCA.cer";
        Value = "-----BEGIN CERTIFICATE-----`n" + [System.Convert]::ToBase64String($rootCert.RawData, [System.Base64FormattingOptions]::InsertLineBreaks) + "`n-----END CERTIFICATE-----";
        Encoding = "ASCII";
    }
    Set-Content @splat
    $splat = @{
        CertStoreLocation = "Cert:\CurrentUser\My";
        DnsName = "swarmmanager1", "localhost", "containerhost1";
        Signer = $rootCert ;
        KeyExportPolicy = "Exportable";
        Provider = "Microsoft Enhanced Cryptographic Provider v1.0";
        Type = "SSLServerAuthentication";
        HashAlgorithm = "sha256";
        TextExtension = @("2.5.29.37= {text}1.3.6.1.5.5.7.3.1");
        KeyLength = 4096;
    }
    $serverCert = New-SelfSignedCertificate @splat
    $splat = @{
        Path = "c:\test\serverCert.cer";
        Value = "-----BEGIN CERTIFICATE-----`n" + [System.Convert]::ToBase64String($serverCert.RawData, [System.Base64FormattingOptions]::InsertLineBreaks) + "`n-----END CERTIFICATE-----";
        Encoding = "Ascii"
    }
    Set-Content @splat

    $privateKeyFromCert = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($serverCert)
    $splat = @{
        Path = "c:\test\privateKey.cer";
        Value = ("-----BEGIN RSA PRIVATE KEY-----`n" + [System.Convert]::ToBase64String($privateKeyFromCert.Key.Export([System.Security.Cryptography.CngKeyBlobFormat]::Pkcs8PrivateBlob), [System.Base64FormattingOptions]::InsertLineBreaks) + "`n-----END RSA PRIVATE KEY-----");
        Encoding = "Ascii";
    }
    Set-Content @splat

    $splat = @{
        CertStoreLocation = "Cert:\CurrentUser\My";
        Subject = "CN=clientCert";
        Signer = $rootCert ;
        KeyExportPolicy = "Exportable";
        Provider = "Microsoft Enhanced Cryptographic Provider v1.0";
        TextExtension = @("2.5.29.37= {text}1.3.6.1.5.5.7.3.2") ;
        HashAlgorithm = "sha256";
        KeyLength = 4096;
    }
    $clientCert = New-SelfSignedCertificate  @splat
    $splat = @{
        Path = "c:\test\clientPublicKey.cer" ;
        Value = ("-----BEGIN CERTIFICATE-----`n" + [System.Convert]::ToBase64String($clientCert.RawData, [System.Base64FormattingOptions]::InsertLineBreaks) + "`n-----END CERTIFICATE-----");
        Encoding = "Ascii";
    }
    Set-Content  @splat
    $clientprivateKeyFromCert = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($clientCert)
    $splat = @{
        Path = "c:\test\clientPrivateKey.cer";
        Value = ("-----BEGIN RSA PRIVATE KEY-----`n" + [System.Convert]::ToBase64String($clientprivateKeyFromCert.Key.Export([System.Security.Cryptography.CngKeyBlobFormat]::Pkcs8PrivateBlob), [System.Base64FormattingOptions]::InsertLineBreaks) + "`n-----END RSA PRIVATE KEY-----");
        Encoding = "Ascii";
    }
    Set-Content  @splat
}
GenerateCerts

& docker --tlsverify --tlscacert=c:\test\rootca.cer --tlscert=c:\test\clientPublicKey.cer --tlskey=c:\test\clientPrivateKey.cer -H=tcp://containerhost1:2376 images

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s