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.
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#
- Défense en profondeur — plusieurs couches indépendantes, aucune n'étant un point
de défaillance unique.
- 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.
- 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.
- Moindre privilège — le service applicatif tourne sans droits root, en systemd
durci, et n'est jamais exposé directement à Internet.
Vue d'ensemble#
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 · fail2banCouche 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ête | Valeur | Rôle |
|---|---|---|
Content-Security-Policy | default-src 'self' (stricte) | Bloque tout script ou style non local |
X-Content-Type-Options | nosniff | Empêche la ré-interprétation des types MIME |
X-Frame-Options | DENY | Anti-clickjacking |
Referrer-Policy | strict-origin-when-cross-origin | Limite la fuite d'URL vers l'extérieur |
Permissions-Policy | caméra, micro, géoloc désactivés | Réduit la surface d'API navigateur |
La CSP est stricte : ni
unsafe-inline, niunsafe-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.
// 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_reqsur 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.
[portfolio]
enabled = true
filter = portfolio
logpath = /var/log/nginx/portfolio.access.json.log
maxretry = 20
findtime = 120
bantime = 3600Durcissement systemd#
Le service applicatif s'exécute avec un compte dédié sans shell et un bac à sable systemd :
DynamicUser=yes
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
PrivateDevices=yes
RestrictAddressFamilies=AF_INET AF_INET6
SystemCallFilter=@system-service
MemoryDenyWriteExecute=yesConcrè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#
| Couche | Contrôle principal |
|---|---|
| Réseau | TLS 1.3, HSTS preload, agrafage OCSP |
| Application | CSP stricte, en-têtes de sécurité, no-sniff |
| Traçabilité | request-id corrélé sur 3 niveaux, audit JSON |
| Anti-abus | rate-limit Nginx + applicatif, honeypot |
| Détection | fail2ban sur journaux JSON |
| Exécution | systemd durci, moindre privilège, 127.0.0.1 |
| Vie privée | IP pseudonymisée, rétention 90 j, zéro tiers |