Infrastructure

Sécurité et traçabilité de ce portfolio

Comment ce site est durci et instrumenté — TLS, CSP stricte, journalisation JSON corrélée par request-id, limitation de débit et fail2ban.

16 septembre 20264 min de lecture867 mots
NginxTLSCSPfail2banLogs JSONPython

Ce portfolio n'est pas seulement une vitrine : c'est aussi une démonstration. Un ingénieur systèmes, réseaux et sécurité se doit d'appliquer à son propre site ce qu'il défend au quotidien. Cette fiche documente la chaîne complète.

Principes retenus#

  1. Défense en profondeur — plusieurs couches indépendantes, aucune n'étant un point

    de défaillance unique.

  2. Traçabilité de bout en bout — chaque requête reçoit un identifiant unique que l'on

    retrouve du serveur web jusqu'à la couche applicative.

  3. Zéro dépendance externe côté visiteur — aucun CDN, aucune police tierce, aucun

    traqueur. La surface d'attaque et l'exposition de données sont réduites au minimum.

  4. Moindre privilège — le service applicatif tourne sans droits root, en systemd

    durci, et n'est jamais exposé directement à Internet.

Vue d'ensemble#

text
                      Internet
                         │  HTTPS (TLS 1.3)
                         ▼
             ┌───────────────────────┐
             │        Nginx          │  ← durcissement, en-têtes, rate-limit
             │  request-id attribué  │
             └───────────┬───────────┘
              statique   │   /api, /healthz
          ┌──────────────┴───────────────┐
          ▼                              ▼
   fichiers statiques           ┌─────────────────┐
   (HTML/CSS/JS)                │  app Python      │  ← 127.0.0.1 uniquement
                                │  systemd durci   │
                                └────────┬─────────┘
                                         ▼
                            audit.jsonl  ·  messages.jsonl
                                         │
                    access.json.log ◀── même request-id ──▶ corrélation
                                         │
                              logrotate  ·  fail2ban

Couche transport — TLS#

  • TLS 1.2 et 1.3 uniquement, protocoles antérieurs désactivés.
  • Suites de chiffrement modernes, préférence serveur pour les échanges à confidentialité

    persistante (forward secrecy).

  • HSTS avec preload : le navigateur refuse toute connexion non chiffrée.
  • Agrafage OCSP pour accélérer la validation du certificat.

Couche applicative — en-têtes de sécurité#

En-têteValeurRôle
Content-Security-Policydefault-src 'self' (stricte)Bloque tout script ou style non local
X-Content-Type-OptionsnosniffEmpêche la ré-interprétation des types MIME
X-Frame-OptionsDENYAnti-clickjacking
Referrer-Policystrict-origin-when-cross-originLimite la fuite d'URL vers l'extérieur
Permissions-Policycaméra, micro, géoloc désactivésRéduit la surface d'API navigateur

La CSP est stricte : ni unsafe-inline, ni unsafe-eval. C'est possible parce que tout le JavaScript et le CSS sont servis en fichiers depuis l'origine, et que le rendu Markdown de cette documentation est fait à la génération, jamais dans le navigateur.

Traçabilité — le request-id partagé#

Le cœur du dispositif. Nginx attribue un identifiant unique à chaque requête via sa variable $request_id, puis :

  • l'écrit dans son journal d'accès JSON (access.json.log) ;
  • le transmet au backend dans l'en-tête X-Request-ID ;
  • le backend l'inscrit dans son journal applicatif (audit.jsonl) ;
  • il est renvoyé au visiteur et affiché en pied de page.

Une seule valeur relie donc les trois niveaux. Face à un incident, on part de l'identifiant et on reconstitue toute la trajectoire de la requête.

json
// access.json.log — vu par Nginx
{"time":"2026-09-16T10:22:04+00:00","remote_addr":"203.0.113.7",
 "request":"POST /api/contact","status":200,"request_id":"a7f3c9d2e1b8..."}

// audit.jsonl — vu par l'application
{"ts":"2026-09-16T10:22:04.118Z","event":"contact_received",
 "request_id":"a7f3c9d2e1b8...","ip_hash":"9f2a...","status":200}

Protection de la vie privée#

L'IP en clair ne vit que dans le journal Nginx, à durée de conservation courte. Dans le journal applicatif, elle est pseudonymisée (HMAC-SHA-256 avec un sel régénéré chaque jour) : on peut regrouper l'activité d'un visiteur sur une journée sans jamais stocker de donnée directement identifiante durablement.

Limitation de débit et anti-abus#

  • Nginx : limit_req sur les points d'API, plafond de connexions par IP.
  • Application : fenêtre glissante par IP — 3 messages/heure sur le contact,

    60 événements/heure sur la télémétrie.

  • Formulaire : champ pot de miel (honeypot) invisible ; s'il est rempli, la

    soumission est silencieusement ignorée mais tracée.

fail2ban#

Une prison lit access.json.log et bannit temporairement les IP qui accumulent des codes 4xx/429, signature typique d'un scan ou d'un brute-force.

ini
[portfolio]
enabled  = true
filter   = portfolio
logpath  = /var/log/nginx/portfolio.access.json.log
maxretry = 20
findtime = 120
bantime  = 3600

Durcissement systemd#

Le service applicatif s'exécute avec un compte dédié sans shell et un bac à sable systemd :

ini
DynamicUser=yes
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
PrivateDevices=yes
RestrictAddressFamilies=AF_INET AF_INET6
SystemCallFilter=@system-service
MemoryDenyWriteExecute=yes

Concrètement : pas d'élévation de privilèges possible, système de fichiers en lecture seule sauf le répertoire de logs, pas d'accès au réseau autre qu'IP, appels système réduits au strict nécessaire.

Conservation des journaux#

logrotate fait tourner les journaux quotidiennement, les comprime et les conserve 90 jours avant purge. À la rotation, un SIGHUP est envoyé au service pour qu'il rouvre proprement ses fichiers.

Récapitulatif des couches#

CoucheContrôle principal
RéseauTLS 1.3, HSTS preload, agrafage OCSP
ApplicationCSP stricte, en-têtes de sécurité, no-sniff
Traçabilitérequest-id corrélé sur 3 niveaux, audit JSON
Anti-abusrate-limit Nginx + applicatif, honeypot
Détectionfail2ban sur journaux JSON
Exécutionsystemd durci, moindre privilège, 127.0.0.1
Vie privéeIP pseudonymisée, rétention 90 j, zéro tiers