Obtaining a wildcard LetsEncrypt cert with Ansible

Earlier this year, LetsEncrypt made their wildcard x509 certificates available to the general public. Whilst this is a massive step forward over individual certificates for each domain, it does come with the overhead of having to distribute the wildcard certificate to the (possibly many) places you would use it. Ignoring that issue for now, I wrote a quick Ansible playbook which uses the dns-01 challenge method and my Memset DNS management modules (available in Ansible 2.6+) to provide the verification.

Without further ado:

---
- hosts: localhost
  gather_facts: no

  vars:
    tmpdir: "/tmp/le/"
    account_key: "le-account-key.pem"
    keyname: "star-domain-com-key.pem"
    csrname: "star-domain-com.csr"
    certname: "star-domain-com.pem"
    fullchain: "star-domain-com-fullchain.pem"
    common_name: "*.domain.com"

  tasks:
  - name: localhost | create temp dir
    file:
      path: "{{ tmpdir }}"
      state: directory

  - name: localhost | create temp account key
    openssl_privatekey:
      path: "{{ tmpdir }}{{ account_key }}"

  - name: localhost | create private key
    openssl_privatekey:
      path: "{{ tmpdir }}{{ keyname }}"

  - name: localhost | create CSR
    openssl_csr:
      path: "{{ tmpdir }}{{ csrname }}"
      privatekey_path: "{{ tmpdir }}{{ keyname }}"
      common_name: "{{ common_name }}"
      country_name: GB
      organization_name: my-OU
      email_address: ops@domain.com

  - name: LetsEncrypt | submit request
    acme_certificate:
      account_key_src: "{{ tmpdir }}{{ account_key }}"
      account_email: me@domain.com
      src: "{{ tmpdir }}{{ csrname }}"
      fullchain_dest: "{{ tmpdir }}{{ certname }}"
      challenge: dns-01
      acme_directory: https://acme-staging-v02.api.letsencrypt.org/directory
      acme_version: 2
      terms_agreed: yes
      remaining_days: 60
    register: challenge

  - name: Memset | create DNS challenge record
    memset_zone_record:
      api_key: "{{ memset_dns_api_key }}"
      state: present
      zone: domain.com
      type: TXT
      record: "{{ challenge['challenge_data']['*.domain.com']['dns-01']['resource'] }}"
      data: "{{ challenge['challenge_data']['*.domain.com']['dns-01']['resource_value'] }}"

  - name: Memset | request DNS reload
    memset_dns_reload:
      api_key: "{{ memset_dns_api_key }}"
      poll: true

  - name: LetsEncrypt | retrieve cert
    acme_certificate:
      account_key_src: "{{ tmpdir }}{{ account_key }}"
      account_email: me@domain.com
      src: "{{ tmpdir }}{{ csrname }}"
      dest: "{{ tmpdir }}{{ certname }}"
      fullchain_dest: "{{ tmpdir }}{{ fullchain }}"
      challenge: dns-01
      acme_directory: https://acme-staging-v02.api.letsencrypt.org/directory
      acme_version: 2
      terms_agreed: yes
      remaining_days: 60
      data: "{{ challenge }}"
    register: cert_retrieval

  - name: Memset | delete DNS challenge record
    memset_zone_record:
      api_key: "{{ memset_dns_api_key }}"
      state: absent
      zone: domain.com
      type: TXT
      record: "{{ challenge['challenge_data']['*.domain.com']['dns-01']['resource'] }}"
      data: "{{ challenge['challenge_data']['*.domain.com']['dns-01']['resource_value'] }}"
    when: cert_retrieval is changed

  - name: localhost | remove the account key
    file:
      path: "{{ tmpdir }}{{ account_key }}"
      state: absent
    when: cert_retrieval is changed

Points to note

I’ve deliberately used the staging endpoint provided by LetsEncrypt; the certs this issues won’t be valid for use but it allows you to test your playbook without hitting the account rate limits.

The last two tasks cleanup the account key and DNS challenge record, but only if the certificate was successfully issued.

Testing

$ openssl x509 -in star-domain-com.pem -noout -text | grep "Subject: CN"
        Subject: CN = *.domain.com

There we have it; one wildcard certificate from LetsEncrypt!