Files
maintenance_page/PROMPT.md
T
fzarifian b968ec8aa5 feat: implémentation initiale du reverse proxy de maintenance
Reverse proxy Nginx (image stable-alpine) qui sert l'app upstream ou une
page de maintenance 503 selon l'IP du client.

- Modes whitelist/blacklist commutables via MAINTENANCE_MODE
- Liste IPv4 via MAINTENANCE_IP_LIST (séparée par virgules, validée au boot)
- Logique en directives Nginx natives (geo + map), zéro Lua/njs
- Page statique sobre HTML+CSS inline, zéro JS, zéro réseau sortant
- Log dédié /var/log/nginx/maintenance.log pour les requêtes bloquées
- Validation des env vars dans /docker-entrypoint.d/10-init.sh
- Stack Docker + docker-compose pour dev local et tests
- 6 cas de tests d'intégration (whitelist/blacklist x autorisée/bloquée + log + nginx -t)
- Lint shellcheck propre sur tous les scripts shell
2026-05-05 13:52:13 +02:00

19 KiB

PROMPT.md — Reverse Proxy Nginx avec Page de Maintenance Conditionnelle par IP

Document de spécification autonome du projet. Toute personne (humaine ou agent IA) peut s'y référer pour comprendre, implémenter et maintenir le projet sans contexte externe.


1. Vision du projet

Construire un reverse proxy Nginx qui, selon l'adresse IP du client, redirige les requêtes vers une application locale (proxy_pass) ou affiche une page de maintenance statique avec un code HTTP 503 Service Unavailable. Le projet est packagé en Docker, agnostique de l'application backend, et la logique (mode whitelist / blacklist + liste d'IP + upstream) est entièrement pilotée par variables d'environnement au démarrage du conteneur.

Cas d'usage typique : exposer une application en pré-production à une équipe interne uniquement, ou bloquer ponctuellement quelques IP indésirables pendant qu'on travaille sur un correctif.


2. Stack technique

Composant Choix Confiance Justification
Reverse proxy Nginx (image nginx:stable-alpine) 🟢 élevée Standard de l'industrie, déterministe, configuration purement déclarative.
Logique conditionnelle Directives natives geo + map + if (pas de Lua, pas de njs) 🟢 élevée Couvre 100 % du besoin sans dépendance tierce. Évite les modules dynamiques.
Templating de config envsubst (depuis gettext) appelé par l'entrypoint Nginx officiel via /etc/nginx/templates/ 🟢 élevée Mécanisme officiel de l'image Nginx, zéro magie, pas de runtime supplémentaire.
Page de maintenance HTML + CSS inline, system font stack, icône SVG inline, zéro JavaScript 🟢 élevée Sert même si tout est cassé en aval. Pas de dépendance réseau (pas de Google Fonts, pas de CDN).
Packaging Docker + docker-compose (pour le dev local et la démo) 🟢 élevée Reproductible, mêmes commandes en local et en prod.
Entrypoint Script bash (validation des env vars + nginx -t) 🟢 élevée Échec rapide au boot si conf invalide, plutôt qu'un 502 silencieux.
Tests Scripts bash + curl lancés contre un conteneur de test (avec un upstream simulé en nginx:alpine ou python -m http.server) 🟢 élevée Boîte noire = vérité terrain. Pas de framework de test à apprendre.
Lint shell shellcheck sur tous les .sh 🟢 élevée Outil standard, signaux nets.
Validation conf nginx -t systématique avant tout commit (pre-commit local) 🟢 élevée Coût zéro, attrape 90 % des bugs.

Aucune techno 🟡 ou 🔵 dans ce projet. Toutes les briques sont dans la zone de fiabilité maximale d'un agent comme moi : Nginx natif, bash POSIX, Docker, curl. Aucune librairie de niche.


3. Architecture et structure des dossiers

appli_test/
├── PROMPT.md                     # Ce fichier — source de vérité du projet
├── README.md                     # Quickstart : comment lancer, comment configurer
├── CHANGELOG.md                  # Conventional Changelog
├── .editorconfig                 # Cohérence indentation entre éditeurs
├── .gitignore
├── .dockerignore
├── Dockerfile                    # Image basée sur nginx:stable-alpine
├── docker-compose.yml            # Stack de dev : proxy + upstream factice
├── docker-compose.test.yml       # Stack de tests d'intégration
│
├── nginx/
│   ├── templates/
│   │   └── default.conf.template # Template traité par envsubst au boot
│   └── snippets/
│       └── maintenance-log.conf  # Format de log dédié maintenance.log
│
├── public/
│   └── maintenance.html          # Page statique servie en 503 (HTML+CSS inline)
│
├── scripts/
│   ├── entrypoint.sh             # Validation env + génération conf + nginx -t + exec nginx
│   ├── build-ip-list.sh          # Transforme MAINTENANCE_IP_LIST="a,b,c" en directives geo
│   └── lint.sh                   # Lance shellcheck + nginx -t
│
└── tests/
    ├── run.sh                    # Orchestrateur : up compose.test, attend, lance les cas, tear down
    ├── cases/
    │   ├── whitelist_authorized_ip.sh    # IP listée → 200 (proxy OK)
    │   ├── whitelist_unauthorized_ip.sh  # IP non listée → 503 + page maintenance
    │   ├── blacklist_blocked_ip.sh       # IP listée → 503
    │   ├── blacklist_normal_ip.sh        # IP non listée → 200
    │   ├── log_dedicated.sh              # Une requête bloquée apparaît dans maintenance.log
    │   └── nginx_syntax.sh               # `nginx -t` retourne 0
    └── fixtures/
        └── upstream/                     # Petit serveur factice pour simuler l'app cible

Pourquoi cette structure ?

  • nginx/templates/ est le chemin que l'entrypoint officiel de l'image Nginx scanne automatiquement pour envsubst. On suit la convention upstream.
  • public/ séparé du templating : la page statique n'a pas à passer par envsubst.
  • scripts/ regroupe tout le shell pour qu'il soit lintable et testable d'un coup.
  • tests/cases/ : un fichier .sh par scénario, lisible, débogable individuellement.

4. Spécification fonctionnelle détaillée

4.1 Variables d'environnement (contrat d'API)

Variable Obligatoire Exemple Description
MAINTENANCE_MODE whitelist ou blacklist Sémantique de la liste. whitelist = seules les IP listées passent, le reste voit la maintenance. blacklist = inverse.
MAINTENANCE_IP_LIST 81.92.47.8,10.0.0.42,192.168.1.5 Liste d'IP IPv4 individuelles séparées par virgules. Espaces tolérés autour des virgules. Pas de CIDR, pas d'IPv6 (out of scope MVP).
UPSTREAM_HOST 127.0.0.1:3000 Hôte:port de l'application backend. Le proxy fait proxy_pass http://$UPSTREAM_HOST.
LISTEN_PORT (défaut 8080) 80 Port d'écoute du conteneur.
SERVER_NAME (défaut _) app.example.com Valeur de server_name dans le bloc Nginx.

4.2 Comportement

  • Topologie : Nginx est directement exposé au client. L'IP source = $remote_addr. Aucune confiance accordée à X-Forwarded-For reçu de l'extérieur.
  • Code HTTP : la page de maintenance est servie avec 503 Service Unavailable (et un en-tête Retry-After: 3600 indicatif).
  • Scope : la maintenance s'applique à tout le domaine — chaque path, chaque méthode, y compris assets et appels API. Aucune exclusion (pas de healthcheck particulier, pas d'exception assets).
  • Logging :
    • access.log standard pour toutes les requêtes (format combined).
    • maintenance.log dédié : ne contient que les requêtes ayant abouti sur la page de maintenance. Format : $time_iso8601 $remote_addr "$request" "$http_user_agent".
  • Pas de bypass token : le seul critère discriminant est l'IP. Pas de cookie magique, pas de header de contournement.
  • Validation au démarrage : si MAINTENANCE_MODE n'est ni whitelist ni blacklist, ou si MAINTENANCE_IP_LIST est vide, ou si UPSTREAM_HOST est absent, l'entrypoint échoue immédiatement avec un message clair sur stderr et exit code 1. Pas de fallback silencieux.

4.3 Logique Nginx (cœur du projet)

Le mécanisme repose sur deux directives :

# Construit dynamiquement par envsubst depuis MAINTENANCE_IP_LIST
geo $ip_in_list {
    default 0;
    81.92.47.8  1;
    10.0.0.42   1;
    # ... une ligne par IP, générée au boot
}

# MAINTENANCE_MODE = whitelist → on bloque si pas dans la liste (ip_in_list=0)
# MAINTENANCE_MODE = blacklist → on bloque si dans la liste (ip_in_list=1)
map "$maintenance_mode:$ip_in_list" $is_in_maintenance {
    default          0;
    "whitelist:0"    1;
    "blacklist:1"    1;
}

L'astuce : $maintenance_mode est lui-même injecté par envsubst, et MAINTENANCE_IP_LIST est transformée en lignes geo par scripts/build-ip-list.sh avant que Nginx ne démarre.


5. Conventions de codage

5.1 Nginx

  • Indentation : 4 espaces, jamais de tabulations.
  • Une directive par ligne, accolades sur leur propre ligne pour les blocs longs.
  • Commentaires uniquement quand le pourquoi n'est pas évident depuis la lecture. Jamais de commentaire qui paraphrase la directive.
  • Pas de if dans les blocs location sauf cas spécifiquement supportés (cf. If Is Evil). On préfère map + try_files + return.
  • Toute conf doit passer nginx -t ; aucune exception.

5.2 Bash

  • Shebang #!/usr/bin/env bash partout.
  • set -euo pipefail en première ligne après le shebang.
  • Toutes les variables entre guillemets : "$VAR", "${ARR[@]}".
  • Vérification d'arguments en début de script avec message d'usage.
  • Tous les scripts .sh doivent passer shellcheck sans warning.

5.3 HTML/CSS (page de maintenance)

  • Un seul fichier public/maintenance.html autonome.
  • CSS inline dans <style>, pas de fichier externe.
  • System font stack : system-ui, -apple-system, "Segoe UI", Roboto, sans-serif.
  • Icône SVG inline pour l'illustration (pas de <img> qui pourrait 404).
  • Layout centré (flex), max-width raisonnable (~480px), respiration visuelle.
  • Doit être lisible sans JS et sans réseau.

5.4 Conventional Commits

Format obligatoire :

<type>(<scope>): <description courte impérative>

[corps optionnel expliquant le pourquoi]

Types autorisés : feat, fix, docs, refactor, test, chore, ci.

Ne JAMAIS ajouter de ligne Co-Authored-By: Claude ... dans les messages de commit. Aucune attribution d'agent IA dans l'historique Git.

5.5 Git workflow — trunk-based

  • Tous les commits vont directement sur main.
  • Pas de branches feature/*, pas de pull requests internes.
  • Conséquence : chaque commit doit être atomique, vert, et déployable. nginx -t + tests/run.sh doivent passer avant chaque commit.
  • Tags Git pour les versions notables : v0.1.0, v0.2.0, etc.

6. Outils et plugins Claude Code

Cette section décrit comment un agent Claude Code doit travailler sur ce projet.

6.1 Superpowers — skills à utiliser systématiquement

Skill Quand l'invoquer
superpowers:brainstorming Avant toute modification non triviale du comportement (nouvelle env var, nouveau code HTTP, nouvelle exclusion de path). On clarifie l'intention avant le code.
superpowers:test-driven-development Pour toute feature comportementale : écrire d'abord un cas de test dans tests/cases/ qui échoue, puis modifier la conf Nginx pour qu'il passe.
superpowers:systematic-debugging Dès qu'un test échoue ou qu'une requête ne se comporte pas comme attendu. Pas de "patch jusqu'à ce que ça marche".
superpowers:verification-before-completion Avant de déclarer une tâche terminée : nginx -t , tests/run.sh , shellcheck scripts/*.sh . Évidence avant assertion.
superpowers:writing-plans Pour toute évolution dépassant 2-3 fichiers (ex : ajout du support CIDR ultérieurement).

6.2 Superpowers — skills non utilisés pour ce projet

  • superpowers:using-git-worktrees : non applicable. Le projet est en trunk-based, mono-développeur, scope minimal. Si le projet grossit et que des features parallèles deviennent nécessaires, ce skill pourra être réintroduit — mais pas avant.

6.3 Context7

  • À utiliser quand on a un doute sur une directive Nginx récente, une option de l'image Docker nginx:stable-alpine, ou la sémantique de envsubst.
  • Inutile pour le HTML/CSS/bash basique — c'est dans la zone 🟢.
  • Préférer Context7 à la recherche web pour toute documentation officielle.

6.4 Frontend Design

  • Applicable uniquement à public/maintenance.html. Si on veut faire évoluer le visuel (au-delà du sobre actuel), on peut invoquer ce skill.
  • Pour le MVP, le design est intentionnellement simple : pas besoin du skill au démarrage.

6.5 Commit Commands

  • Tout commit doit suivre les Conventional Commits (cf. § 5.4).
  • Interdiction absolue de Co-Authored-By dans les commits.
  • Commits atomiques : un commit = un changement cohérent et testé.

7. Phases de développement

MVP en une seule phase (choix utilisateur). Le projet est suffisamment petit pour qu'un découpage en phases produirait plus de cérémonie que de valeur.

Phase 1 — MVP complet

  • Squelette projet : Dockerfile, docker-compose.yml, .gitignore, .dockerignore, .editorconfig, README.md quickstart, CHANGELOG.md.
  • Page de maintenance public/maintenance.html : HTML+CSS inline, system fonts, icône SVG, layout centré, message sobre en français.
  • Template Nginx nginx/templates/default.conf.template :
    • Bloc server écoutant sur ${LISTEN_PORT}.
    • Directives geo (générées dynamiquement) et map pour la décision.
    • location / qui :
      • sert /maintenance.html avec status 503 + Retry-After si $is_in_maintenance = 1,
      • fait proxy_pass http://${UPSTREAM_HOST} sinon.
    • Log dédié maintenance.log conditionné par $is_in_maintenance.
  • Script scripts/build-ip-list.sh : transforme MAINTENANCE_IP_LIST en lignes Nginx dans un fichier inclus par le template.
  • Script scripts/entrypoint.sh : valide les env vars (MAINTENANCE_MODE ∈ {whitelist, blacklist}, MAINTENANCE_IP_LIST non vide, UPSTREAM_HOST défini), appelle build-ip-list.sh, lance nginx -t, puis exec nginx -g "daemon off;".
  • Tests d'intégration tests/run.sh + 6 cas dans tests/cases/ couvrant whitelist (autorisée + bloquée), blacklist (bloquée + normale), log dédié, syntaxe Nginx.
  • docker-compose.test.yml : stack qui combine le proxy avec un upstream factice (image python:3.12-alpine lançant un http.server ou un autre nginx:alpine qui sert un texte fixe).
  • scripts/lint.sh : shellcheck scripts/*.sh tests/run.sh tests/cases/*.sh + nginx -t (via conteneur jetable).
  • README.md : usage (docker run -e MAINTENANCE_MODE=... -e MAINTENANCE_IP_LIST=... -e UPSTREAM_HOST=... -p 8080:8080 image), exemples docker-compose, comment lancer les tests, comment ajouter une IP.
  • Validation finale : tous les tests verts, image Docker construit, README à jour.

8. Design & UX (page de maintenance)

Direction esthétique : sobre avec un peu de style. Pas minimaliste austère, pas branding. Niveau "page d'erreur soignée d'une bonne app indé".

Élément Choix
Palette Fond #fafaf9 (presque blanc cassé), texte #1c1917 (presque noir), accent #0891b2 (cyan calme) pour l'icône.
Typographie System font stack, titre 28-32px, corps 16px, line-height 1.6.
Icône SVG inline ~64px représentant un outil (clé, engrenage, ou cône de chantier). Couleur d'accent.
Layout Flex centré horizontalement et verticalement, min-height: 100vh, max-width contenu ~480px, padding généreux.
Ton Neutre, en français, rassurant : « Site en maintenance. Nous revenons très vite. » + sous-titre court. Aucune date estimée (pas demandé).
Responsive Mobile-first par défaut (largeurs en pourcentage / max-width).
Accessibilité Contraste AA minimum, <title> explicite, lang="fr" sur <html>.

9. Règles impératives

Liste de règles non négociables. Tout PR/commit qui les viole doit être refusé.

  1. Aucune dépendance Lua, njs, ou module Nginx tiers. On reste sur les directives natives.
  2. Aucun JavaScript dans la page de maintenance. Elle doit fonctionner navigateur texte / curl / w3m.
  3. Aucune requête réseau sortante depuis la page de maintenance (pas de Google Fonts, pas de CDN, pas d'analytics).
  4. nginx -t doit passer avant chaque commit.
  5. Tous les scripts shell doivent passer shellcheck sans warning.
  6. Tous les tests d'intégration doivent passer avant chaque commit sur main.
  7. Aucun secret en dur dans l'image, les templates ou les scripts. Tout passe par variables d'environnement.
  8. Aucune écriture sur le disque par Nginx en dehors des logs (access.log, error.log, maintenance.log).
  9. La page de maintenance retourne strictement 503, jamais 200 ni 307.
  10. La maintenance s'applique à tout le domaine. Aucune exclusion de path tant que ce n'est pas une exigence explicite et discutée.
  11. Aucune ligne Co-Authored-By dans les messages de commit.
  12. Toute IP doit être validée syntaxiquement par build-ip-list.sh avant d'être injectée dans la conf Nginx (regex IPv4). Une IP malformée → erreur de boot, pas un silence.
  13. Validation explicite à l'entrypoint. Si une variable obligatoire manque ou est invalide, le conteneur sort en erreur avec un message lisible. Pas de défaut implicite.
  14. Pas de bypass token, pas de cookie magique, pas de header de contournement. Le seul critère est l'IP.

10. Stratégie de fiabilité

Cette section décrit comment un agent doit se comporter face à l'incertitude.

10.1 Toutes les technos sont 🟢 dans ce projet

Aucun composant 🟡 ni 🔵. Si une évolution future introduit une techno 🟡 (ex : OpenResty/Lua) ou 🔵 (ex : njs, module dynamique exotique), elle doit être :

  • explicitement marquée avec son niveau de confiance dans une mise à jour de PROMPT.md,
  • accompagnée d'une vérification Context7 pour récupérer la doc officielle à jour,
  • isolée dans un commit dédié (pas mêlée à d'autres changements), pour pouvoir être revertée proprement.

10.2 Quand un pattern Nginx ne marche pas comme prévu

  1. Lire error.log d'abord. 80 % du temps, il y a un message explicite.
  2. Reproduire avec un cas de test minimal dans tests/cases/.
  3. Ne pas ajouter de if dans une location comme rustine — c'est presque toujours le mauvais outil.
  4. Si la directive native ne suffit pas, proposer une alternative dans un commit séparé et prévenir l'utilisateur avant de la merger.

10.3 Vérification systématique avant tout Done

Checklist à exécuter mentalement avant de déclarer quoi que ce soit terminé :

  • docker compose -f docker-compose.test.yml up --abort-on-container-exit retourne 0 ?
  • tests/run.sh affiche OK pour chaque cas ?
  • scripts/lint.sh ne produit aucune sortie ?
  • nginx -t passe sur le template instancié avec un jeu d'env vars représentatif ?
  • README.md reflète bien les env vars actuelles ?
  • CHANGELOG.md mentionne le changement ?

Évidence avant assertion : ne jamais dire « ça marche » sans avoir relancé les tests dans la session courante. Le skill superpowers:verification-before-completion formalise cette discipline.

10.4 Quand demander à l'utilisateur

  • Toute extension du contrat d'env vars (nouvelle variable, changement de format).
  • Tout changement de comportement HTTP (code de retour, en-têtes).
  • Toute exception au scope « maintenance s'applique à tout le domaine ».
  • Toute introduction d'un composant 🟡 ou 🔵.

Pour le reste (refactor interne, amélioration de la page de maintenance, ajout de tests), l'agent peut avancer en autonomie tant qu'il respecte les règles impératives du § 9.


Fin du PROMPT.md. Toute évolution de ce document doit être commitée avec un message docs(prompt): <changement> et discutée avec l'utilisateur en amont si elle modifie le scope ou le contrat d'API.