Files
ita-ito-horaires/DEPLOY.md
T
vliaudatandClaude Opus 5 eae3f89aca fix: let the panel reach the API over plain HTTP, deliberately
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. Let's Encrypt's ISRG Root YR was issued in May 2026 and is
not even in an up-to-date Ubuntu CA bundle yet; the kit's firmware
predates it. The handshake fails before a request is ever sent, which is
why neither Traefik nor the application saw anything at all while the
device reported "API connection cannot be established".

Ruled out first, with evidence: TLS 1.2 and the ECDHE-RSA-AES-GCM suites
an ESP32 needs are both offered, and the intermediate is not
cross-signed by an older root, so no alternate path exists in what is
served.

A Traefik router now serves four device paths over :80, ahead of the
entrypoint-wide redirect. The administration stays on TLS. The device
token travels in clear; it is used for nothing else and is revocable
from the settings page, and the image URL is an unguessable content hash.

DEVICE_ALLOW_HTTP existed but was never read — a setting that does
nothing misrepresents what it protects. The device routes now refuse an
unencrypted request unless it is set, so opening this door is a written
decision rather than the silent consequence of a proxy change.

DEPLOY.md records the whole diagnosis, including the commands that
distinguish a TLS failure from an application one, and what to do the
day the firmware learns the new roots.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012cSY9pVhZmJUKNN7wf1Myd
2026-09-21 22:17:16 +02:00

6.9 KiB

Déploiement

Procédures d'exploitation pour horaires.ita-ito.com. Le README couvre l'installation locale et la configuration d'Authentik.

Première installation sur le VPS

Prérequis : Docker, Docker Compose, et un Traefik déjà en route possédant le réseau externe nommé par TRAEFIK_NETWORK.

git clone <dépôt> /opt/horaires && cd /opt/horaires

cp .env.example .env
sed -i "s|^AUTH_SECRET=.*|AUTH_SECRET=$(openssl rand -base64 33)|" .env
sed -i "s|^POSTGRES_PASSWORD=.*|POSTGRES_PASSWORD=$(openssl rand -base64 24)|" .env
# Renseignez APP_DOMAIN, AUTH_URL, AUTH_AUTHENTIK_*, TRANSLATION_API_KEY.

mkdir -p backups
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --build --wait

Les migrations s'appliquent au démarrage. Si elles échouent, le conteneur s'arrête avant de servir quoi que ce soit, plutôt que d'exposer une base incohérente : docker compose logs app dira pourquoi.

Ensuite :

docker compose exec app node .migrator/node_modules/prisma/build/index.js migrate status
curl -fsS https://horaires.ita-ito.com/api/health

Puis, une fois connecté : Paramètres pour le canton et les intervalles de réveil, Fériés → Synchroniser maintenant, Horaires pour la vraie semaine, et enfin l'appairage de l'écran (README).

Mise à jour

cd /opt/horaires
./scripts/backup.sh                 # ou attendre le dump nocturne
git pull
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --build --wait
docker compose logs -f app          # ^C une fois « démarrage de l'application »

--wait rend la main seulement quand le healthcheck passe. S'il ne passe pas, la mise à jour a échoué et l'ancien conteneur a déjà été remplacé : voir le retour arrière ci-dessous.

Avant une mise à jour qui touche au rendu (polices, gabarit, logo), sachez que le nom de fichier de l'image change et que tous les écrans redessineront une fois. C'est sans gravité, seulement un cycle de batterie.

Sauvegarde

Le service backup dépose un dump compressé par jour dans ./backups et purge au-delà de BACKUP_KEEP_DAYS (14 par défaut).

Dump immédiat :

docker compose exec -T db pg_dump -U horaires horaires | gzip > backups/horaires-$(date +%Y%m%d-%H%M%S).sql.gz

Ces fichiers contiennent les horaires, les messages et le journal d'audit. Ils ne contiennent aucun secret : les jetons d'appareil n'y figurent que sous forme d'empreintes, et les identifiants Authentik vivent dans .env, qui n'est pas dans les sauvegardes. Sauvegardez .env séparément, ailleurs.

Restauration

docker compose -f docker-compose.yml -f docker-compose.prod.yml stop app
gunzip -c backups/horaires-20260920-030000.sql.gz \
  | docker compose exec -T db psql -U horaires -d horaires
docker compose -f docker-compose.yml -f docker-compose.prod.yml start app

Après une restauration, les écrans appairés depuis le dump ne le sont plus du point de vue de la base : ils recevront un 401 et devront être réappairés par le portail captif. Rien d'autre ne se perd.

Retour arrière

L'image porte la version du dépôt, donc revenir en arrière veut dire revenir au commit :

git log --oneline -10
git checkout <commit-précédent>
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --build --wait

Si la mise à jour comportait une migration, revenir au code ne suffit pas : le schéma est déjà migré. Prisma ne défait pas une migration. Restaurez alors le dump pris avant la mise à jour, puis reprenez le code. C'est la raison d'être de la sauvegarde préalable.

Ce qu'il faut surveiller

Signe Où Que faire
L'écran n'a pas donné signe de vie alerte du tableau de bord batterie, puis Wi-Fi de la boutique
Synchro des fériés en échec alerte, page Fériés le cache local tient ; relancer à la main
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

C'est arrivé sur cette installation, et c'est réglé — voici pourquoi, pour le jour où ça recommence.

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.

Diagnostic, dans l'ordre :

  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.

  2. Quelle chaîne est servie, et jusqu'à quelle racine ?

    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 :

    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 :

    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 :

  • 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.