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!

Wildcard LetsEncrypt renewal with Ansible and Memset

2 thoughts on “Wildcard LetsEncrypt renewal with Ansible and Memset

  • 31st May 2019 at 18:56
    Permalink

    Hi there,

    I’ve been working on a project to renew my companies SSL Certificates over the last couple months, and I think I finally have it all working except one issue that I can’t seem to find information on: Getting the remaining_days piece working. I’m confused as to how to use it correctly and it doesn’t appear to be working in my project. As a result my Renew task errors out most of the time with the message that I’ve hit my limit. Some of my confusion lays around which files the module needs from the last time certificates were generated and which files it needs me to generate new for the new certificates I’m requesting. The CSR and Account Key for example. Can I keep everything as normal and simply pass in the old certificate so the module can check its expiration? Or does LE keep track of this and if it’s not working then it’s a bug? Any help understanding this would be appreciated. Thank you!

    Reply
    • 13th June 2019 at 10:34
      Permalink

      Hi John, apologies for taking a while to get back to you. I’m afraid I’m not 100% on this, but having read the docs (https://docs.ansible.com/ansible/devel/modules/acme_certificate_module.html), I’m pretty sure that information is stored in LE’s database – I don’t pass the old cert to the command so it has no way of determining the remaining lifespan. Hope that’s some help!

      Reply

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.