Héberger sa propre instance
Trois conteneurs derrière un reverse proxy HTTPS. Aucun mot de passe n'est stocké : le SSO de l'école est rejoué à chaque connexion, les cookies PASS vivent dans Redis.
Le dépôt ne contient rien de l'instance publique : domaine, admins, secrets,
tout passe par le .env.
Prérequis
- Docker avec Compose 2.24 ou plus récent
- Un domaine et un reverse proxy TLS. Les cookies sont
Secure: en HTTP la connexion ne tient pas. - Un compte élève IMT Atlantique valide.
- ~1 Go de RAM, 2 Go si on compile l'image sur place.
Démarrer
git clone https://git.az4aaz.xyz/az4aaz/paff.git
cd paff
cp .env.example .env && $EDITOR .env
docker compose up -d
docker compose up -d --build compile depuis les sources au lieu de tirer
l'image publiée. Le service écoute sur 127.0.0.1:3000 (PAFF_BIND_ADDR,
PAFF_PORT).
curl -I http://127.0.0.1:3000/
docker compose logs -f paff
Reverse proxy
pass.example.org {
reverse_proxy 127.0.0.1:3000
}
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
X-Forwarded-For n'est cru que si la connexion vient d'une IP de
PAFF_TRUSTED_PROXIES (loopback + privés par défaut, ce qui couvre les bridges
docker). Un proxy sur une autre machine doit y être ajouté, sinon le
rate-limiting compte tout le trafic sur une seule IP.
Un proxy conteneurisé sur le même réseau docker peut viser paff:3000 et se
passer du mapping de port.
Variables
| Variable | Défaut | Rôle |
|---|---|---|
PASS_URL |
https://pass.imt-atlantique.fr |
Base PASS |
REDIS_URL |
redis://127.0.0.1:6379 |
Sessions + cache (imposé par le compose) |
SHEET_ENGINE_URL |
http://sheetengine:3000 |
Conversion de la feuille en PDF. Vide : le lien PDF répond indisponible, le classeur reste téléchargeable |
LISTEN_ADDR |
0.0.0.0:3000 |
Écoute dans le conteneur |
PAFF_PUBLIC_URL |
vide | Repli quand une requête arrive sans Host |
PAFF_BIND_ADDR / PAFF_PORT |
127.0.0.1 / 3000 |
Publication du port |
PAFF_TRUSTED_PROXIES |
loopback + privés | CIDRs autorisés à envoyer X-Forwarded-For |
SESSION_TTL |
86400 | Inactivité tolérée avant expiration (s). Repoussée à chaque usage |
SESSION_MAX_AGE |
604800 | Plafond depuis la connexion (s), jamais repoussé |
EVENT_CACHE_TTL |
1800 | Cache des événements (s) |
DETAIL_CACHE_TTL |
1800 | Cache des détails (s) |
GRADES_CACHE_TTL |
900 | Cache du relevé (s) |
PAFF_ADMINS |
vide | user_ids CAS admin, virgules |
FICHES_ROOT |
/data/fiches |
Fiches partagées |
SIGNAL_OUTBOX_TOKEN |
vide | Bearer du relais Signal, vide = désactivé |
PAFF_SIGNATURE_KEY |
vide | Chiffre les tracés de signature (procédure dans .env.example) |
ANALYTICS_SCRIPT_URL / ANALYTICS_WEBSITE_ID |
vides | Télémétrie optionnelle |
ANALYTICS_TAG |
vide | Étiquette des mesures, si plusieurs instances partagent un Umami |
TZ |
Europe/Paris |
Fuseau du conteneur |
RUST_LOG |
paff=debug,info |
Verbosité |
Durée des sessions : SESSION_TTL est une fenêtre glissante. Tant qu'une
page PAFF est ouverte et à l'écran, elle le signale toutes les cinq minutes et
l'échéance recule ; en profite aussi la session PASS, que le serveur relance au
plus une fois par quart d'heure pour éviter d'avoir à refaire le pont au clic
suivant. Rien n'est envoyé par un onglet en arrière-plan, et aucune tâche de
fond n'entretient la session de quelqu'un qui n'est pas là. SESSION_MAX_AGE
est le mur derrière : passé ce délai depuis la connexion, l'échéance ne recule
plus et il faut se reconnecter.
Devenir admin : se connecter une fois, relever User ID: ... dans
docker compose logs paff, le mettre dans PAFF_ADMINS, docker compose up -d.
Télémétrie : aucune par défaut. Sans ANALYTICS_SCRIPT_URL, aucune balise
tierce et aucune origine externe dans la CSP. Les pages appellent malgré tout
/static/analytics.js, qui ne fait alors rien : il n'émet que si un script de
télémétrie a été chargé. Avec ANALYTICS_TAG, chaque mesure porte l'étiquette
choisie, de quoi distinguer deux instances qui remontent au même endroit.
Umami 2.18 au minimum, 2.19 de préférence. La normalisation des URLs avant
envoi n'existe qu'à partir de la 2.18, et c'est elle qui empêche un code d'UE ou
le nom cherché dans l'annuaire de partir tel quel : en dessous, la recherche
/annuaire?q=... arrive entière dans Umami, l'attribut data-exclude-search ne
couvrant que les navigations internes et pas la page d'ouverture. La 2.19 ajoute
l'envoi qui survit à la navigation, sans lequel la mesure d'une connexion
réussie est perdue au moment où la page change.
Sauvegarder
Deux volumes : <projet>_redis_data (sessions, cache, assos, avis d'UE, plans)
et <projet>_fiches_data. <projet> est le nom du dossier.
docker compose exec redis redis-cli SAVE
docker run --rm -v paff_redis_data:/data -v "$PWD:/backup" alpine \
tar czf /backup/redis-$(date +%F).tar.gz -C /data .
Restaurer : tar xzf dans un volume vide, conteneurs arrêtés.
Mettre à jour
git pull
docker compose pull && docker compose up -d
Les sessions survivent (Redis en AOF).
Sans docker
redis-server --daemonize yes
cp .env.example .env
cargo run --release
Le binaire sert static/ depuis le dossier courant : le lancer depuis la racine
du dépôt. Unité systemd minimale :
[Service]
User=paff
WorkingDirectory=/opt/paff
EnvironmentFile=/opt/paff/.env
ExecStart=/opt/paff/paff
Restart=on-failure
Limites
- Ça ne marche qu'avec IMT Atlantique : CAS/SAML, SSO Moodle et le service iCal
sont codés en dur (
src/auth/,src/modules/).PASS_URLvise une autre instance Alcuin, le reste demanderait du travail. - Catalogue des UE et crédits ECTS = promo actuelle
(
crates/paff-core/src/grades.rs,scripts/scrape-catalog.js). - Rien n'est mutualisé entre instances : la vôtre repart avec un annuaire, des avis et des fiches vides.
Licence
AGPL-3.0. Une instance publique modifiée doit publier ses sources.