Add a Layer of Security to Your Docker Environment Variables Without Swarm

Recently, the vendor of a system we support at work pushed us to deploy their application on a Microsoft Windows server using Docker. Their recommendation is to create a powershell script with all of the environment variables in it and run it at startup. The script contains multiple service account credentials and a password for an X.509 certificate in plain text. We weren’t comfortable leaving things this way, but all of the research we’d done indicated we need to be using kubernetes or Docker Swarm to be able to use the “secrets” feature that was designed for this sort of thing, so we were forced to be creative.

My first thought was to encrypt the script with OpenSSL with an asymmetric key-pair, but I knew I’d either end up with a plain text private key or a plain text password for an encrypted private key because OpenSSL doesn’t support any integration with the native Microsoft certificate store. I did some searching around for methods of encrypting and decrypting with powershell commands that could leverage the cert store and landed on this article https://sid-500.com/2017/10/29/powershell-encrypt-and-decrypt-data/. The idea was to use the concepts described in this article to encrypt the script at rest, and create a wrapper script that will decrypt the script on demand and run it.

This idea is not perfect and will not prevent visibility into the “sensitive” variables from prying eyes with access to the host. The variables will still be visible if the container is “inspected” (by running ‘docker inspect’), if the container is attached to and the variables are examined, or if the variables are dumped by logs of the application. It does however prevent the variables from being written in plain text to the disk and only allows the user with the private key in his or her certificate store to read the variables on the disk and execute the script.

If this method meets the security needs of your project or organization, you can implement it using the following steps:

  1. Log in as the user you’ll be starting the container with (important because the certificate will be in his or her certificate store) and run the following command to create a self-signed certificate with the necessary key usages:
    New-SelfSignedCertificate -DnsName DockerScript -CertStoreLocation "Cert:\CurrentUser\My" -KeyUsage KeyEncipherment,DataEncipherment, KeyAgreement -Type DocumentEncryptionCert
    
  2. Create a directory to store your scripts:
    mkdir C:\docker_scripts
  3. Copy your existing, plain text script into the new directory you created in the previous step, which may look something like the one below. We’ll call it plaintext_script.ps1.
    docker run -d `
    -p 443:443 `
    --rm `
    -e SECRETVARIABLE1='secretvalue1' `
    -e SECRETVARIABLE2='secretvalue2' `
    -e SECRETVARIABLE3='secretvalue3' `
    -e SECRETVARIABLE4='secretvalue4' `
    -e SECRETVARIABLE5='secretvalue5' `
    dockerhub/path
    
  4. Encrypt your plain text script using the command below:
    Get-Content "C:\docker_scripts\plaintext_script.ps1" | Protect-CmsMessage -To cn=DockerScript -OutFile "C:\docker_scripts\encrypted_script.txt"
    
  5. The contents of encrypted_script.ps1 should now look something like this:
    -----BEGIN CMS-----
    MIICggYJKoZIhvcNAQcDoIICczCCAm8CAQAxggFHMIIBQwIBADArMBcxFTATBgNVBAMMDERvY2tl
    clNjcmlwdAIQQ0qkyH2hNZFGNPy0GVUqGjANBgkqhkiG9w0BAQcwAASCAQBsYLHgpVFcznh7F/+k
    QJUz50W/m3yoYvUQvpKzTBczplxFyJdwnssdfnCaflqBTxg07ZeK6BCinkRy6LhLc2yPqVbWu6EU
    +DjkUAr2yF8gdqz++J1fJOToA2pUyecZuFvtrO5fJ0v4j6FPNZ7XkJBq+t/WwbTmWIuhJBbgk5ZT
    iMtA5Xs6xaUAlL/lRLNUPtiJHkzb2j2ATf/WxjKxeL/vDcVaFObBEkVbeVzFtfZsYCCDWweIz5aH
    uPSflxgNdn5a4yTBpZUUWV22EAgTXI5POZzhYceBtirAT3OOozIHhaaGyLGQMW8Mo1lZTq/PGJE1
    dbeeDf4AJS20NfbB5V0AMIIBHQYJKoZIhvcNAQcBMB0GCWCGSAFlAwQBKgQQuBxq7jWpGbet4Esv
    YqqeZYCB8K/+paawXUubezwWESjJ3go7sVdy2Fs8IoRVV1lB5FFAWP8Fqdr4/RlNgCL5fDfKJVWM
    lkCX4ksS7XHBHvYwo/uenskChb+JMki5PA0a00vkhMwXHclZhzBJOr9XMjUv0lv63fi0eLG/kUXx
    C5SlJ3Ui9Lepm2nSag+4EQSWrGBsdwiyCTTjUOTgILwSg+3GUSdlb10MmP5/d+ym25EXvBjdN/76
    gqr75m50hrPj8Q2q97e+0Nq9BUwAP8P+PPYJvc9FDBFurxgKeR5KfjTdWjQC60AckmVFmhr51GoO
    2hBjN1dU2/v9no8VkscSdrjybQ==
    -----END CMS-----
  6. Delete your plain text script:
    del C:\docker_scripts\plaintext_script.ps1
  7. Finally create a wrapper script that will decrypt your encrypted script on demand using the private key in the user’s certificate store and execute it. We’ll call it “docker_secure_wrapper.ps1”:
    $scriptPath = split-path -parent $MyInvocation.MyCommand.Definition
    
    $script = Unprotect-CmsMessage -Path "$scriptPath\encrypted_script.txt"
    
    Invoke-Command -ScriptBlock ([scriptblock]::Create($script))
  8. You can use the following command to automate the execution of the script above:
    powershell -executionpolicy unrestricted -command ". 'C:\docker_scripts\docker_secure_wrapper.ps1'"

    I’d love to hear your concerns and criticisms about this solution in the comments. Please share how you’ve dealt with this on your projects and in your work environment, especially while maintaining vendor support for those that don’t offer kubernetes and Docker Swarm based deployments.