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:
+3
-2
@@ -63,8 +63,9 @@ DEVICE_IMAGE_FORMAT=bmp
|
|||||||
# « true » : c'est une décision explicite, pas un effet de bord d'une
|
# « true » : c'est une décision explicite, pas un effet de bord d'une
|
||||||
# configuration de proxy.
|
# configuration de proxy.
|
||||||
#
|
#
|
||||||
# À activer quand le firmware ESP32 ne peut pas valider la chaîne TLS — le cas
|
# À activer seulement si le firmware ESP32 s'avère incapable de valider la
|
||||||
# lorsque la racine Let's Encrypt est plus récente que le firmware lui-même.
|
# 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
|
# Le jeton d'appareil circule alors en clair : il ne sert à rien d'autre et se
|
||||||
# révoque depuis /admin/parametres. Voir DEPLOY.md.
|
# révoque depuis /admin/parametres. Voir DEPLOY.md.
|
||||||
DEVICE_ALLOW_HTTP=false
|
DEVICE_ALLOW_HTTP=false
|
||||||
|
|||||||
@@ -107,62 +107,75 @@ la sauvegarde préalable.
|
|||||||
| Messages sans traduction | alerte, page Messages | `npm run translate:test` pour savoir pourquoi |
|
| Messages sans traduction | alerte, page Messages | `npm run translate:test` pour savoir pourquoi |
|
||||||
| Conteneur *unhealthy* | `docker compose ps` | `docker compose logs app` |
|
| 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
|
**C'est arrivé sur cette installation. Le diagnostic a pris une soirée et
|
||||||
jour où ça recommence.**
|
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é
|
### La cause, une fois pour toutes
|
||||||
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.
|
|
||||||
|
|
||||||
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 ?**
|
L'application sert désormais les deux écritures (`skipTrailingSlashRedirect` et
|
||||||
`docker compose logs app --since 30m | grep /api/setup` et
|
des réécritures dans `next.config.ts`). Si vous ajoutez une route destinée à
|
||||||
`docker logs traefik --since 30m | grep horaires`.
|
l'appareil, **ajoutez-y sa variante avec barre oblique**.
|
||||||
Si les deux sont vides, l'échec est dans la couche TLS, pas dans l'application.
|
|
||||||
|
|
||||||
2. **Quelle chaîne est servie, et jusqu'à quelle racine ?**
|
### Ce qui n'était PAS en cause
|
||||||
```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.
|
|
||||||
|
|
||||||
3. **Le protocole est-il en cause ?** Souvent soupçonné, rarement coupable :
|
Trois hypothèses ont été poursuivies avant la bonne. Elles sont consignées ici
|
||||||
```bash
|
pour que personne ne les reprenne :
|
||||||
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`.
|
|
||||||
|
|
||||||
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
|
### Pourquoi c'était si long : les outils silencieux
|
||||||
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
|
Trois fois de suite, un outil muet a été lu comme un diagnostic négatif.
|
||||||
`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
|
| Outil | Piège |
|
||||||
`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.
|
| 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
|
**Et l'écran disait la réponse depuis le début.** Il envoyait son rapport
|
||||||
`horaires-device` et remettez `DEVICE_ALLOW_HTTP=false`.
|
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.
|
||||||
|
|||||||
+3
-31
@@ -38,40 +38,12 @@ services:
|
|||||||
traefik.http.routers.horaires.tls.certresolver: ${TRAEFIK_CERTRESOLVER:-myresolver}
|
traefik.http.routers.horaires.tls.certresolver: ${TRAEFIK_CERTRESOLVER:-myresolver}
|
||||||
traefik.http.routers.horaires.middlewares: horaires-hsts
|
traefik.http.routers.horaires.middlewares: horaires-hsts
|
||||||
traefik.http.services.horaires.loadbalancer.server.port: "3010"
|
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.stsSeconds: "31536000"
|
||||||
traefik.http.middlewares.horaires-hsts.headers.stsIncludeSubdomains: "true"
|
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:
|
backup:
|
||||||
# A nightly dump kept for two weeks. Small, boring, and the only thing
|
# A nightly dump kept for two weeks. Small, boring, and the only thing
|
||||||
|
|||||||
Reference in New Issue
Block a user