First-time Let's Encrypt cert issuance (temp ACME-challenge nginx + one-shot certbot), ported from the manual forge-docker Makefile bootstrap
Find a file
Nikki Claude e0df2a2b4d Add setup-tls-bootstrap: first-time cert issuance role
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>
2026-08-08 21:54:00 +07:00
defaults Add setup-tls-bootstrap: first-time cert issuance role 2026-08-08 21:54:00 +07:00
meta Add setup-tls-bootstrap: first-time cert issuance role 2026-08-08 21:54:00 +07:00
tasks Add setup-tls-bootstrap: first-time cert issuance role 2026-08-08 21:54:00 +07:00
templates Add setup-tls-bootstrap: first-time cert issuance role 2026-08-08 21:54:00 +07:00
README.md Add setup-tls-bootstrap: first-time cert issuance role 2026-08-08 21:54:00 +07:00

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 renew sidecar/cron (nginx-router-docker's nginx-certbot-renew service, setup-forgejo's equivalent) — those assume a cert already exists, same division of labor as the original Makefile (make certs/make startup for issuance vs. the standalone certbot-renew service for renewal).
  • DNS. Assumes tls_bootstrap_domain already 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).