Ansible: b64decode filter when using _copy_ not binary safe - example for Windows

Introduction

Sometimes you would like to fetch a file from one server, then put it onto another. There are many ways to solve this. One way that I have gotten very used to is to use the Ansible module slurp to fetch the content of the file into a variable, then putting it onto the destination server using the module copy. This has worked really well for me for quite some time, as I unutil now only have dealt with text (i.e letsencrypt certificates in PEM format).

The other day, we ran into a situation where we needed to fetch a PKCS12 formatted certificate and send it to a Windows server. It took us quite some time before we figured out that:

  • The normal way of doing this with the copy module and the filter b64decode is NOT binary safe

There are long rants about this around the internet, claiming that doing it this way sends the content through Ansible’s templating system. By doing so, it is apparently impossible to force the output to be binary. In our case, the binary content is converted to UTF8, as if it was a text string. The result is a file that is about twice the size of the original certificate file on the source system.

    #--- this does not work as planned, outputs UTF8
    - win_copy:
        content: "{{ remote.content | b64decode }}"
        dest:    "{{ certificate_destination }}"

This is how we solved it in the end:

  • We run the playbook for the windows host tst01-web-w01a
  • The PKCS12 certificate is stored in our linux host tst-certificatemanager-l01
  • We use delegate_to to tell Ansible to fetch the certificate from a different server
  • The magic sauce => [System.Convert]::FromBase64String("{{ remote.content }}") | Set-Content {{ certificate_destination }} -encoding byte

This example assumes that you have already converted your PEM formatted certificate to PKCS12 format on your server, and put it next to your other letsencrypt certificate.

- hosts: tst01-web-w01a
  vars:
    letsencrypt_host:           tst-certificatemanager-l01
    letsencrypt_path:           /etc/nginx/ssl
    letsencrypt_site:           api.tst01.corp.example.com
    certificate_destination:    C:\ansible\mycert.test.p12

    - name: Slurp certificate
      slurp:
        src:                    "{{letsencrypt_path}}/{{letsencrypt_site}}/fullcert.p12"
      register:                 remote
      delegate_to:              "{{ letsencryp_host }}"
      become:                   true
      become_method:            sudo
      remote_user:              "{{ ansible_user }}"

    - name: Place the certificate on the remote host
      win_shell: |
        [System.Convert]::FromBase64String("{{ remote.content }}") | Set-Content {{ certificate_destination }} -encoding byte

(Note: I am deliberately breaking the “whitespace” rule of PEP8 in my examples above. Makes it easier for me to read the code.)

References