diff --git a/infra/traefik/.gitignore b/infra/traefik/.gitignore deleted file mode 100644 index af2309bd..00000000 --- a/infra/traefik/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -# Secrets / generated TLS material — never commit these. -epicnabbo-fullchain.pem -epicnabbo-key.pem -*.pem -*.key diff --git a/infra/traefik/README.md b/infra/traefik/README.md deleted file mode 100644 index d7e33ad0..00000000 --- a/infra/traefik/README.md +++ /dev/null @@ -1,42 +0,0 @@ -# Traefik infrastructure (epicnabbo.nl) - -This directory mirrors the live Traefik dynamic configuration used to proxy -`epicnabbo.nl` (and subdomains) on the production host. The dynamic config -lives on the server at `/docker/proxyserver/dynamic/`. - -## Fix: HTTP 525 / broken layout (origin TLS chain) - -The site returned **HTTP 525 (Cloudflare SSL handshake failed)** for every -asset, which broke the whole UI (JS/CSS chunks, the Nitro client, the -toolbar, the "Enter Hotel" button, etc.). - -Root cause: Traefik's ACME resolver stored the `epicnabbo.nl` leaf -certificate in `/letsencrypt/acme.json` **without** the Let's Encrypt -intermediate. When Cloudflare connects to the origin in "Full (strict)" mode -it cannot build the certificate chain and aborts the TLS handshake → 525. - -Fix: the `epicnabbo` router serves a **file-based certificate** that includes -the full chain (leaf + LE YR2 intermediate), configured in -`dynamic/epicnabbo-tls.yml` and referenced from `dynamic/epicnabbo.nl.yml` -(`tls: {}` enables TLS on the router so the SNI matches the file cert). - -### Files - -- `dynamic/epicnabbo.nl.yml` — router/service/serversTransport for epicnabbo.nl. -- `dynamic/epicnabbo-tls.yml` — file-based certificate (leaf + intermediate). -- `regenerate-epicnabbo-chain.sh` — rebuilds the full chain from `acme.json`. - -### NOT committed (contain secrets) - -- `epicnabbo-fullchain.pem` — leaf + intermediate. -- `epicnabbo-key.pem` — private key. - -### Re-generating the chain (e.g. after cert renewal, before 2026-10-03) - -```bash -./infra/traefik/regenerate-epicnabbo-chain.sh -docker restart traefik -``` - -The `epicnabbo.nl` entry must be **absent** from `acme.json` so Traefik does -not prefer the (incomplete-chain) ACME certificate over the file certificate. diff --git a/infra/traefik/dynamic/epicnabbo-tls.yml b/infra/traefik/dynamic/epicnabbo-tls.yml deleted file mode 100644 index b1ef9bcd..00000000 --- a/infra/traefik/dynamic/epicnabbo-tls.yml +++ /dev/null @@ -1,4 +0,0 @@ -tls: - certificates: - - certFile: /etc/traefik/dynamic/epicnabbo-fullchain.pem - keyFile: /etc/traefik/dynamic/epicnabbo-key.pem diff --git a/infra/traefik/dynamic/epicnabbo.nl.yml b/infra/traefik/dynamic/epicnabbo.nl.yml deleted file mode 100644 index 6b9b4c08..00000000 --- a/infra/traefik/dynamic/epicnabbo.nl.yml +++ /dev/null @@ -1,22 +0,0 @@ -http: - routers: - epicnabbo: - entryPoints: - - websecure - rule: "Host(`epicnabbo.nl`) || Host(`www.epicnabbo.nl`)" - service: epicnabbo-svc - middlewares: - - default-security-headers - tls: {} - - services: - epicnabbo-svc: - loadBalancer: - passHostHeader: true - serversTransport: epicnabbo-transport - servers: - - url: "https://172.21.0.1:9443" - - serversTransports: - epicnabbo-transport: - insecureSkipVerify: true diff --git a/infra/traefik/regenerate-epicnabbo-chain.sh b/infra/traefik/regenerate-epicnabbo-chain.sh deleted file mode 100755 index 1f725bcb..00000000 --- a/infra/traefik/regenerate-epicnabbo-chain.sh +++ /dev/null @@ -1,65 +0,0 @@ -#!/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."