Certificates with Ansible, Letsencrypt and Cloudflare

  • December 2, 2016
  •  
  • December 3, 2018

This is an old example (albeit with a few updates) on how to use Ansible’s ACME-module to generate free certificates and validated them with a DNS record.

The example use Cloudflare for DNS, but any provider with an ansible module works.

To use the example, add your own email, api token and domain name to variables. To receive a certificate with an actual trusted root, change ACME Directory to https://acme-v02.api.letsencrypt.org/directory

---
- hosts: localhost
  vars:
    connection: local
    cloudflare_account_email: ''
    cloudflare_account_api_token: ''
    domain_name: ''

  tasks:
  - name: Create folder to store everything
    file:
      path: '~/.ssh/letsencrypt'
      state: directory
      mode: 0700

  - name: Create Letsencrypt Account private key
    openssl_privatekey:
      path: '~/.ssh/letsencrypt/letsencrypt.pem'
  
  - name: Create Domain Private Key
    openssl_privatekey:
      path: '~/.ssh/letsencrypt/{{ domain_name }}.pem'

  - name: Create Certificate Signing Request
    openssl_csr:
      path: '~/.ssh/letsencrypt/{{ domain_name }}.csr'
      privatekey_path: '~/.ssh/letsencrypt/{{ domain_name }}.pem'
      common_name: '{{ domain_name }}'
      subject_alt_name: "{{ item.value | map('regex_replace', '^', 'DNS:') | list }}"
    with_dict:
      dns_server:
      - '{{ domain_name }}'
      - '*.{{ domain_name }}'

  - name: Create ACME Challenge
    acme_certificate:
      account_key_src: '~/.ssh/letsencrypt/letsencrypt.pem'
      acme_directory: 'https://acme-staging-v02.api.letsencrypt.org/directory'
      acme_version: 2
      challenge: dns-01
      csr: '~/.ssh/letsencrypt/{{ domain_name }}.csr'
      fullchain_dest: '~/.ssh/letsencrypt/{{ domain_name }}.fullchain.pem'
      terms_agreed: yes
    register: le_challenge

  - name: Update DNS record
    cloudflare_dns:
      account_api_token: '{{ cloudflare_account_api_token }}'
      account_email: '{{ cloudflare_account_email }}'
      zone: '{{ domain_name }}'
      record: "{{ item['value']['dns-01']['resource'] }}"
      type: TXT
      value: "{{ item['value']['dns-01']['resource_value'] }}"
    when: le_challenge is changed
    with_dict: "{{ le_challenge['challenge_data'] }}"
    ignore_errors: yes

  - name: Wait a bit to let DNS update
    wait_for:
      timeout: 30
    when: le_challenge is changed

  - name: Validate ACME Challenge
    acme_certificate:
      account_key_src: '~/.ssh/letsencrypt/letsencrypt.pem'
      acme_directory: 'https://acme-staging-v02.api.letsencrypt.org/directory'
      acme_version: 2
      challenge: dns-01
      csr: '~/.ssh/letsencrypt/{{ domain_name }}.csr'
      data: '{{ le_challenge }}'
      fullchain_dest: '~/.ssh/letsencrypt/{{ domain_name }}.fullchain.pem'
      terms_agreed: yes
    when: le_challenge is changed

  - name: Remove TXT record after validating
    cloudflare_dns:
      account_api_token: '{{ cloudflare_account_api_token }}'
      account_email: '{{ cloudflare_account_email }}'
      zone: '{{ domain_name }}'
      record: "{{ item['value']['dns-01']['resource'] }}"
      type: TXT
      value: "{{ item['value']['dns-01']['resource_value'] }}"
      state: absent
    when: le_challenge is changed
    with_dict: "{{ le_challenge['challenge_data'] }}"
    ignore_errors: yes