morphit/ops/ansible/roles/tls/tasks/main.yml

111 lines
4.3 KiB
YAML

# Role: tls
#
# Certbot-managed Let's Encrypt cert with automatic renewal per
# OPERATIONS.md §35. The cert is requested via standalone mode
# on port 80 — BunkerWeb hands off port 80 to certbot during
# renewal via certbot's pre/post hooks (configured in the
# bunkerweb role).
#
# For first issuance, no service yet binds port 80, so standalone
# works cleanly.
---
- name: Install certbot
ansible.builtin.apt:
name:
- certbot
state: present
update_cache: true
- name: Check if cert already exists
ansible.builtin.stat:
path: "/etc/letsencrypt/live/{{ morphit_domain }}/fullchain.pem"
register: tls_cert_stat
- name: Determine certbot server
ansible.builtin.set_fact:
tls_certbot_server: "{{ 'https://acme-staging-v02.api.letsencrypt.org/directory' if tls_use_staging else 'https://acme-v02.api.letsencrypt.org/directory' }}"
- name: Probe whether Let's Encrypt is reachable (defer issuance to first-online if not)
# Any HTTP response from the ACME directory = we can reach it now. A connection
# failure (offline / censored) leaves .status undefined → we skip issuance and let
# morphit-first-online obtain the cert automatically once the box has internet.
ansible.builtin.uri:
url: "{{ tls_certbot_server }}"
method: GET
timeout: 8
register: tls_acme_probe
failed_when: false
changed_when: false
when: not tls_cert_stat.stat.exists
- name: Note that TLS issuance is deferred to first-online (Let's Encrypt unreachable now)
ansible.builtin.debug:
msg: >-
Let's Encrypt is not reachable right now, so the real certificate is DEFERRED.
BunkerWeb serves its self-signed fallback on the LAN, and morphit-first-online
obtains the Let's Encrypt certificate automatically the moment this box has
internet. Nothing to do — this is expected for an offline install.
when:
- not tls_cert_stat.stat.exists
- tls_acme_probe.status is not defined
- name: Request initial cert (standalone)
ansible.builtin.command:
cmd: >-
certbot certonly --standalone --non-interactive --agree-tos
--email {{ tls_acme_email }}
--server {{ tls_certbot_server }}
-d {{ morphit_domain }}
register: tls_certbot_result
# Non-fatal: if the domain does not resolve to this box yet, the install still
# completes and morphit-first-online retries issuance until it succeeds.
failed_when: false
changed_when: tls_certbot_result.rc == 0
when:
- not tls_cert_stat.stat.exists
- tls_acme_probe.status is defined
- name: Ensure certbot renewal timer is enabled
ansible.builtin.systemd:
name: certbot.timer
enabled: true
state: started
- name: Ensure the certbot deploy-hook directory exists
# The Ubuntu certbot package creates /etc/letsencrypt/renewal-hooks/{deploy,pre,post}
# on install, and certbot certonly (above) has already run — but ensuring it
# is a cheap guard so writing the hook can never fail on a missing dir.
ansible.builtin.file:
path: /etc/letsencrypt/renewal-hooks/deploy
state: directory
owner: root
group: root
mode: '0755'
- name: Configure certbot renewal hooks for BunkerWeb
ansible.builtin.copy:
dest: /etc/letsencrypt/renewal-hooks/deploy/morphit-bunkerweb-reload.sh
owner: root
group: root
mode: '0755'
content: |
#!/bin/sh
# Runs after certbot renews the cert.
# cp663 #7 — certbot RESETS the cert to root-only (0700 archive)
# on every renewal, which would silently break BunkerWeb's TLS
# (it runs as GID 101) ~60 days out. Re-assert group-readability
# so it self-heals. GID 101 is BunkerWeb's in-image user.
chgrp -R 101 /etc/letsencrypt/live /etc/letsencrypt/archive 2>/dev/null || true
chmod -R g+rX /etc/letsencrypt/live /etc/letsencrypt/archive 2>/dev/null || true
# Reload BunkerWeb (belt-and-braces; the scheduler also watches).
docker exec bunkerweb sh -c 'kill -HUP 1' 2>/dev/null || true
- name: Print TLS verification reminder
ansible.builtin.debug:
msg: |
TLS cert in place at /etc/letsencrypt/live/{{ morphit_domain }}/.
Renewal timer active. Verify with:
systemctl status certbot.timer
certbot renew --dry-run
when: tls_cert_stat.stat.exists or (tls_certbot_result is defined and tls_certbot_result.rc | default(1) == 0)