Sécurité — headers HTTP, applicatif, tokens, uploads
Ce document répond à : "Quels sont les dispositifs qui garantissent la sécurité du serveur Telaria ?" La sécurité est traitée par couches : réseau (UFW, Fail2ban), transport (TLS), navigateur (headers), application (Symfony).
1. Couche réseau — UFW + Fail2ban
Décrits en détail dans 01-provisionement.md. Résumé :
- UFW : seuls 3 ports ouverts — 80 (HTTP→redirect), 443 (HTTPS), 9501 (SSH custom)
- Fail2ban : bannissement automatique après 3 tentatives SSH échouées (ban 1h)
- SSH : port 9501, PasswordAuthentication désactivé, PermitRootLogin non
2. Security headers HTTP
Les headers de sécurité sont configurés côté Apache et s'appliquent à toutes les réponses. Ils instruisent le navigateur sur les politiques à appliquer.
Configuration Apache (/etc/apache2/conf-available/security-headers.conf) :
<IfModule mod_headers.c> # Empêche le navigateur de deviner le type MIME (sniffing) Header always set X-Content-Type-Options "nosniff" # Interdit l'intégration dans une iframe (clickjacking) Header always set X-Frame-Options "DENY" # Contrôle les informations envoyées au site référent Header always set Referrer-Policy "strict-origin-when-cross-origin" # Désactive les fonctionnalités navigateur non utilisées Header always set Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()" # Cross-Origin isolation (requis pour SharedArrayBuffer, Spectre mitigation) Header always set Cross-Origin-Embedder-Policy "require-corp" Header always set Cross-Origin-Opener-Policy "same-origin" Header always set Cross-Origin-Resource-Policy "same-origin" # HSTS (voir 04-tls-https.md) Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" </IfModule>
Content Security Policy (CSP)
La CSP est la défense principale contre les attaques XSS. Elle définit les origines autorisées pour chaque type de ressource.
Stratégie adoptée : mode Report-Only progressif
# Phase 1 : observation (Report-Only — ne bloque rien) Header always set Content-Security-Policy-Report-Only \ "default-src 'self'; \ script-src 'self' 'nonce-{NONCE}'; \ style-src 'self' 'unsafe-inline'; \ img-src 'self' data:; \ font-src 'self'; \ connect-src 'self'; \ frame-ancestors 'none'; \ report-uri /csp-report" # Phase 2 (après validation) : mode enforce # Remplacer Content-Security-Policy-Report-Only par Content-Security-Policy
Pourquoi progressif ? Une CSP trop stricte casse les styles ou les scripts légitimes. En Report-Only, les violations sont collectées sans impact utilisateur, permettant d'affiner la politique avant de l'appliquer.
3. Sécurité TLS
Détaillée dans 04-tls-https.md. Points clés :
- TLS 1.2 minimum, TLS 1.3 préféré
- Ciphers ECDHE uniquement (Perfect Forward Secrecy)
- OCSP stapling activé
- HSTS avec preload
4. Sécurité applicative Symfony
Authentification et sessions
# config/packages/framework.yaml framework: session: cookie_secure: true # Cookie HTTPS uniquement cookie_samesite: strict # Protection CSRF cross-site cookie_httponly: true # Inaccessible depuis JavaScript gc_maxlifetime: 3600 # Expiration session 1h
Autorisation par Voters
Les règles d'accès aux ressources sont encapsulées dans des Voters Symfony :
// RecipeVoter.php class RecipeVoter extends Voter { protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool { $user = $token->getUser(); return match ($attribute) { 'EDIT', 'DELETE' => $subject->getOwner() === $user || in_array('ROLE_ADMIN', $user->getRoles()), default => false, }; } }
Avantage : la logique d'autorisation est centralisée, testable unitairement, et ne se répète pas dans les controllers.
Protection CSRF
Sur toutes les actions destructives (suppression d'une entité) :
#[Route('/recipe/{id}/delete', name: 'recipe_delete', methods: ['POST'])] #[IsCsrfTokenValid('delete-recipe-{id}')] public function delete(Recipe $recipe): Response { ... }
Template Twig correspondant :
<form method="post" action="{{ path('recipe_delete', {id: recipe.id}) }}"> <input type="hidden" name="_token" value="{{ csrf_token('delete-recipe-' ~ recipe.id) }}"> <button type="submit">Supprimer</button> </form>
Rate limiting
Composant RateLimiter Symfony sur les endpoints sensibles :
# config/packages/rate_limiter.yaml framework: rate_limiter: contact_form: policy: token_bucket limit: 5 rate: { interval: '1 minute' } api_endpoints: policy: token_bucket limit: 60 rate: { interval: '1 minute' }
// Dans le controller — vérification en entrée de méthode $limiter = $this->rateLimiterFactory->create($request->getClientIp()); if (!$limiter->consume()->isAccepted()) { throw new TooManyRequestsHttpException(); }
5. Tokens API — SHA-512
Les tokens d'authentification MCP (et API interne) sont stockés hachés en SHA-512, jamais en clair.
Génération
php bin/console app:token:generate # Génère un token aléatoire, affiche la valeur en clair (à transmettre au client) # et stocke le hash SHA-512 en base
Vérification
// Lors de la vérification d'un token entrant $isValid = hash_equals( hash('sha512', $tokenFromRequest), $storedHash );
hash_equals est obligatoire (timing-safe comparison) pour éviter les attaques par timing.
Rotation
Si un token est compromis :
php bin/console app:token:revoke <token_id> php bin/console app:token:generate # Communiquer le nouveau token au client concerné
6. Sécurisation des uploads
Le service FileUploader implémente trois protections :
Validation MIME réelle (pas l'extension)
$finfo = new \finfo(FILEINFO_MIME_TYPE); $mimeType = $finfo->file($uploadedFile->getPathname()); if (!in_array($mimeType, $this->allowedMimeTypes)) { throw new InvalidArgumentException("Type MIME non autorisé : $mimeType"); }
L'extension déclarée par l'utilisateur est ignorée. finfo lit les magic bytes du fichier.
Protection path traversal
// Ne jamais utiliser directement le nom original $safeFilename = $slugger->slug($originalFilename); $newFilename = $safeFilename . '-' . uniqid() . '.' . $extension; // Vérifier que le chemin final est bien dans le répertoire attendu $targetPath = realpath($uploadDir . '/' . $newFilename); if (!str_starts_with($targetPath, realpath($uploadDir))) { throw new SecurityException("Path traversal détecté"); }
Logs des tentatives suspectes
if ($violationDetected) { $this->logger->warning('Upload suspect', [ 'original_name' => $originalFilename, 'mime_detected' => $mimeType, 'user_id' => $this->security->getUser()?->getId(), 'ip' => $request->getClientIp(), ]); }
7. Procédure en cas de clé API exposée
Si une clé API est committée par erreur :
- Révoquer immédiatement la clé chez le fournisseur (Anthropic Console, OpenAI, etc.)
- Supprimer du dépôt :
git filter-branchougit-filter-repo+ force-push (coordonner avec Mathieu) - Générer une nouvelle clé et mettre à jour
.env.localsur le serveur - Documenter l'incident dans l'historique git (message de commit explicite)
Règle :
.env.localn'est jamais commité..gitignoredoit exclure*.env.localet.env.local.
8. Checklist de sécurité
| Dispositif | Statut |
|---|---|
| SSH port custom (9501), no-password | âś… |
| UFW (3 ports max) | âś… |
| Fail2ban (SSH) | âś… |
| TLS 1.2+ uniquement | âś… |
| HSTS + preload | âś… |
| OCSP stapling | âś… |
| X-Frame-Options DENY | âś… |
| X-Content-Type-Options nosniff | âś… |
| CSP (Report-Only progressif) | âś… |
| COEP/COOP/CORP | âś… |
| Cookie secure + samesite | âś… |
| Tokens SHA-512 | âś… |
| CSRF sur destructives | âś… |
| Rate limiting | âś… |
| Upload MIME validation + path traversal | âś… |
| Mises Ă jour auto (unattended-upgrades) | âś… |
Étape suivante : 06-performance.md