Files
EpicNext-Cms/infra/traefik/regenerate-epicnabbo-chain.sh
T
openhands 8292cc8ffb
Local Build and Deploy / deploy (push) Successful in 1m2s
fix(infra): serve full TLS chain for epicnabbo.nl to resolve Cloudflare 525
Traefik's ACME resolver stored the epicnabbo.nl leaf certificate without
the Let's Encrypt intermediate. With Cloudflare in Full (strict) mode the
origin TLS handshake failed (HTTP 525), breaking every asset and the whole
site layout (JS/CSS chunks, Nitro client, toolbar).

Configure the epicnabbo router to use a file-based certificate that
includes the full chain (leaf + LE YR2 intermediate), referenced from
dynamic/epicnabbo-tls.yml. Add a regeneration script and README.
2026-07-18 18:37:56 +02:00

66 lines
2.5 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# regenerate-epicnabbo-chain.sh
#
# Builds a complete TLS chain (leaf + Let's Encrypt intermediate) for
# epicnabbo.nl and writes it next to the Traefik dynamic config so the
# origin presents a full chain to Cloudflare.
#
# Why: Traefik's ACME resolver stored the leaf certificate in
# /letsencrypt/acme.json without the issuing intermediate. When Cloudflare
# talks to the origin in "Full (strict)" mode it cannot build the chain and
# returns HTTP 525 (SSL handshake failed), which broke every asset on the
# site (JS/CSS chunks, the Nitro client, etc.). Serving the full chain from
# a file-based certificate fixes the handshake.
#
# Usage (run on the host as root):
# ./infra/traefik/regenerate-epicnabbo-chain.sh
#
# The generated files (epicnabbo-fullchain.pem / epicnabbo-key.pem) contain
# the private key and MUST NOT be committed to git.
set -euo pipefail
TRAEFIK_DYNAMIC="/docker/proxyserver/dynamic"
ACME_JSON="/docker/proxyserver/letsencrypt/acme.json"
INTERMEDIATE_URL="http://yr2.i.lencr.org/"
if [ ! -f "$ACME_JSON" ]; then
echo "acme.json not found at $ACME_JSON" >&2
exit 1
fi
WORK="$(mktemp -d)"
trap 'rm -rf "$WORK"' EXIT
# Extract the epicnabbo.nl leaf certificate + private key from acme.json.
docker exec traefik cat /letsencrypt/acme.json 2>/dev/null > "$WORK/acme.json"
python3 - "$WORK/acme.json" "$WORK/leaf.pem" "$WORK/key.pem" <<'PY'
import sys, json, base64
path, leaf_out, key_out = sys.argv[1], sys.argv[2], sys.argv[3]
data = json.load(open(path))
found = False
for _resolver, v in data.items():
for c in (v.get("Certificates") or []):
if c.get("domain", {}).get("main") == "epicnabbo.nl":
open(leaf_out, "wb").write(base64.b64decode(c["certificate"]))
open(key_out, "wb").write(base64.b64decode(c["key"]))
found = True
break
if found:
break
if not found:
sys.exit("epicnabbo.nl certificate not found in acme.json")
PY
# Fetch the Let's Encrypt YR2 intermediate (issuer of the leaf).
curl -fsSL "$INTERMEDIATE_URL" -o "$WORK/intermediate.der"
openssl x509 -inform der -in "$WORK/intermediate.der" -out "$WORK/intermediate.pem"
# Assemble leaf + intermediate into a full chain.
cat "$WORK/leaf.pem" "$WORK/intermediate.pem" > "$TRAEFIK_DYNAMIC/epicnabbo-fullchain.pem"
cp "$WORK/key.pem" "$TRAEFIK_DYNAMIC/epicnabbo-key.pem"
chmod 644 "$TRAEFIK_DYNAMIC/epicnabbo-fullchain.pem" "$TRAEFIK_DYNAMIC/epicnabbo-key.pem"
echo "Regenerated full chain at $TRAEFIK_DYNAMIC/epicnabbo-fullchain.pem"
echo "Restart Traefik for the change to take effect."