- Jinja 100%
Ported from /opt/forge-docker/Makefile's manual two-stage pattern (make startup / make certs, docker-compose-init.yml) into an idempotent ansible role: temporary ACME-challenge-only nginx + one-shot certbot certonly --webroot, torn down after, cert files left behind. No-op if a cert already exists. Solves the chicken-and-egg problem where a TLS-serving vhost's ssl_certificate line makes nginx refuse to start at all before a cert exists. Signed-off-by: Nikki Claude <nikki.claude@avallon.pw> |
||
|---|---|---|
| defaults | ||
| meta | ||
| tasks | ||
| templates | ||
| README.md | ||
setup-tls-bootstrap
First-time Let's Encrypt certificate issuance, ported from the manual
two-stage Makefile pattern that originally bootstrapped forge.avallon.pw
by hand (/opt/forge-docker/Makefile, docker-compose-init.yml,
startup.sh) into an idempotent ansible role.
The chicken-and-egg problem this solves
A TLS-serving nginx vhost references ssl_certificate files that must
already exist, or nginx refuses to start at all — not just the HTTPS
server block, the whole process. But you can't get a cert from Let's
Encrypt without an HTTP server answering the ACME HTTP-01 challenge on
port 80 for that domain. Chicken, egg.
This role breaks the cycle: it stands up a temporary, ACME-challenge-only
nginx (no TLS block, nothing that could fail to start) on port 80, runs a
one-shot certbot certonly --webroot against it, tears the temporary nginx
down, and leaves the cert files behind — so that when the real stack
starts afterward, the cert it needs is already there.
Usage
Run this before deploying the real TLS vhost config, in the same role or playbook:
- name: Bootstrap TLS cert if this domain doesn't have one yet
ansible.builtin.include_role:
name: setup-tls-bootstrap
vars:
tls_bootstrap_domain: forge.avallon.pw
tls_bootstrap_email: certs@avallon.pw
tls_bootstrap_letsencrypt_dir: "{{ forgejo_install_dir }}/letsencrypt"
tls_bootstrap_webroot_dir: "{{ forgejo_install_dir }}/webroot"
- name: Deploy the real TLS vhost config
ansible.builtin.template:
src: nginx/conf.d/forge.conf.j2
dest: "{{ forgejo_install_dir }}/nginx/conf.d/forge.conf"
when: nginx_included | bool
notify: reload forgejo-nginx
If a cert already exists at
{{ tls_bootstrap_letsencrypt_dir }}/live/{{ tls_bootstrap_domain }}/fullchain.pem
(this role already ran once, or the cert was inherited some other way — a
host symlink, a manual copy), the whole role is a no-op. Safe to always
include unconditionally ahead of a TLS deploy.
Variables
| Variable | Required | Default | Description |
|---|---|---|---|
tls_bootstrap_domain |
yes | "" |
domain to issue a cert for |
tls_bootstrap_email |
yes | "" |
email for the ACME account |
tls_bootstrap_letsencrypt_dir |
yes | "" |
host path bind-mounted as /etc/letsencrypt |
tls_bootstrap_webroot_dir |
yes | "" |
host path bind-mounted as /var/www/certbot |
tls_bootstrap_http_port |
no | 80 |
host port the temporary nginx binds — must be the port the domain actually resolves to; the real stack must not be listening on it yet |
tls_bootstrap_nginx_image |
no | nginx:1.27-alpine |
image for the temporary challenge server |
tls_bootstrap_certbot_image |
no | certbot/certbot |
image for the one-shot certbot run |
tls_bootstrap_staging |
no | false |
use Let's Encrypt's staging environment (untrusted certs, no rate limits) — for testing this role itself |
What it doesn't do
- Renewal. This is first-issuance only. Renewal is handled separately by
each role's own
certbot renewsidecar/cron (nginx-router-docker'snginx-certbot-renewservice,setup-forgejo's equivalent) — those assume a cert already exists, same division of labor as the original Makefile (make certs/make startupfor issuance vs. the standalonecertbot-renewservice for renewal). - DNS. Assumes
tls_bootstrap_domainalready resolves to this host. If it doesn't, the certbot run fails with a clear error and the role fails loudly (checks the cert file actually exists afterward, doesn't just trust certbot's exit code).