Files
ulicraft-server-v1/tooling/issue-letsencrypt.sh
Oier Bravo Urtasun ab3d2c201b feat(ingress): public distribution.${BASE_DOMAIN} static vhost
Serve a static site at distribution.${BASE_DOMAIN} whose web root lives in
another repo (DISTRIBUTION_WEB_ROOT, bind-mounted read-only into caddy):
  - caddy/conf.d/30-distribution.caddy  file_server vhost
  - docker-compose.distribution.yml     mount snippet + external web root
  - issue-letsencrypt.sh / .env.example add distribution to LE_SUBDOMAINS
  - ulicraft-caddy.conf.tmpl            TLS-front block proxying to caddy

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 00:30:27 +02:00

102 lines
4.2 KiB
Bash
Executable File

#!/usr/bin/env bash
# issue-letsencrypt.sh — issue per-name Let's Encrypt certificates via the OVH
# DNS-01 challenge (acme.sh) and install them where Caddy serves them.
#
# Run MANUALLY on the host. One separate certificate per name:
# ${BASE_DOMAIN} + <sub>.${BASE_DOMAIN} for each sub in LE_SUBDOMAINS.
# Certs land in certs/<name>/{cert.pem,key.pem} (gitignored) and the host nginx
# is reloaded so it picks them up (see nginx/ulicraft.conf.tmpl).
#
# DNS-01 needs no inbound ports — works for a LAN-only host. It only needs OVH
# API credentials with DNS-record write access for the zone.
#
# .env vars required:
# BASE_DOMAIN e.g. ulicraft.net
# LE_EMAIL account email for Let's Encrypt
# OVH_AK OVH_AS OVH application key + secret
# OVH_CK OVH consumer key (obtained on first run — see below)
# OVH_END_POINT OVH API endpoint (e.g. ovh-eu)
# optional:
# LE_SUBDOMAINS space-separated (default: "auth pack distribution")
# LE_STAGING=1 use the LE staging CA (untrusted, for dry runs)
#
# First-time OVH consumer key: set OVH_AK/OVH_AS/OVH_END_POINT, leave OVH_CK
# empty, run this once — acme.sh prints an authorization URL. Visit it, grant
# access, then copy the consumer key it shows into OVH_CK in .env and re-run.
# (Or create a token directly: https://eu.api.ovh.com/createToken/ with
# GET/POST/PUT/DELETE on /domain/zone/*.)
set -euo pipefail
cd "$(dirname "$0")/.." # repo root
log() { printf '\n\033[1;32m==>\033[0m %s\n' "$*"; }
warn() { printf '\033[1;33m!!\033[0m %s\n' "$*" >&2; }
die() { printf '\033[1;31mxx\033[0m %s\n' "$*" >&2; exit 1; }
# ---- config / preflight ----------------------------------------------------
[ -f .env ] || die ".env not found (copy .env.example and fill it)"
# shellcheck disable=SC1091
set -a; . ./.env; set +a
: "${BASE_DOMAIN:?BASE_DOMAIN unset in .env}"
: "${LE_EMAIL:?LE_EMAIL unset in .env}"
: "${OVH_AK:?OVH_AK unset in .env}"
: "${OVH_AS:?OVH_AS unset in .env}"
: "${OVH_END_POINT:?OVH_END_POINT unset in .env (e.g. ovh-eu)}"
# OVH_CK may be empty on the very first run (acme.sh will print the auth URL).
export OVH_AK OVH_AS OVH_END_POINT
export OVH_CK="${OVH_CK:-}"
ACME="${ACME_SH:-$HOME/.acme.sh/acme.sh}"
[ -x "$ACME" ] || ACME="$(command -v acme.sh || true)"
[ -n "$ACME" ] && [ -x "$ACME" ] || die "acme.sh not found. Install:
curl https://get.acme.sh | sh -s email=${LE_EMAIL}
then re-run (or set ACME_SH=/path/to/acme.sh)."
CERT_DIR="${CERT_DIR:-certs}"
read -r -a SUBDOMAINS <<< "${LE_SUBDOMAINS:-auth pack distribution}"
if [ "${LE_STAGING:-0}" = "1" ]; then
SERVER="letsencrypt_test"; warn "STAGING mode — certs will NOT be trusted by browsers"
else
SERVER="letsencrypt"
fi
# Reload the host nginx after each install so new certs take effect. Override
# RELOAD_CMD if nginx runs elsewhere (e.g. a container or a non-systemd host).
RELOAD="${RELOAD_CMD:-sudo systemctl reload nginx} || true"
# Full name list: apex + each subdomain.
domains=("$BASE_DOMAIN")
for s in "${SUBDOMAINS[@]}"; do domains+=("${s}.${BASE_DOMAIN}"); done
log "CA=$SERVER names: ${domains[*]}"
# Register the ACME account once (idempotent).
"$ACME" --register-account -m "$LE_EMAIL" --server "$SERVER" >/dev/null 2>&1 || true
# ---- issue + install one cert per name --------------------------------------
issued=0
for d in "${domains[@]}"; do
log "issuing $d (OVH DNS-01)"
rc=0
"$ACME" --issue --dns dns_ovh --server "$SERVER" --keylength ec-256 -d "$d" || rc=$?
# acme.sh: 0 = issued/renewed, 2 = skipped (cert still valid, not due). Both ok.
case "$rc" in
0|2) ;;
*) die "acme.sh failed for $d (rc=$rc). If this is the first OVH run, follow
the consumer-key authorization URL printed above, set OVH_CK in .env, re-run." ;;
esac
mkdir -p "$CERT_DIR/$d"
"$ACME" --install-cert -d "$d" --ecc \
--key-file "$CERT_DIR/$d/key.pem" \
--fullchain-file "$CERT_DIR/$d/cert.pem" \
--reloadcmd "$RELOAD"
echo " installed -> $CERT_DIR/$d/{cert.pem,key.pem}"
issued=$((issued+1))
done
log "done — $issued certificate(s) under $CERT_DIR/"
echo " serve them: point nginx at certs/<name>/ — see nginx/ulicraft.conf.tmpl"
echo " renewals: acme.sh installs a cron; --reloadcmd reloads nginx automatically."