03-comment-je-travaille/vps/05-securite.md

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 :

  1. Révoquer immédiatement la clé chez le fournisseur (Anthropic Console, OpenAI, etc.)
  2. Supprimer du dépôt : git filter-branch ou git-filter-repo + force-push (coordonner avec Mathieu)
  3. Générer une nouvelle clé et mettre à jour .env.local sur le serveur
  4. Documenter l'incident dans l'historique git (message de commit explicite)

Règle : .env.local n'est jamais commité. .gitignore doit exclure *.env.local et .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

Assistant documentaire

Posez une question sur la documentation. Les réponses citent leurs sources — un clic ouvre le document à gauche.

Loading…
Loading the web debug toolbar…
Attempt #