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
+3 -2
View File
@@ -63,8 +63,9 @@ DEVICE_IMAGE_FORMAT=bmp
# « true » : c'est une décision explicite, pas un effet de bord d'une
# configuration de proxy.
#
# À activer quand le firmware ESP32 ne peut pas valider la chaîne TLS — le cas
# lorsque la racine Let's Encrypt est plus récente que le firmware lui-même.
# À activer seulement si le firmware ESP32 s'avère incapable de valider la
# chaîne TLS. Celui du kit Seeed (1.5.12) y parvient sans problème : ne pas
# supposer le contraire devant une panne de connexion, voir DEPLOY.md.
# Le jeton d'appareil circule alors en clair : il ne sert à rien d'autre et se
# révoque depuis /admin/parametres. Voir DEPLOY.md.
DEVICE_ALLOW_HTTP=false
+62 -49
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**.
### Ce qui n'était PAS en cause
Trois hypothèses ont été poursuivies avant la bonne. Elles sont consignées ici
pour que personne ne les reprenne :
- **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.
### Pourquoi c'était si long : les outils silencieux
Trois fois de suite, un outil muet a été lu comme un diagnostic négatif.
| 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 |
**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
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:"
# 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
```
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.
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`.
Un `308` à l'étape 3 est la panne d'origine qui revient.
Deux corrections possibles :
### Si le TLS est réellement en cause un jour
- **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.
- **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*.
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.
Le jour où le firmware apprend les racines récentes, retirez le routeur
`horaires-device` et remettez `DEVICE_ALLOW_HTTP=false`.
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.
+3 -31
View File
@@ -38,40 +38,12 @@ services:
traefik.http.routers.horaires.tls.certresolver: ${TRAEFIK_CERTRESOLVER:-myresolver}
traefik.http.routers.horaires.middlewares: horaires-hsts
traefik.http.services.horaires.loadbalancer.server.port: "3010"
# The admin is only ever served over TLS; say so to the browsers.
# Everything is served over TLS, the panel included: its firmware
# validates the chain without trouble. A plain-HTTP route existed here
# for a while, on the belief that it could not — see DEPLOY.md.
traefik.http.middlewares.horaires-hsts.headers.stsSeconds: "31536000"
traefik.http.middlewares.horaires-hsts.headers.stsIncludeSubdomains: "true"
# --- The panel, in clear, on four paths only ---
#
# The e-ink firmware carries a certificate-authority bundle fixed when it
# was built, so it cannot validate a chain rooted in an authority created
# afterwards — which is exactly the case with Let's Encrypt's ISRG Root YR
# (May 2026). The handshake fails before a request is ever sent, which is
# why neither Traefik nor the application sees anything at all.
#
# This router therefore serves the four device paths over plain HTTP. The
# administration stays on TLS. The trade-off is real and bounded: the
# device token travels in clear, it is used for nothing else, and it can
# be revoked from Paramètres → Appareils. The image URL is an unguessable
# content hash.
#
# It listens on its own entrypoint rather than on :80. Traefik applies an
# entrypoint's HTTP→HTTPS redirection before routing, so no router
# priority can escape it — the port has to be one that never redirects.
# That also makes the restriction structural: nothing but these four
# paths is reachable on 2300, and it is Traefik that guarantees it rather
# than application code.
#
# Requires the matching `device` entrypoint in the shared traefik.yml.
# Remove this block the day the firmware learns the new roots, and set
# DEVICE_ALLOW_HTTP=false — the application refuses plain requests
# without it.
traefik.http.routers.horaires-device.rule: >-
Host(`${APP_DOMAIN}`) && (PathPrefix(`/api/setup`) || PathPrefix(`/api/display`)
|| PathPrefix(`/api/log`) || PathPrefix(`/api/device/`))
traefik.http.routers.horaires-device.entrypoints: device
traefik.http.routers.horaires-device.service: horaires
backup:
# A nightly dump kept for two weeks. Small, boring, and the only thing