Sécuriser un portfolio : défense en profondeur
Comment j'ai implémenté une architecture de sécurité complète pour ce portfolio. OWASP Top 10, threat modeling STRIDE, et preuves d'attaques bloquées.
Le contexte
Ce portfolio n'est pas juste une vitrine. C'est une démonstration de compétences en sécurité applicative. L'objectif : implémenter une architecture de sécurité complète, documentée et testable.
Ce post documente la conception défensive. Pour la validation offensive (j'ai pentest mon propre site quelques mois plus tard pour identifier les angles morts), voir le retour de pentest. Pour l'observabilité côté homelab qui ingère les events, voir le mini-SOC Wazuh.
Architecture de defense
3 couches de protection
Internet
|
v
[Cloudflare] -----> DDoS protection, TLS 1.3, WAF
|
v
[Next.js Middleware] --> Rate limiting, honeypots, validation
|
v
[Application] ----> Input sanitization, CSP, security headers
Chaque couche a un rôle spécifique. Si une couche est compromise, les autres restent actives.
Conformite OWASP Top 10
| Risque | Protection |
|---|---|
| A01 - Broken Access Control | CSP strict + frame-ancestors 'none' |
| A02 - Cryptographic Failures | HSTS force + TLS 1.3 via Cloudflare |
| A03 - Injection | Input sanitization + pattern detection |
| A05 - Security Misconfiguration | Headers sécurité complets |
| A07 - Auth Failures | Rate limiting (5 contacts/15min) |
| A09 - Logging Failures | Logs JSON structurés pour tous les events sécurité |
Threat Model (STRIDE)
J'ai appliqué la méthodologie STRIDE pour identifier les menaces :
- Spoofing : Honeypot dans le formulaire pour piéger les bots
- Tampering : Sanitization de tous les inputs
- Repudiation : Logging structuré de toutes les requêtes suspectes
- Information Disclosure : Pas de stack traces, headers X-Powered-By désactivés
- Denial of Service : Rate limiting multi-niveau (global + formulaire)
- Elevation of Privilege : Pas d'admin, architecture statique
Implementations techniques
Middleware de sécurité (Next.js)
// Rate limiting global : 100 req/min
if (!checkRateLimit(ip)) {
logSecurityEvent("RATE_LIMIT_EXCEEDED", ip, pathname);
return new NextResponse("Too Many Requests", { status: 429 });
}
// Rate limiting contact : 5 tentatives/15min
if (!checkContactRateLimit(ip)) {
logSecurityEvent("CONTACT_RATE_LIMIT", ip, pathname);
return new NextResponse("Too Many Requests", { status: 429 });
}
Honeypots
Des endpoints pièges détectent les scans malveillants :
const HONEYPOT_PATHS = [
"/wp-admin", "/.env", "/phpinfo.php",
"/phpmyadmin", "/.git/config", "/backup.sql"
];
if (isHoneypotPath(pathname)) {
logSecurityEvent("HONEYPOT_TRIGGERED", ip, pathname);
blockedIPs.set(ip, Date.now() + BLOCK_DURATION);
return new NextResponse("Not Found", { status: 404 });
}
Quand un scanner teste /wp-admin, l'IP est bloquée 15 minutes.
Détection de patterns malveillants
const SUSPICIOUS_PATTERNS = [
// SQL Injection
/('|")\s*(or|and)\s*('|")?1('|")?\s*=\s*('|")?1/i,
/union\s+(all\s+)?select/i,
// XSS
/<script[\s>]/i,
/javascript:/i,
// Path Traversal
/\.\.\//,
/%2e%2e%2f/i,
];
if (hasSuspiciousPattern(fullUrl)) {
logSecurityEvent("SUSPICIOUS_REQUEST", ip, pathname);
return new NextResponse("Bad Request", { status: 400 });
}
Honeypot formulaire
Un champ invisible piège les bots qui remplissent tous les champs :
// Honeypot check - if filled, it's a bot
if (honeypot) {
// Silently "succeed" to not reveal the trap
setFormStatus("success");
return;
}
Validation et sanitization des inputs
// Sanitize HTML entities
function sanitizeHtml(input: string): string {
return input.replace(/[&<>"'`=/]/g, (char) => HTML_ENTITIES[char]);
}
// Validate before sending
const validation = validateContactInput(formData);
if (!validation.valid) {
setFormError(validation.errors.join(". "));
return;
}
Security Headers
// Headers configurés dans next.config.ts
{
"Strict-Transport-Security": "max-age=63072000; includeSubDomains; preload",
"X-Frame-Options": "SAMEORIGIN",
"X-Content-Type-Options": "nosniff",
"X-XSS-Protection": "1; mode=block",
"Referrer-Policy": "strict-origin-when-cross-origin",
"Cross-Origin-Opener-Policy": "same-origin",
"Cross-Origin-Resource-Policy": "same-origin",
"Content-Security-Policy": "default-src 'self'; frame-ancestors 'none'..."
}
Exemples d'attaques bloquees
| Attaque | Payload | Resultat |
|---|---|---|
| SQL Injection | ' OR '1'='1 | 400 Bad Request |
| XSS | <script>alert(1)</script> | Input sanitized |
| Path Traversal | ../../../etc/passwd | 400 Bad Request |
| Rate Limit | 100+ req/min | 429 + Retry-After |
| Scanner | GET /wp-admin | Logged + 404 + IP blocked |
| Bot spam | Honeypot filled | Silent success (no email) |
Logging securite
Chaque événement de sécurité est logué en JSON structuré :
Événements tracés :
RATE_LIMIT_EXCEEDEDHONEYPOT_TRIGGEREDSUSPICIOUS_REQUESTIP_BLOCKEDCONTACT_RATE_LIMIT
Choix techniques justifiés
Pourquoi Next.js middleware ? Exécution avant le rendu, dans le même runtime que l'app. Bloque les attaques au plus tôt côté origine (Cloudflare en amont filtre déjà le gros du bruit, le middleware durcit ce qui passe).
Pourquoi Formspree ? Service externe = pas de backend à sécuriser, pas de DB exposée. Moins de surface d'attaque, plus simple à auditer.
Pourquoi rate limiting in-memory ?
Le portfolio tourne en single container Coolify (un seul process Node), donc une Map in-memory suffit. Redis serait overkill : Cloudflare gère déjà le rate limit volumétrique en amont, le middleware ne fait que la couche fine au-dessus.
Limites connues
- Rate limit reset au redémarrage du container (deploy ou OOM)
- Pas de WAF applicatif complet (Cloudflare en amont assure cette couche)
- In-memory storage = pas de persistence cross-restart
- Fenêtre fixe (pas sliding window) : burst théorique 2x à la frontière de minute. Mitigé par CF en amont et par les Wazuh aggregate rules en aval (cf pentest qui valide ce trade-off)
Ces limites sont acceptables pour un portfolio personnel avec Cloudflare devant et un mini-SOC derrière.
Integration avec mon mini-SOC homelab
Depuis la migration Vercel vers self-hosted (mai 2026), les logs de sécurité du portfolio remontent dans ma stack d'observabilité homelab :
- Vector sur le serveur Hetzner ship les container logs Coolify vers Loki centralisé (LXC dédié sur mon Proxmox local)
- Grafana affiche un dashboard temps réel des events
HONEYPOT_TRIGGERED,RATE_LIMIT_EXCEEDED,SUSPICIOUS_REQUEST - Les patterns suspects critiques peuvent déclencher une alerte Telegram via la même infra Wazuh qui monitore tout le homelab
Le pipeline complet : Next.js middleware → console.log JSON → Docker stdout → Vector → Loki → Grafana → Telegram (sur seuils définis).
Détails techniques de cette stack dans le post dédié au mini-SOC homelab avec les rules custom et l'architecture complète.
Ce que j'ai appris
- Défense en profondeur : plusieurs couches indépendantes
- STRIDE : méthodologie structurée pour identifier les menaces
- OWASP Top 10 : checklist concrète de vulnérabilités à couvrir
- Security logging : les logs structurés changent tout pour le monitoring
- Minimal attack surface : pas de backend = moins de risques
- Observabilité centralisée : ship les events sécurité vers une stack qui les analyse, sinon les logs restent dans le container et meurent au prochain deploy
La sécurité est intégrée nativement dans tous les sites que je livre, avec observabilité optionnelle vers une stack centralisée si tu veux du monitoring continu. Consultez mon parcours technique pour voir mes autres projets en production, ou demandez un devis pour un site sécurisé de A à Z.
Besoin d'un site vitrine professionnel ?
Je crée des sites sur mesure pour les entreprises à Caen et partout en France. Devis gratuit, réponse sous 24h.