feat(infra): provision the public rob.fyi vhost + LE cert in infra-setup
Some checks failed
deploy / build-api (push) Waiting to run
deploy / build-web (push) Successful in 1m33s
deploy / deploy-api (push) Has been cancelled
deploy / deploy-web (push) Has been cancelled

infra-setup only handled the internal (newsfeed.internal) cert and vhost and
just printed a generic "set it up yourself" note for the WAN name. Now it also,
on the web host:

- issues the Let's Encrypt cert for rob.fyi idempotently (certbot, Cloudflare
  DNS-01, ECDSA, --keep-until-expiring), skipping cleanly if the CF token or
  certbot is absent;
- installs the certbot renew deploy-hook (reload nginx);
- installs the public vhost, gated on the cert existing so a missing cert can't
  break `nginx -t` for all of nginx (external-tls.md §4).

PUBLIC_FQDN/CERT_EMAIL/CF_CREDS are infra-truth vars at the top; set PUBLIC_FQDN
empty to skip. Cloudflare A record + OPNsense :443 forward remain manual and are
called out in the closing output.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016fKZzDpvjiJ9eYbPGgJvUP
This commit is contained in:
2026-07-08 12:19:31 +03:00
parent 62d5bc6569
commit 19dfe793e0
2 changed files with 69 additions and 5 deletions

View File

@@ -21,6 +21,9 @@ API_HOST="slartibartfast.kosherinata.internal" # newsfeed-api + newsfeed-worke
WEB_HOST="oolon.kosherinata.internal" # nginx: SPA + API reverse proxy
API_PORT="8081"
CERT_NAME="newsfeed" # dot-free; serves ${CERT_NAME}.internal
PUBLIC_FQDN="rob.fyi" # WAN name (Let's Encrypt); "" to skip
CERT_EMAIL="ops@rob.fyi" # certbot registration email
CF_CREDS="/root/.certbot-internal" # Cloudflare API token on the web host
RUNNER_PUBKEY="${HOME}/.ssh/id_gitea_ci.pub" # runner key distributed to gitea_ci
PROVISIONER_PW="${HOME}/.step/secrets/provisioner"
ROOT_CA="/etc/pki/ca-trust/source/anchors/root-internal.pem"
@@ -163,8 +166,66 @@ VHOST
nginx -t
systemctl reload nginx
"
info "[$host] to publish on the WAN, set server_name + a Let's Encrypt cert in"
info " asset/nginx/newsfeed.public.conf and symlink it into sites-enabled."
provision_public_vhost "$host"
}
# Issue the public Let's Encrypt cert for $PUBLIC_FQDN (idempotent) and install the WAN
# vhost, gated on the cert existing (external-tls.md). Skips cleanly if PUBLIC_FQDN is
# unset or the Cloudflare credential isn't on the host.
provision_public_vhost() {
local host="$1"
[[ -n "$PUBLIC_FQDN" ]] || { info "[$host] PUBLIC_FQDN unset; skipping WAN vhost"; return 0; }
if [[ "$DRY_RUN" == 1 ]]; then info "[$host] would issue LE cert + install ${PUBLIC_FQDN} vhost"; return 0; fi
info "[$host] issuing Let's Encrypt cert for ${PUBLIC_FQDN} (certbot, Cloudflare DNS-01)"
# --keep-until-expiring makes this a no-op when the cert is still valid, so it is safe
# to run unconditionally. DNS-01 needs no inbound :80 and works even before the WAN A
# record / OPNsense forward exist. Requires the Cloudflare token at $CF_CREDS.
run_remote "$host" "
if [ ! -f '$CF_CREDS' ]; then
echo 'WARN: $CF_CREDS missing; cannot issue ${PUBLIC_FQDN} cert' >&2
exit 0
fi
command -v certbot >/dev/null || { echo 'WARN: certbot not installed' >&2; exit 0; }
certbot certonly \
-m '$CERT_EMAIL' --agree-tos --no-eff-email --noninteractive \
--cert-name '$PUBLIC_FQDN' \
--key-type ecdsa \
--dns-cloudflare \
--dns-cloudflare-credentials '$CF_CREDS' \
--dns-cloudflare-propagation-seconds 60 \
--keep-until-expiring \
-d '$PUBLIC_FQDN'
# nginx reload after future renewals (external-tls.md §3), once per host.
install -d -m 0755 /etc/letsencrypt/renewal-hooks/deploy
cat > /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh <<'HOOK'
#!/bin/sh
systemctl reload nginx 2>/dev/null || true
HOOK
chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
"
info "[$host] installing WAN vhost (${PUBLIC_FQDN})"
local vhost; vhost="$(cat "$REPO_ROOT/asset/nginx/newsfeed.public.conf")"
# Gate the vhost on the cert existing — nginx -t fails on a missing ssl_certificate and
# would block ALL of nginx from reloading (external-tls.md §4).
run_remote "$host" "
if ! test -d /etc/letsencrypt/live/${PUBLIC_FQDN}; then
echo 'NOTE: ${PUBLIC_FQDN} cert not present; skipping WAN vhost install' >&2
exit 0
fi
cat > /etc/nginx/sites-available/newsfeed.public.conf <<'VHOST'
${vhost}
VHOST
ln -sfn ../sites-available/newsfeed.public.conf /etc/nginx/sites-enabled/newsfeed.public.conf
nginx -t
systemctl reload nginx
"
info "[$host] ${PUBLIC_FQDN} served. Ensure the Cloudflare A record ${PUBLIC_FQDN} ->"
info " kosherinata WAN IP exists (unproxied) and OPNsense forwards :443 -> oolon"
info " (reverse-proxies.md §2)."
}
# Mint the first internal cert (idempotent) and enable renewal (internal-tls.md §4).