home tools books contact

Generate Let's Encrypt SSL Certificates with Ansible and Cloudflare

Ansible Automation

This tutorial will show you how to create an Ansible playbook that makes it easy to automate the creation and renewal of Let's Encrypt SSL certificates.

What is Let’s Encrypt?

Let’s Encrypt is a non-profit certificate authority that provides SSL certificates for free. Let’s Encrypt certificates are valid for 90 days and the validation is done using HTTP or DNS which means you can automate the creation process.

This is better than the old way of email validation and having to manually apply certificates because it saves time and reduces the chances of making mistakes or forgetting to renew the certificate.

For this tutorial, we will use DNS as the method of validation by creating an Ansible task that automatically updates the Cloudflare DNS record. If you don’t use Cloudflare, you can still get an idea of how to generate the SSL from this playbook.

Create Ansible Playbook

Create a file called ssl.yml and add the following:

- hosts: localhost
  gather_facts: no
  vars:
    certs_path: ../certs
    crt_common_name: graspingtech.com
    crt_subject_alt_name: 
      - www.graspingtech.com
    cloudflare_email:     "{{ lookup('env','CF_EMAIL') }}"
    cloudflare_api_token: "{{ lookup('env','CF_API_TOKEN') }}"
    cloudflare_zone:      "{{ lookup('env','CF_ZONE') }}"
  
  tasks:
  - name: create directory to store certs
    file:
      path: "{{ certs_path }}"
      state: directory

  - name: generate account key
    openssl_privatekey:
      path: "{{ certs_path }}/account-key.pem"
      size: 4096

  - name: generate signing key
    openssl_privatekey:
      path: "{{ certs_path }}/{{ crt_common_name }}.pem"
      size: 4096

  - name: generate csr
    openssl_csr:
      path: "{{ certs_path }}/{{ crt_common_name }}.csr"
      privatekey_path: "{{ certs_path }}/{{ crt_common_name }}.pem"
      common_name: "{{ crt_common_name }}"
      subject_alt_name: "DNS:{{ crt_subject_alt_name | join(',DNS:') }}"

  - name: create acme challenge
    acme_certificate:
      acme_version: 2
      terms_agreed: yes
      account_key_src: "{{ certs_path }}/account-key.pem"
      src: "{{ certs_path }}/{{ crt_common_name }}.csr"
      cert: "{{ certs_path }}/{{ crt_common_name }}.crt"
      challenge: dns-01
      acme_directory: https://acme-v02.api.letsencrypt.org/directory
      remaining_days: 60
    register: challenge

  - name: create cloudflare TXT records
    cloudflare_dns:
      account_api_token: "{{ cloudflare_api_token }}"
      account_email: "{{ cloudflare_email }}"
      zone: "{{ cloudflare_zone }}"
      record: "{{ challenge.challenge_data[item]['dns-01'].record }}"
      type: TXT
      value: "{{ challenge.challenge_data[item]['dns-01'].resource_value }}"
      solo: true
      state: present
    with_items: "{{ [crt_common_name] + crt_subject_alt_name }}"
    when: challenge is changed

  - name: validate acme challenge
    acme_certificate:
      acme_version: 2 
      account_key_src: "{{ certs_path }}/account-key.pem"
      src: "{{ certs_path }}/{{ crt_common_name }}.csr"
      cert: "{{ certs_path }}/{{ crt_common_name }}.crt"
      fullchain: "{{ certs_path }}/{{ crt_common_name }}-fullchain.crt"
      chain: "{{ certs_path }}/{{ crt_common_name }}-intermediate.crt"
      challenge: dns-01
      acme_directory: https://acme-v02.api.letsencrypt.org/directory
      remaining_days: 60
      data: "{{ challenge }}"
    when: challenge is changed

  - name: delete cloudflare TXT record
    cloudflare_dns:
      account_api_token: "{{ cloudflare_api_token }}"
      account_email: "{{ cloudflare_email }}"
      zone: "{{ cloudflare_zone }}"
      record: "{{ challenge.challenge_data[item]['dns-01'].record }}"
      type: TXT
      state: absent
    with_items: "{{ [crt_common_name] + crt_subject_alt_name }}"
    when: challenge is changed

Assign your domain name to the crt_common_name variable and any extra subdomains you want to include in the certificate to the crt_subject_alt_name variable.

Since the playbook is using Cloudflare DNS to validate the Let’s Encrypt certificate, you will also need to assign your Cloudflare API credentials to the cloudflare_* variables. You can either assign them directly in the playbook or pass them in via environment variables by running the commands below before running the playbook:

export CF_EMAIL=<your_cloudflare_email>
export CF_API_TOKEN=<your_cloudflare_api_token>
export CF_ZONE=<your_cloudflare_api_zone>

Run Playbook

Run the Ansible playbook with the following command:

ansible-playbook ssl.yml

After running the playbook. The playbook will create a new folder called certs and the generated keys and certificates will be stored inside it.

Output of Ansible playbook generating Let’s Encrypt SSL

certs folder

Let’s Encrypt SSL certs generated via Ansible

Conclusion

That’s all there is to it. After running the playbook, you should have a certs folder containing your SSL certificate. Now you can use another playbook to deploy and install the certificate to your web or application server.

If you ever need to renew the certificate, you can run this playbook again and the new certificate will replace the existing one.

Written by: Tony Mackay