david alfonso

Certbot webroot auth with Ansible

Certbot is the recommended software to generate Let's Encrypt certificates and prove you control the domain. It does so by implementing the ACME protocol.

The webroot authenticator plugin allows to renew certificates without stopping the web server. It works by writing a challenge file to the domain's document root path that will be requested by the Let's Encrypt validation service. This file will live in a hidden folder named .well-known/acme-challenge/ and will be accessed through the standard HTTP port.

Apache configuration

Let's start from the end by making sure that Apache will serve files in the expected path:

<VirtualHost *:80>
  # ...

  # Avoid redirecting to HTTPS if it's an acme challenge request
  RewriteEngine On
  RewriteCond %{REQUEST_URI} !^/\.well\-known/acme\-challenge/
  RewriteRule ^(.*)$ https://%{HTTP_HOST}$1 [R=301,L]

We now face a chicken-egg problem where we need a web server in the authentication process but Apache will fail to start if a referenced file does not exist.

A solution is to add a conditional clause to the HTTPS virtual host so that it's only active when the certificate exists:

<IfFile /etc/letsencrypt/live/your-domain.com/fullchain.pem>
  <VirtualHost *:443>
    # ...
    SSLCertificateFile /etc/letsencrypt/live/your-domain.com/fullchain.pem
    # ...

This allows to start Apache serving only on the HTTP port, but we'll need to reload the configuration after the certificate is generated.

Ansible orchestration

Let's assume we're managing multiple domains and have these variables defined:

apache_cert_path: /etc/letsencrypt/live
  - hostname: domain1.com
    # other info for domain1
  - hostname: domain2.com
    # other info for domain2

We need to first install and start Apache with the appropriate configuration. We could use the geerlingguy apache role for that:

- name: Use geerlingguy apache role
    name: geerlingguy.apache
    # our config...

If we're using a firewall like UFW, we have to open the ports, but note that it won't restart and apply the new config until the end of the play:

- name: UFW - Allow HTTP/HTTPS connections
    rule: allow
    name: Apache Full
  notify: Restart ufw

Now, before setting up certbot, let's check if there are any certificates missing and, if that's the case, force a UFW restart (reload is not supported) to make sure the HTTP port will be opened for the authentication process.

- name: Certbot - Check if certificates exist
    path: "{{ apache_cert_path }}/{{ item.hostname }}/fullchain.pem"
  loop: "{{ apache_sites }}"
  register: certbot_certs_stat

- name: Certbot - Force UFW restart
    name: ufw
    state: restarted
  when: certbot_certs_stat.results | rejectattr('stat', 'defined') != []

Finally, using geerlingguy certbot role, create the certificates and reload the Apache config, if necessary.

- name: Certbot - Include certbot role
    name: geerlingguy.certbot
    certbot_create_method: webroot
    certbot_create_if_missing: true
    # other settings...

- name: Certbot - Force Apache config reload after creating new certificates
    name: apache2
    state: reloaded
  when: certbot_certs_stat.results | rejectattr('stat', 'defined') != []