tooling/issue-letsencrypt.sh: manual script, issues one cert per name (apex + LE_SUBDOMAINS) with acme.sh + OVH DNS-01 (no inbound ports), installs to certs/<name>/ and reloads nginx. nginx/ulicraft.conf.tmpl: TLS vhosts for apex/pack (static) + auth (proxy to drasl). OVH creds and LE_EMAIL in .env.example; certs/ gitignored. plan/15-letsencrypt.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
102 lines
4.2 KiB
Bash
Executable File
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")
|
|
# 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}"
|
|
|
|
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."
|