Protéger un projet Symfony2 contre les bruteforce

5 gravatarParGrégoire Marchal -29/08/2012

Pour protéger mon tout nouveau blog des attaques par bruteforce, je pensais créer un bundle comme je l'avais fait pour symfony1 avec le sfAntiBruteForcePlugin. Puis, en montant mon serveur, je suis tombé sur un tutoriel qui utilise Fail2ban pour protéger notamment l'accès SSH des attaques par bruteforce. Pour information, le principe Fail2ban est d'observer des fichiers de log afin de détecter des activités anormales. Dans ce cas, par défaut, il bannit l'IP de la machine source pendant 10 minutes grâce à iptables et vous prévient par e-mail.

Il est possible d'ajouter des filtres personnalisés à Fail2ban, c'est ce que j'ai fait pour protéger la page de login de la zone d'administration de mon blog. Je vais vous expliquer comment.

Tout d'abord, il faut savoir où sont les logs d'accès à votre serveur web. J'utilise nginx, donc ils sont là : /var/log/nginx*/*access*.log.

Ensuite, il faut identifier ce qui se passe quand une tentative de login est effectuée. J'utilise le FOSUserBundle, donc il y a un POST qui est fait sur l'URL /login_check. Il est visible dans les logs sous la forme suivante :
1.2.3.4 - - [29/Aug/2012:11:36:30 +0200] "POST /login_check HTTP/1.1" 302 ...

Pas besoin de savoir si l'authentification a réussi ou non, on considère que si on atteint cette page X fois en moins de 10 minutes, c'est une attaque. On a donc toutes les informations nécessaires pour écrire notre filtre. Pour cela, il faut créer le fichier suivant : /etc/fail2ban/filter.d/nginx-login.conf.

# Blocks IPs that access to authenticate using web application's login page
# Scan access log for POST /login_check
[Definition]
failregex = <HOST> -.*POST /login_check
ignoreregex =

On voit donc bien qu'on surveille tous les POST effectués sur une URL commençant par "/login_check". Il ne reste plus qu'à modifier la configuration de Fail2ban pour utiliser ce filtre. Ajoutez ceci à la fin du fichier /etc/fail2ban/jail.conf :

[nginx-login]

enabled  = true
filter   = nginx-login
port     = http,https
logpath  = /var/log/nginx*/*access*.log
maxretry = 5

On indique donc les logs à surveiller, le filtre à utiliser et le nombre d'essais autorisés.

N'oubliez pas de recharger le service pour prendre en compte la nouvelle configuration : sudo service fail2ban force-reload. Vérifiez dans les logs que tout s'est bien passé : /var/log/fail2ban.log (il peut notamment vous dire que votre expression régulière est invalide). Testez ensuite de bruteforcer votre site (attention, vous allez être banni 10 minutes si ça se passe bien !).

Voilà, vous avez maintenant l'esprit tranquille !

Gestion du cache HTTP avec Symfony2

7 gravatarParGrégoire Marchal -27/08/2012

Avant de vous lancer dans la lecture de cet article, je vous conseille de lire la documentation Symfony concernant le cache. Comme d'habitude c'est assez clair, et ça me permet de ne pas avoir à tout vous expliquer :).

Pour ce blog, la stratégie que j'ai adoptée est la suivante : lors de l'affichage d'un article, j'utilise la date de dernière modification de celui-ci pour définir l'en-tête HTTP "Last-Modified". Si dans la requête je vois que le client a déjà cette version en cache, je peux arrêter le traitement et retourner le code HTTP 304 "Not Modified". Dans le cas contraire, je continue le traitement pour servir la page au client, qui au passage la stockera en cache pour un éventuel futur appel.

Niveau code, voilà ce que ça donne dans mon contrôleur :

public function displayAction(Post $post)
{
    $response = new Response();
    $response->setLastModified($post->getModificationDate());
    $response->setPublic();

    if ($response->isNotModified($this->getRequest())) {
        return $response; // this will return the 304 if the cache is OK
    }

    // Do some stuff here...

    return $this->render('...:display.html.twig', array(
        'post' => $post,
        // ...
    ), $response);
}

Les 3 premières lignes permettent de définir la stratégie utilisée pour la gestion du cache. Notez l'appel à setPublic() pour définir que le cache est commun à tous les utilisateurs, et non pas propre à l'utilisateur courant. Cela permet aux caches partagés (c'est-à-dire les proxy caches et les gateway caches) de stocker le cache également. Ainsi, si un utilisateur A a déjà affiché un article, un utilisateur B pourra profiter du cache qui a été généré pour l'utilisateur A, sans re-générer toute la page. Dans mon cas, c'est Symfony2 qui joue le rôle de gateway cache.

Ensuite, j'appelle la méthode $response->isNotModified() en passant la requête en paramètre. C'est donc le framework qui va décider si on retourne un code 304 ou non, en comparant la date du header "Last-Modified" (que l'on vient de définir) à la date du header "If-Modified-Since" envoyé par le client (date de la version qu'il a dans son cache). Si l'article n'a pas été modifié depuis sa mise en cache, on n'a plus qu'à retourner l'objet réponse, qui représente alors le fameux code 304 "Not Modified".

Dans le cas contraire, on poursuit le traitement du contrôleur. Dans mon cas il s'agit principalement de préparer le formulaire pour l'ajout de commentaires... Enfin je traite mon template, en n'oubliant de passer mon objet $response en 3e paramètre de $this->render(), afin que le header "Last-Modified" soit bien ajouté à la réponse pour la mise en cache ; si vous oubliez ce dernier point, Symfony va créer un nouvel objet Response standard, sans header pour la gestion du cache, et donc nos clients ne stockeront jamais notre page en cache !

Prochainement, je vous parlerai des "edge side includes" pour gérer le cache de la colonne de gauche de mon blog, et aussi des event listeners pour gérer la date de modification des articles en prenant en compte les commentaires...

A bientôt !

Symfonic.fr passe sous Symfony2

11 gravatarParGrégoire Marchal -25/08/2012

Ça y est, le blog nouveau est arrivé ! Changement d'hébergement, changement de look, changement de moteur et surtout changement de sujet : en route pour Symfony2 ! Il était grand temps de donner un coup de jeune à symfonic.fr, c'est chose faite.

Côté hébergement, j'étais sur un mutualisé OVH premier prix : facile à utiliser mais trop limité dès qu'on veut s'amuser un peu... Je suis donc passé sur un Virtual Private Server (Kimsufi / OVH), version "small" (bien conseillé par @bourvill, expert OVH ;)). Limité en RAM (512Mo), j'ai remplacé apache par nginx + php-fpm et je ne le regrette pas ! C'est léger et rapide, voyez vous-même ! Pour le reste du côté "admin sys", n'étant pas expert dans ce domaine, je me suis basé sur ce tuto, avec quelques adaptations. Si vous pensez qu'il manque des choses à ce niveau, je suis preneur !

Côté look, j'ai fait simple. Ceux qui le connaissent auront bien sûr reconnu le twitter bootstrap. Simple, propre, beau : j'aime bien. Et valide W3C, oui monsieur !

Côté backend, j'ai dit adieux sans regret à wordpress. A sa place, Symfony2 biensûr ! Je me suis basé sur le tutoriel du site du zéro, auquel j'ai ajouté les fonctionnalités dont j'avais besoin pour ce blog : gestion multilingue, gestion fine du cache, la partie admin, flux RSS, ... Je reviendrai plus en détails sur certains points dans les prochains articles.

Et sinon, si vous constatez des bugs, pensez à des améliorations ou autre, n'hésitez pas à m'en faire part sur github !