ci: make TLS provisioning dynamic as domains are added

Adding a domain was a manual per-domain script run. With domains arriving
continuously that does not hold, so certificate issuance is now automatic.

HTTP already needed no work: the nginx catch-all serves any Host and the SPA
resolves its tenant from that header. Only TLS needed a name-by-name step.

Two mechanisms:

- setup-wildcard-tls.sh issues one DNS-01 wildcard for *.<apex>, after which a
  new tenant subdomain is live over HTTPS with zero certificate work.
- sync-domains.sh reconciles tenant-owned domains against a desired list on a
  10-minute timer: issues what is missing, skips certificates with >30 days
  left, skips names already covered by the wildcard, waits out unpropagated
  DNS, and caps issuance per run so a bad source cannot burn the weekly ACME
  budget.

Safety properties worth stating: a failed fetch of the desired list aborts the
run rather than reading as "remove every domain"; removing a domain disables
its server block but keeps the certificate, so re-adding is instant; malformed
hostnames are rejected before reaching certbot or an nginx server_name.

The source is pluggable - a file today, the Phase 9 domain registry once it
exists, whose MarketplaceDomain statuses already match what this needs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
sdarbinyan
2026-08-18 12:21:39 +04:00
parent 28861953c8
commit c721120e85
6 changed files with 511 additions and 9 deletions

View File

@@ -0,0 +1,12 @@
[Unit]
Description=Reconcile tenant TLS domains
After=network-online.target nginx.service
Wants=network-online.target
[Service]
Type=oneshot
ExecStart=/srv/marketplaces/bin/sync-domains.sh
# A failed run must not tear down what is already serving; the next run retries.
SuccessExitStatus=0
StandardOutput=journal
StandardError=journal

View File

@@ -0,0 +1,12 @@
[Unit]
Description=Reconcile tenant TLS domains every 10 minutes
[Timer]
OnBootSec=2min
OnUnitActiveSec=10min
# Spread load so many servers do not all hit the ACME API at once.
RandomizedDelaySec=90s
Persistent=true
[Install]
WantedBy=timers.target