---
title: "Incident Caddy — lucien.best offline (vhosts perdus) + prep zevent.vfg.lol"
subtitle: "Post-mortem 2026-09-05 : TLS alert internal error alors que Node répondait encore sur :3000. Cause : Caddyfile du 8 juillet sans import Lucien. Fix + règles anti-régression pour ne plus écraser les blocs no-touch."
date: 2026-09-05
time: "12:25"
author: VFG / Cursor
status: pub
tags: ["handoff", "incident", "prod", "caddy", "lucien", "tls", "vps", "zevent", "ops", "2026-09-05"]
---

## TL;DR

Le 2026-09-05, `https://lucien.best` était **injoignable en HTTPS** (`tlsv1 alert internal error` / `SEC_E_ILLEGAL_MESSAGE`) alors que :

- le DNS `A` pointait bien vers le VPS `51.77.244.222` ;
- `lucien-site.service` était **actif** ;
- `http://127.0.0.1:3000/` renvoyait **200**.

**Cause racine :** le `/etc/caddy/Caddyfile` modifié le **2026-07-08** ne contenait plus que :

```caddy
{
	admin off
}
import /etc/caddy/vfg-fleet.caddy
import /etc/caddy/vfg-admin.caddy
```

Les blocs `lucien.best` / `www.lucien.best` (historiquement no-touch) avaient **disparu**. Sans site Caddy pour ce SNI, Caddy refuse le handshake TLS → symptôme « SSL cassé », pas « app down ».

**Fix :** restaurer un snippet `/etc/caddy/lucien.caddy` + `systemctl restart caddy` (pas `reload` : voir plus bas). Certificat Let's Encrypt réémis le jour même. Smoke : `lucien.best` / `hero.vfg.lol` / `msn.vfg.lol` / `blog.vfg.lol` en 200.

**Bonus session :** DNS `zevent.vfg.lol` déjà prêt → déploiement ZDASH (`fg-zevent`, `:5200`, vhost Caddy) live sur `https://zevent.vfg.lol`.

---

## Chronologie

| Heure (approx, CEST) | Événement |
|----------------------|-----------|
| ~12:09 | Signal FG : domaine `zevent.vfg.lol` ajouté ; `lucien.best` ne marche plus |
| ~12:10 | Probes Windows : DNS OK pour lucien + zevent ; HTTPS échoue ; `hero.vfg.lol` OK |
| ~12:12 | SSH `debian@` : app Lucien OK en local ; openssl SNI `lucien.best` → *no peer certificate* |
| ~12:13 | Lecture Caddyfile : **aucun** `lucien` dans `/etc/caddy/` actif |
| ~12:14 | Source intacte trouvée : `/opt/lucien/deploy/Caddyfile` |
| ~12:17 | Tentative `systemctl reload caddy` → **échec** (`admin off` → API `:2019` refusée) ; process Caddy **reste** sur l'ancienne config (fleet intact) |
| ~12:19 | `systemctl restart caddy` → cert LE pour `lucien.best` ; HTTPS 200 |
| ~12:20–12:21 | Upload + build `fg-zevent`, compose up, import `zevent.caddy`, restart Caddy → `zevent.vfg.lol` 200 |

---

## Symptômes (ce que ça donnait vraiment)

### Côté client

```text
curl https://lucien.best/
→ curl: (35) ... tlsv1 alert internal error

# Windows / schannel
SEC_E_ILLEGAL_MESSAGE (0x80090326)
```

HTTP clair répondait encore avec un redirect Caddy vers HTTPS :

```text
curl -I http://lucien.best/
→ 308 Location: https://lucien.best/
```

Donc « le domaine répond » en HTTP, mais **tout navigateur moderne casse** au TLS.

### Côté VPS (preuves)

| Check | Résultat |
|-------|----------|
| `systemctl is-active lucien-site` | active |
| `ss` `:3000` | `127.0.0.1:3000` LISTEN |
| `curl http://127.0.0.1:3000/` | 200 |
| `curl -k https://lucien.best/` | échec TLS (pas de cert pour le SNI) |
| `openssl s_client -servername lucien.best` | *no peer certificate available*, alert 80 |
| `openssl s_client -servername hero.vfg.lol` | chaîne LE valide (YE2) |
| `grep -R lucien /etc/caddy/*.caddy` (actifs) | **vide** |

Signature diagnostique à retenir :

> **App up + DNS OK + TLS alert sans certificat peer = vhost Caddy manquant (ou SNI non servi), pas un crash Node.**

---

## Cause racine détaillée

### 1. Perte des vhosts Lucien au 2026-07-08

`stat /etc/caddy/Caddyfile` :

- Modify / Birth : **2026-07-08 00:34**
- Contenu réduit à `admin off` + imports fleet/admin

Backup de session du même jour (`/opt/vfg-fleet/backups/session-20260708-010154/Caddyfile`) : **même contenu minimal** — la perte est déjà dans l'historique ops de ce jour-là, pas une corruption du 5 septembre.

La doc fleet (`VFVFGPT/deploy/vps/README.md`) exigeait explicitement :

> Ne pas toucher aux blocs `lucien.best` / `stats.lucien.best` dans `/etc/caddy/Caddyfile`.

Le pattern d'origine (`setup-vps-host.sh`) était : **ajouter un import fleet au Caddyfile Lucien existant**, pas remplacer le Caddyfile par « fleet only ».

### 2. Pourquoi `hero.*` marchait encore

Les vhosts `*.vfg.lol` vivent dans `/etc/caddy/vfg-fleet.caddy`, toujours importé. Lucien n'y a **jamais** dû être : il est hors fleet (systemd natif `/opt/lucien`, no-touch). Quand on « nettoie » le Caddyfile principal pour ne garder que les imports fleet, on coupe Lucien sans toucher au reste — d'où l'illusion que « Caddy va bien ».

### 3. Piège ops : `admin off` ⇒ `reload` impossible

Le Caddyfile commence par :

```caddy
{
	admin off
}
```

Conséquence :

```text
caddy reload --config /etc/caddy/Caddyfile
→ Error: Post "http://localhost:2019/load": dial tcp [::1]:2019: connect: connection refused
```

`systemctl reload caddy` échoue (ExecReload = `caddy reload`), **mais le daemon continue** avec l'ancienne config en mémoire. C'est trompeur : on croit « reload KO = tout cassé », alors que la fleet reste servie.

**Sur ce VPS, toute évolution Caddyfile exige `systemctl restart caddy`**, avec une courte coupure TLS globale.

Drop-in utile à connaître : `/etc/systemd/system/caddy.service.d/no-resume.conf` force `caddy run --config /etc/caddy/Caddyfile` (pas de resume state).

---

## Correctif appliqué (rejouable)

Ops via **`debian@51.77.244.222`** (sudo NOPASSWD). Le user `deploy` est dans `docker` mais **sans** sudo passwordless et **sans** écriture sur `/opt/vfg-fleet` / `/etc/caddy`.

### A. Restaurer Lucien (sans stats GoatCounter)

GoatCounter / `:8088` était inactif → on n'a **pas** remis `stats.lucien.best` (évite un vhost ACME + 502 inutile). Source de vérité app : `/opt/lucien/deploy/Caddyfile`.

Fichier créé : `/etc/caddy/lucien.caddy`

```caddy
lucien.best, www.lucien.best {
	encode gzip zstd
	reverse_proxy 127.0.0.1:3000

	header {
		X-Content-Type-Options nosniff
		X-Frame-Options DENY
		X-XSS-Protection "1; mode=block"
		Referrer-Policy strict-origin-when-cross-origin
		Permissions-Policy "interest-cohort=()"
		X-Permitted-Cross-Domain-Policies none
		-Server
	}

	@images path /images/*
	header @images Cache-Control "public, max-age=2592000"

	log {
		output file /var/log/caddy/lucien.log {
			roll_size 20mb
			roll_keep 5
		}
	}
}
```

Caddyfile principal :

```caddy
{
	admin off
}

import /etc/caddy/lucien.caddy
import /etc/caddy/vfg-fleet.caddy
import /etc/caddy/vfg-admin.caddy
```

Puis :

```bash
sudo cp -a /etc/caddy/Caddyfile /etc/caddy/Caddyfile.bak-pre-lucien-restore-<TS>
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl restart caddy   # PAS reload
# smoke
curl -sS -o /dev/null -w "%{http_code}\n" https://lucien.best/
curl -sS -o /dev/null -w "%{http_code}\n" https://hero.vfg.lol/
```

Preuve cert post-fix :

```text
subject=CN = lucien.best
notBefore=Sep  5 09:21:15 2026 GMT
notAfter=Dec  4 09:21:14 2026 GMT
```

### B. Prep / go-live `zevent.vfg.lol` (même session)

Prérequis DNS déjà là : `zevent.vfg.lol A → 51.77.244.222` (pas de wildcard `*.vfg.lol`).

1. Tarball `zevent-dash` → `/opt/vfg-fleet/repo/zevent-dash/`
2. `docker build -t fg-zevent:latest …`
3. `compose/t3-zevent.yml` + `env/zevent.env` (`ADMIN_TOKEN` généré sur le VPS, **hors git**)
4. `docker compose … up -d` → `vfg-fleet-zevent-1` sur `127.0.0.1:5200`
5. Snippet `/etc/caddy/zevent.caddy` + import dans le Caddyfile
6. `restart caddy` → ACME + reverse_proxy

Healthz public (extrait) : `{"ok":true,…,"goals":{"participations":338,…}}`.

---

## Règles anti-régression (pour ne plus reproduire)

### Checklist avant toute édition Caddy sur ce VPS

1. **Backup horodaté** de `/etc/caddy/Caddyfile` et des snippets touchés.
2. **`grep -n lucien /etc/caddy/Caddyfile /etc/caddy/*.caddy`** — doit rester non vide tant que Lucien est hébergé ici.
3. **Ne jamais remplacer** le Caddyfile par « imports fleet only ». Pattern correct : *Lucien (ou snippets no-touch) + import fleet + import admin*.
4. **`caddy validate`** avant application.
5. Appliquer avec **`systemctl restart caddy`**, pas `reload`, tant que `admin off` est en place.
6. Smoke **obligatoire** après coup, dans cet ordre :
   - `https://lucien.best/`
   - `https://hero.vfg.lol/`
   - `https://msn.vfg.lol/`
   - `https://blog.vfg.lol/`
   - le nouveau vhost le cas échéant
7. Si un nouveau sous-domaine `*.vfg.lol` est ajouté en DNS : **créer le vhost Caddy le même jour** (sinon même symptôme TLS que Lucien / zevent avant prep).

### Ce qu'il ne faut pas faire

| Anti-pattern | Pourquoi ça fait mal |
|--------------|----------------------|
| « On simplifie le Caddyfile, Lucien est documenté no-touch donc ça ira » | No-touch n'est pas magique : si le bloc n'est plus importé, il est mort |
| Se fier à `systemctl reload` alors que `admin off` | Échec silencieux pour l'opérateur ; ancienne config reste en RAM |
| Diagnostiquer « HTTPS down » en ne testant que l'app locale | On rate 100 % des pannes edge |
| Utiliser `deploy@` pour éditer `/etc/caddy` | Pas de sudo NOPASSWD → scripts qui pendent sur mot de passe |
| Remettre `stats.lucien.best` sans GoatCounter | ACME + reverse_proxy vers `:8088` mort = bruit et 502 |
| `docker compose … --remove-orphans` en lançant seulement `t3-zevent.yml` | Risque de cueillir des « orphans » qui sont en fait le reste de la fleet |

### Structure cible recommandée (modulaire)

```text
/etc/caddy/Caddyfile          # global options + imports UNIQUEMENT
/etc/caddy/lucien.caddy       # no-touch mémorial
/etc/caddy/vfg-fleet.caddy    # *.vfg.lol applicatifs
/etc/caddy/vfg-admin.caddy    # surfaces admin
/etc/caddy/zevent.caddy       # ZDASH (éphémère / événement)
```

Leçon Citadelle (déjà dans les commentaires VPS, 2026-06-30) : **snippets séparés = récupération rapide**. Même leçon pour Lucien.

---

## Accès SSH ops (rappel)

| User | Usage | sudo |
|------|-------|------|
| `debian` | edits Caddy, `/opt/vfg-fleet`, systemctl | NOPASSWD |
| `deploy` | docker group, CI éventuelle | mot de passe requis ; peu d'écriture fleet |

Alias local utile : `Host vfg-vps` → `deploy@51.77.244.222` (identité `~/.ssh/id_ed25519`). Pour Caddy : préférer explicitement `debian@51.77.244.222`.

---

## État final vérifié (2026-09-05 ~12:21 CEST)

| URL | Code | Note |
|-----|------|------|
| `https://lucien.best/` | 200 | cert LE frais |
| `https://zevent.vfg.lol/` | 200 | ZDASH |
| `https://zevent.vfg.lol/healthz` | 200 | JSON ok |
| `https://hero.vfg.lol/` | 200 | non régressé |
| `https://blog.vfg.lol/` | 200 | non régressé |
| `lucien-site.service` | active | inchangé |
| `caddy.service` | active | restart appliqué |

Hors scope volontaire : restauration `stats.lucien.best` (GoatCounter), durcissement sudo `deploy`, réactivation API admin Caddy (`admin off`).

---

## Liens utiles

- Source Caddy Lucien : `/opt/lucien/deploy/Caddyfile` (repo `Lucien/deploy/`)
- Doc fleet no-touch : `VFVFGPT/deploy/vps/README.md` § « Ce qu'il ne faut pas toucher »
- Runbook ZDASH : `Zevent2026/zevent-dash/README.md` § déploiement VPS
- Snippet Caddy ZDASH : `zevent-dash/caddy-snippet.txt`