Creating a Let’s Encrypt Wildcard Cert with Ansible

The other day I needed to add a wildcard cert to one of our staging servers. Using Jeff Geerling’s Ansible Role – Certbot (for Let’s Encrypt) for single domains provides an out of the box experience. Since we need a wildcard cert before installing Apache or Nginx we need to use a DNS plugin, there Is no web server to validate against. The plugins are not installed by default so we will need to run the Certbot role without any domains the install the plugin and run Certbot again with domains.

The Setup Role

We will use the setup role to install the DNS plugin in between steps. Certbot uses ini files for settings. We will need two template files. For this demo I am using CloudFlare, the technique should work for the other supported DNS hosts.

Letsencrypt cli Template
roles/setup/files/templates/letsencrypt_cli.ini.j2

# Let's Encrypt site-wide configuration
dns-cloudflare-credentials = /etc/letsencrypt/dnscloudflare.ini
# Use the ACME v2 staging URI for testing things
#server = https://acme-staging-v02.api.letsencrypt.org/directory
# Production ACME v2 API endpoint
server = https://acme-v02.api.letsencrypt.org/directory

CloudFlare Template
roles/setup/files/templates/confcloudflare.ini.j2

# Cloudflare API credentials used by Certbot
dns_cloudflare_email = {{setup_dns_cloudflare_email}}
dns_cloudflare_api_key = {{setup_dns_cloudflare_api_key}}

roles/setup/tasks/main.yml

--
  - name: Install certbot-dns-cloudflare
    shell: "cd /opt/certbot/certbot-dns-cloudflare && python setup.py install"
    when: "'demoweb' in group_names"
  - name: Create certbot settings folder
    file:
      path: /etc/letsencrypt
      state: directory
      owner: root
      group: root
      mode: 0700
    when: "'demoweb' in group_names"
  - name: Create Certbot ini files
    template:
      src: "{{ item.src }}"
      dest: "{{ item.dest }}"
      owner: root
      group: root
      mode: 0600
    with_items:
      - { src: 'files/templates/confcloudflare.ini.j2', dest: '/etc/letsencrypt/dnscloudflare.ini' }
      - { src: 'files/templates/letsencrypt_cli.ini.j2', dest: '/etc/letsencrypt/cli.ini' }
    when: "'demoweb' in group_names"

roles/setup/detaults/main.yml

setup_dns_cloudflare_email: ''
setup_dns_cloudflare_api_key: ''

The Playbook

Below is a sample playbook. It does not include php or mySQL needed for a full LAMP server.

main.yml

- hosts: demo
  remote_user: sudousername # Change this to your remote user
  become: true
  pre_tasks:
  - name: Install python for Ansible
    raw: test -e /usr/bin/python || (apt -y update && apt install -y python-minimal)
    changed_when: False
  - name: Set timezone
    timezone:
      name: America/Chicago
  vars_files:
    - vars/main-vars.yml

  roles:
    - geerlingguy.pip
    - { role: geerlingguy.certbot, certbot_certs: [] } # Just install certbot
    - setup # Install DNS plugin
    - geerlingguy.certbot # Create the cert for our site
    - geerlingguy.apache # Install Apache with Dynamic Vhosts

vars/vars-main.yml

# Cerbot
certbot_create_if_missing: yes
certbot_install_from_source: yes # includes the plugin folders to aid install
certbot_email: "[email protected]" # Your admin email address
certbot_create_method: standalone 
certbot_create_standalone_stop_services:
    - apache
    # - nginx
## In my tests you have to use `certbot` not `certbot-auto` with the dns plugins
certbot_create_command: "certbot certonly --noninteractive --dns-cloudflare --agree-tos --email {{ cert_item.email | default(certbot_admin_email) }} -d {{ cert_item.domains | join(',') }}"

## If you have 2 servers web01 and web02 this will setup the wilcard cert per server 
## *.web01.example.com
## *.web02.example.com
certbot_certs:
  - email: "{{ certbot_email }}"
    domains:
      - "*.{{ansible_nodename}}.{{base_domain}}"
setup_dns_cloudflare_email: "[email protected]" # Your CloudFlare email
## To protect your API Key encrypt it with (tun it in terminal):
## ansible-vault encrypt_string  'cloudflareapikey' --name 'setup_dns_cloudflare_api_key'
setup_dns_cloudflare_api_key: !vault |
          $ANSIBLE_VAULT;1.1;AES256
          62636164366637336237386437373030326162316236663365613930626438663737666337366230
          3536333562666666366338613666386532383237643335360a613131386630393863343135306433
          37323264393462363261313436363265663065343834373861373864393732653236376138636232
          6163343664353030380a346166626631373366386163373935613033386633336664633037346366
          61326135646639353462353530393832346564373665323564353864626364363232

# Apache
base_domain: "michaelpporter.com" # your domain
apache_remove_default_vhost: true
apache_global_vhost_settings: |
  DirectoryIndex index.php index.html index.shtml
apache_vhosts:
  - servername: "{{ansible_nodename}}.{{base_domain}}"
    extra_parameters: |
      ErrorLog ${APACHE_LOG_DIR}/error.log
      LogFormat "%V %h %l %u %t \"%r\" %s %b" vcommon
apache_vhosts_ssl:
  - servername: "{{ansible_nodename}}.{{base_domain}}"
    certificate_file: "/etc/letsencrypt/live/{{ansible_nodename}}.{{base_domain}}/fullchain.pem"
    certificate_key_file: "/etc/letsencrypt/live/{{ansible_nodename}}.{{base_domain}}/privkey.pem"
    extra_parameters: |
      ErrorLog ${APACHE_LOG_DIR}/error.log
      LogFormat "%V %h %l %u %t \"%r\" %s %b" vcommon

# PHP
## Adjust to your needs
php_version: "7.1"
php_install_recommends: no
php_memory_limit: "256M"
php_display_errors: "On"
php_display_startup_errors: "On"
php_realpath_cache_size: "1024K"
php_opcache_enabled_in_ini: true
php_opcache_memory_consumption: "192"
php_opcache_max_accelerated_files: 4096
php_max_input_vars: "4000"
php_upload_max_filesize: "64M"
php_max_file_uploads: "20"
php_post_max_size: "32M"
php_date_timezone: "America/Chicago"