chore: drop the plain-HTTP route now the panel is proven on TLS

The transport log settles it: the device reaches the server over https
and validates the Let's Encrypt chain without trouble. The dedicated
port, the Traefik entrypoint and the plain-HTTP router were all built on
a hypothesis the evidence has since refused.

DEVICE_ALLOW_HTTP goes back to false, so the device routes refuse an
unencrypted request again.

DEPLOY.md is rewritten around the real cause — the firmware does not
follow redirects, and a trailing slash was answered with a 308 — and
records the three hypotheses that were wrong, so nobody spends another
evening on them. It also names the trap that made this slow: Traefik,
a production Next server and tcpdump were each read as saying "no
traffic" when all three were simply silent by default.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012cSY9pVhZmJUKNN7wf1Myd
This commit is contained in:
2026-09-21 23:19:45 +02:00
co-authored by Claude Opus 5
parent 235af90aed
commit 13abcf3242
3 changed files with 67 additions and 81 deletions
+61 -48
View File
@@ -107,62 +107,75 @@ la sauvegarde préalable.
| Messages sans traduction | alerte, page Messages | `npm run translate:test` pour savoir pourquoi |
| Conteneur *unhealthy* | `docker compose ps` | `docker compose logs app` |
## Le panneau tombe sur le certificat TLS
## Le panneau ne se connecte pas
**C'est arrivé sur cette installation, et c'est réglé — voici pourquoi, pour le
jour où ça recommence.**
**C'est arrivé sur cette installation. Le diagnostic a pris une soirée et
trois fausses pistes ; voici ce qu'il faut savoir avant de recommencer.**
Le firmware de l'écran embarque un magasin d'autorités de certification **figé
au moment de sa compilation**. Il ne peut donc pas valider une chaîne qui
s'enracine sur une autorité créée après lui. Let's Encrypt a mis en service la
racine `ISRG Root YR` le 13 mai 2026 ; le firmware du kit est antérieur, et la
poignée de main TLS échoue **avant** qu'une seule requête soit émise — d'où le
symptôme déroutant : l'écran affiche « API connection cannot be established »
et ni Traefik ni l'application n'ont rien vu passer.
### La cause, une fois pour toutes
Diagnostic, dans l'ordre :
Le firmware **ne suit pas les redirections**. Il demande `/api/setup/` avec une
barre oblique finale ; Next répondait `308` pour normaliser, l'écran traitait ce
code comme un échec, n'obtenait jamais de jeton, puis se faisait refuser
`/api/display` en `401` et affichait « API connection cannot be established ».
1. **L'écran a-t-il seulement atteint le serveur ?**
`docker compose logs app --since 30m | grep /api/setup` et
`docker logs traefik --since 30m | grep horaires`.
Si les deux sont vides, l'échec est dans la couche TLS, pas dans l'application.
L'application sert désormais les deux écritures (`skipTrailingSlashRedirect` et
des réécritures dans `next.config.ts`). Si vous ajoutez une route destinée à
l'appareil, **ajoutez-y sa variante avec barre oblique**.
2. **Quelle chaîne est servie, et jusqu'à quelle racine ?**
```bash
echo | openssl s_client -connect horaires.ita-ito.com:443 \
-servername horaires.ita-ito.com 2>/dev/null | grep -E "^ *[0-9] s:"
```
Comparez la racine à ce que le système connaît :
```bash
openssl crl2pkcs7 -nocrl -certfile /etc/ssl/certs/ca-certificates.crt \
| openssl pkcs7 -print_certs -noout | grep "Root YR"
```
Si une machine à jour ne la connaît pas, un firmware de 2025 encore moins.
### Ce qui n'était PAS en cause
3. **Le protocole est-il en cause ?** Souvent soupçonné, rarement coupable :
```bash
echo | openssl s_client -connect horaires.ita-ito.com:443 -tls1_2 | grep "Cipher is"
```
Un ESP32 a besoin de TLS 1.2 et d'une suite `ECDHE-RSA-AES*-GCM`.
Trois hypothèses ont été poursuivies avant la bonne. Elles sont consignées ici
pour que personne ne les reprenne :
Deux corrections possibles :
- **La chaîne TLS.** La racine `ISRG Root YR` date de mai 2026 et n'est pas dans
le magasin d'Ubuntu ; j'en ai conclu qu'un firmware antérieur ne pouvait pas
la valider. **Faux** : le journal montre l'écran arriver en `https` sans
difficulté.
- **Le réseau de la boutique.** Un test depuis un téléphone sur le même Wi-Fi a
montré que rien n'était filtré.
- **Le port.** Un entrypoint Traefik en clair sur `2300` a été mis en place puis
retiré : il n'a jamais servi à rien.
- **Préférer une racine ancienne.** `preferredChain: "ISRG Root X1"` sur le
resolver dans `traefik.yml`, puis renouvellement. `ISRG Root X1` est dans à
peu près tous les magasins depuis 2021. Aucun compromis de sécurité, mais
cela dépend de ce que Let's Encrypt propose encore comme chaîne alternative,
et cela touche le Traefik partagé par tous les sites.
### Pourquoi c'était si long : les outils silencieux
- **Servir l'écran en clair** — ce qui est fait ici. Le routeur
`horaires-device` dans `docker-compose.prod.yml` expose sur `:80` les seules
routes `/api/setup`, `/api/display`, `/api/log` et `/api/device/`, avec une
priorité qui passe devant la redirection HTTP→HTTPS générale. L'administration
reste en TLS. Le jeton d'appareil circule alors en clair : il ne sert à rien
d'autre et se révoque depuis *Paramètres → Appareils*.
Trois fois de suite, un outil muet a été lu comme un diagnostic négatif.
L'application **refuse** une requête d'appareil non chiffrée tant que
`DEVICE_ALLOW_HTTP=true` n'est pas posé : ouvrir cette porte est une décision
écrite, pas la conséquence silencieuse d'une configuration de proxy.
| Outil | Piège |
|---|---|
| Traefik | ne journalise pas les accès par défaut |
| Next.js en production | ne journalise pas les requêtes |
| `tcpdump` | met sa sortie en tampon sans `-l` : fichier vide malgré du trafic |
Le jour où le firmware apprend les racines récentes, retirez le routeur
`horaires-device` et remettez `DEVICE_ALLOW_HTTP=false`.
**Et l'écran disait la réponse depuis le début.** Il envoyait son rapport
d'erreur sur `/api/log` — sans jeton, puisqu'il n'avait jamais réussi à
s'appairer — et la route répondait `401`. Un appareil incapable de
s'authentifier est précisément celui qu'il faut écouter : ces journaux sont
maintenant acceptés et écrits dans les logs du serveur.
### Le diagnostic, dans l'ordre
```bash
# 1. Par quel transport l'écran arrive-t-il, et arrive-t-il ?
docker compose logs app --since 3h | grep "\[device\]"
# 2. Que dit l'écran lui-même ?
docker compose logs app --since 3h | grep "appareil non authentifié"
# 3. Les deux écritures répondent-elles sans redirection ?
for p in /api/setup /api/setup/ /api/display /api/display/; do
curl -s -o /dev/null -w "$p -> %{http_code} %{redirect_url}\n" \
"https://horaires.ita-ito.com$p"
done
```
Un `308` à l'étape 3 est la panne d'origine qui revient.
### Si le TLS est réellement en cause un jour
Le firmware valide aujourd'hui la chaîne Let's Encrypt. Si une racine future
lui échappait vraiment, deux voies : `preferredChain: "ISRG Root X1"` sur le
resolver dans `traefik.yml`, ou un entrypoint en clair réservé aux quatre
routes de l'appareil. Dans ce second cas, `DEVICE_ALLOW_HTTP=true` est
nécessaire : l'application refuse une requête d'appareil non chiffrée sans
cette décision écrite.