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]
</VirtualHost>
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
# ...
</VirtualHost>
</IfFile>
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
apache_sites:
- 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
ansible.builtin.include_role:
name: geerlingguy.apache
vars:
# 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
community.general.ufw:
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
ansible.builtin.stat:
path: "{{ apache_cert_path }}/{{ item.hostname }}/fullchain.pem"
loop: "{{ apache_sites }}"
register: certbot_certs_stat
- name: Certbot - Force UFW restart
ansible.builtin.service:
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
ansible.builtin.include_role:
name: geerlingguy.certbot
vars:
certbot_create_method: webroot
certbot_create_if_missing: true
# other settings...
- name: Certbot - Force Apache config reload after creating new certificates
ansible.builtin.service:
name: apache2
state: reloaded
when: certbot_certs_stat.results | rejectattr('stat', 'defined') != []