Crowdsec – ispconfig / iptables

Je suis passé de fail2ban (que j’utilise depuis lonnngtemps) à Crowdsec pour les raisons suivantes :

  • fail2ban est (très) gourmand en ressource serveur (trop)
  • Crowdsec dispose d’un volet « scénario » que je trouve bien malin et qui le rend « plus intelligent » et permet de mutualiser les IP frauduleuse.

Au final je bloque certainement plus d’attaques pour bien moins de CPU (surtout) consommés.

C’est plutôt flagrant sur le côté « économie de ressources CPU » sur les graph :

Comparatif stats (munin) avant avec fail2ban et après avec crowdsec

Mise en œuvre

Mon contexte : des serveurs avec le panel ISPconfig (ce dernier écoute sur le port 8080 – important pour la suite) sur Debian 11.

Je ne vais pas détailler ici ce qu’il y a dans la documentation Crowdsec, d’autant que ça peut changer. Pour mon OS (Debian) actuellement c’est :

# Installation du dépôt
curl -s https://install.crowdsec.net | sudo sh
# Installation de crowdsec
apt install crowdsec

Du coup, comme ISPconfig écoute déjà sur le port 8080 et que Crowdsec utilise ce port pour son API il faut modifier celui-ci (moi je passe à 8079) :

sed -i -e "s/8080/8079/g" /etc/crowdsec/config.yaml
sed -i -e "s/8080/8079/g" /etc/crowdsec/local_api_credentials.yaml
systemctl restart crowdsec

ISPconfig n’utilise pas logrotate pour la rotation de log apache, il a son propre processus intégré. Pour que Crowdsec puisse lire tout les logs HTTP (ici apache) il va falloir lui donner le chemin. MAIS. ISPconfig nomme les logs par date et créer un lien symbolique vers « access.log » :

# ls /var/log/ispconfig/httpd/david.mercereau.info/ -la
total 173480
drwxr-xr-x   2 root root     4096 30 juin  12:19 .
drwxr-xr-x 176 root root    12288 21 juin  16:05 ..
[...]
-rw-r--r--   1 root root  4712747 29 juin  23:59 20240629-access.log
-rw-r--r--   1 root root  2836678 30 juin  14:08 20240630-access.log
lrwxrwxrwx   1 root root       19 30 juin  12:19 access.log -> 20240630-access.log
lrwxrwxrwx   1 root root       55 30 juin  00:12 yesterday-access.log -> /var/www/clients/client3/web196/log/20240629-access.log

Ceci cause un problème pour la surveillance des changement crowdsec : https://github.com/crowdsecurity/crowdsec/issues/574. On pourrait indiquer /var/log/ispconfig/httpd/*/access.log mais il faudrait modifier le paramètre : https://docs.crowdsec.net/docs/data_sources/file/#poll_without_inotify et les performances (occupation) CPU s’en font directement sentir… C’est dommage. J’ai donc préféré indiquer /var/log/ispconfig/httpd//*[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]*.log à crowdsec :

echo "filenames:
  # Vhost
  - /var/log/ispconfig/httpd/*/[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]*.log
labels:
  type: apache2" > /etc/crowdsec/acquis.d/ispconfig.yaml

Mais pour que le « nouveau » log soit pris en considération je fais un restart du service crowdsec à 0:20 (la rotation ayant lieu à ~0:10) chez moi au vu des date de création des fichiers de logs :

20 0 * * * systemctl restart crowdsec

C’est un peu du bricolage mais c’est le meilleurs compromis que j’ai jusque là.

EDIT : j’ai changé mon fusil d’épaule pour (aussi) ménager le nombre de fichier suivi par crowdsec (j’ai un serveur avec 200 vhost). En effet je me suis rendu compte que /var/log/apache2/other_vhosts_access.log était surveillé via /etc/crowdsec/acquis.yaml donc il y avait double surveillance des access. J’ai donc juste ajouté les « error.log » dans mon /etc/crowdsec/acquis.d/ispconfig.yaml

filenames:
  - /var/log/ispconfig/httpd/*/error.log
labels:
  type: apache2

J’ajoute d’autres « collection » en fonction des services que j’ai sur ce serveur :

cscli collections install crowdsecurity/base-http-scenarios
cscli scenarios install   crowdsecurity/http-bf-wordpress_bf  crowdsecurity/http-bf-wordpress_bf_xmlrpc  crowdsecurity/http-wordpress_user-enum  crowdsecurity/http-wordpress_wpconfig
cscli collections install crowdsecurity/dovecot
echo "filenames:
  - /var/log/mail.log
labels:
  type: syslog " > /etc/crowdsec/acquis.d/mail.yaml 
cscli collections install fulljackz/pureftpd

A ce stade il n’y a aucun ‘effet’ (pas de blocage). De mon côté j’utilise le firewall iptables donc j’ai utiliser le bouncer qui va bien pour lui :

apt install crowdsec-firewall-bouncer-iptables

De la même façon on change le port pour joindre l’API

sed -i -e "s/8080/8079/g" /etc/crowdsec/bouncers/crowdsec-firewall-bouncer.yaml
systemctl restart crowdsec-firewall-bouncer

Des commandes utiles :

cscli decision list  
cscli alert list
# Pour voir toutes les IP blacklisté (intégrant les IP renvoyé par l'API centrale crowdsec
ipset list crowdsec-blacklists 
# Supprimer une IP bloqué
cscli decisions delete -i x.x.x.x

Il est possible d’aller bien plus loin, avant de blacklisté on peut demander un captcha à l’utilisateur : https://www.it-connect.fr/comment-proteger-son-site-wordpress-avec-crowdsec/ (pas mal…)

Ressources :

Bémols

Du fait de la « mutualisation » des blacklistes il y a de la data qui est envoyé chez un tiers… bon même si la société est Française le site est hébergé à San Francisco (mention légal) Typiquement le modèle économique est de récupérer de la data (les IP malveillantes) sur les « crowdsec community » pour détecter des intrusions et vendre des bases d’IP à bloquer aux autres… (note : ce partage vers l’API centrale est désactivable : FAQ / Troubleshooting | CrowdSec)

Le dashboard local est déprécié Cscli dashboard deprecation | CrowdSec au profil de l’APP en ligne crowdsec non open source pour le coup… (pour le coup c’est pas indispensable à l’usage de Crowdsec)

J’ai l’impression que le modèle économique se dessine et que ça se ferme un peu…

Toute proportion gardé bien sûr, on parle d’IP malveillante et non de data utilisateur… Je voudrais pas faire mon « libo-terroriste » hein :slight_smile:

Plugin Munin

Moi j’aime bien monitorer ce qui ce passe et comme le dashboard local Crowdsec n’est plus maintenu, a minima j’ai fais un plugin pour avoir des graph’ dans Munin :

Il permet d’avoir des graph’s par pays et par scénario.

ISPConfig : Un mode maintenance avec SSL

By RRZEicons (Cc-sa)

Afin de réaliser des maintenances ou migration sur mon serveur qui héberge ~100 sites et qui tourne avec le panel ISPConfig j’ai mis en place un petit mot d’excuse qui dit en substance : « maintenance en cours, merci de repasser plus tard ». C’est mieux que « La connexion a échoué » ou « Can’t Connect to MySQL Server on nian nian nian » non ?. C’était plutôt simple jusqu’à l’arrivé massive du SSL. parce que quand on tente de joindre https://david.mercereau.info ou https://zici.fr et qu’on est redirigé vers la même page de maintenance, avec le SSL ça coince / ça affiche un message d’erreur, normal… hors c’est ce qu’on cherche à éviter.

La configuration de prod en temps normal  :

  • Apache sur le port 80 & 443
  • Let’sEncrypt délivre les certificat SSL
  • Le tout est motorisé par ISPConfig

La configuration de maintenance :

  • Apache sur le port 80 & 443 (toujours)
  • Lighttpd sur le port 81 & 444
  • Iptables redirige le trafic arrivant sur le port 80 vers le port 81 en PREROUTING sauf pour mon IP (ça permet donc de bricoler)

Installation de lighttpd

Apété, et ayé :

aptitude -y install lighttpd
update-rc.d lighttpd remove
mkdir /var/www/maintenance
echo "Serveur en maintenance" > /var/www/maintenance/index.html # Vous pouvez faire une belle page html c'est mieux !

La configuration de lighttpd : /etc/lighttpd/lighttpd.conf

server.modules = (
	"mod_access",
)

server.document-root        = "/var/www/maintenance"
server.upload-dirs          = ( "/var/cache/lighttpd/uploads" )
server.errorlog             = "/var/log/lighttpd/error.log"
server.pid-file             = "/var/run/lighttpd.pid"
server.username             = "www-data"
server.groupname            = "www-data"
server.port                 = 81
server.error-handler-404 = "/index.html"

index-file.names            = ( "index.html", "index.lighttpd.html" )
url.access-deny             = ( "~", ".inc" )
static-file.exclude-extensions = ( ".php", ".pl", ".fcgi" )

include_shell "/usr/share/lighttpd/create-mime.assign.pl"
include_shell "/usr/share/lighttpd/include-conf-enabled.pl"

Récupérer la configuration SSL

J’ai fais un script qui récupère dans configuration apache (généré par ISPConfig chez moi), les chemins vers les certificats SSL Let’sEncrypt et qui (re)génère la conf lighttpd (qui crée le fichier /etc/lighttpd/conf-enabled/maintenance-mode-ssl.conf).

Préparation avant de lancer le script :

pear install config
mkdir /etc/lighttpd/certs/

C’est bon, on peut lancer le script :

<?php
require_once 'Config.php';

# Add include "maintenance-mode-ssl.conf" In lighttpd.conf
$lighttpdConfig='/etc/lighttpd/conf-enabled/maintenance-mode-ssl.conf';
# Content of SSL .pem
$ligttpdSslDir = '/etc/lighttpd/certs/';
# SSL port
$ligttpdSslPort = 444;

$sitesEnableds = scandir('/etc/apache2/sites-enabled/');
$lighttpdConfigContent='';
foreach($sitesEnableds as $sitesEnabled) {
    if ($sitesEnabled != '.' && $sitesEnabled != '..' && $sitesEnabled != '000-ispconfig.vhost' && $sitesEnabled !=  '000-apps.vhost' && $sitesEnabled != '000-default.conf' && $sitesEnabled != '000-ispconfig.conf') {
        $DocumentRoot=null;
        $ServerAlias=null;
        $ServerAliasMultiple=null;
        $ServerName=null;
        $conf = new Config();
        $root = $conf->parseConfig('/etc/apache2/sites-enabled/'.$sitesEnabled, 'apache');
        if (PEAR::isError($root)) {
            echo 'Error reading config: ' . $root->getMessage() . "\n";
            exit(1);
        }
        // Parse du fichier
        $i = 0;
        while ($item = $root->getItem('section', 'VirtualHost', null, null, $i++)) {
            foreach ($item->children as $child) {
                if ($child->name == 'ServerName') {
                    $ServerName = $child->content;
                }
                if ($child->name == 'ServerAlias') {
                    $ServerAliasMultiple=explode(" ", $child->content);
                    foreach ($ServerAliasMultiple as $ServerAliasOne) {
                        if (is_null($ServerAlias)) {
                            $ServerAlias[]=$ServerAliasOne;
                        } elseif (!in_array($ServerAliasOne, $ServerAlias))  {
                            $ServerAlias[]=$ServerAliasOne;
                        }
                    }
                }
                if ($child->name == 'DocumentRoot') {
                    $DocumentRoot = $child->content;
                }
            }
        }
        if ($ServerName != null && $DocumentRoot != null) {
            if (is_file(substr($DocumentRoot, 0, -3).'ssl/'.$ServerName.'-le.key')) {
                $key=file_get_contents(substr($DocumentRoot, 0, -3).'ssl/'.$ServerName.'-le.key');
                $crt=file_get_contents(substr($DocumentRoot, 0, -3).'ssl/'.$ServerName.'-le.crt');
                file_put_contents($ligttpdSslDir.'/'.$ServerName.'.pem', $key.$crt);
                $lighttpdConfigContent.='$HTTP["host"] == "'.$ServerName.'" {
  $SERVER["socket"] == ":'.$ligttpdSslPort.'" {
    ssl.engine = "enable" 
    ssl.pemfile = "'.$ligttpdSslDir.$ServerName.'.pem" 
  }
}
';
                if (is_array($ServerAlias)) {
                    foreach ($ServerAlias as $ServerAliasJustOne) {
                    $lighttpdConfigContent.= '$HTTP["host"] == "'.$ServerAliasJustOne.'" {
  $SERVER["socket"] == ":'.$ligttpdSslPort.'" {
    ssl.engine = "enable" 
    ssl.pemfile = "'.$ligttpdSslDir.$ServerName.'.pem" 
  }
}
';
                    }
                }
            }
        }
    }
}
file_put_contents($lighttpdConfig, $lighttpdConfigContent);
?>

ça doit générer quelque chose comme ça dans le fichier /etc/lighttpd/conf-enabled/maintenance-mode-ssl.conf :

$HTTP["host"] == "zici.fr" {
  $SERVER["socket"] == ":444" {
    ssl.engine = "enable" 
    ssl.pemfile = "/etc/lighttpd/certs/zici.fr.pem" 
  }
}
$HTTP["host"] == "david.mercereau.info" {
  $SERVER["socket"] == ":444" {
    ssl.engine = "enable" 
    ssl.pemfile = "/etc/lighttpd/certs/david.mercereau.info.pem" 
  }
}
$HTTP["host"] == "calcpv.net" {
  $SERVER["socket"] == ":444" {
    ssl.engine = "enable" 
    ssl.pemfile = "/etc/lighttpd/certs/calcpv.net.pem" 
  }
}
[...]

Maintenance On/Off

Pour se mettre en mode maintenance :

# Mon ip : 
IPMAISONDEMOI=X.X.X.X
# Lancer le script ci-dessus qui génère la conf SSL pour lighttpd 
php maintenance-ssl-lighttpd.php
# Démarrage du serveur d'excuse
service lighttpd start
# Redirection du port 80 vers 81 sauf pour mon IP
iptables -A INPUT -p tcp --dport 81 -j ACCEPT
iptables -A INPUT -p tcp --dport 444 -j ACCEPT
iptables -t nat -A PREROUTING \! -s ${IPMAISONDEMOI} -p tcp --dport 80 -j DNAT --to-destination ${IPDUSERVEUR}:81
iptables -t nat -A PREROUTING \! -s ${IPMAISONDEMOI} -p tcp --dport 443 -j DNAT --to-destination ${IPDUSERVEUR}:444
iptables -t nat -A POSTROUTING -j MASQUERADE

Et pour désactiver le mode maintenance :

# Si vous n'avez pas d'autres règles (c'est mal) :
iptables -F
iptables -t nat -F
# Sinon redémarrer votre service iptables 
# Lighttpd n'est plus utile
service lighttpd stop

Firewall : Mon script iptables

Je partage ici mon script de firewall iptable. C’est un script « à l’ancienne », dans du bash… ça fait le taf, mais rien de bien transsudant. En gros :

  • On ferme tout les ports sauf ceux qui nous intéresse (80, 25, icmp…)
  • Petite fonction pour ouvrir les ports mis en écoute sur Portsentry. Portsentry c’est un petit logiciel de sécurité en mode « pot de miel ». On met des ports en écoute mais il n’y a rien derrière. Dès que quelqu’un tente de s’y connecter (un robot ou quelqu’un de malveillant), ça bloque son IP dans le firewall pour un temps donnée. C’est radical si vous déplacez le port SSH du 22 vers autre chose et que vous mettez Portsentry à écouter (entre autre) sur le 22…
  • Mode maintenance du serveur web (lancé via ./iptables.sh maintenance). Il permet de mettre une page de maintenance pour tout le monde sauf pour vous (j’explique en détail dans cet article)
#!/bin/bash

## IP :
# Chez moi
MOI="A.A.A.A" 
# Mon serveur
SRV1="X.X.X.X"

IPT="/sbin/iptables"
PORTSENTRYCONF="/etc/portsentry/portsentry.conf"

export IPT PORTSENTRYCONF

function portsentryOpen() {
	. ${PORTSENTRYCONF}
	IFS=',' read -ra TCP_PORTS_SPLIT <<< "${TCP_PORTS}"
	for TCP_PORT in "${TCP_PORTS_SPLIT[@]}"; do 
		${IPT} -A INPUT -p tcp --dport ${TCP_PORT} -j ACCEPT
	done
	IFS=',' read -ra UDP_PORTS_SPLIT <<< "${UDP_PORTS}"
	for UDP_PORT in "${UDP_PORTS_SPLIT[@]}"; do 
		${IPT} -A INPUT -p udp --dport ${UDP_PORT} -j ACCEPT
	done
}

# Remise a 0
${IPT} -F
${IPT} -t nat -F

# Les connexions entrantes sont bloquées par défaut
${IPT} -P INPUT DROP
# Les connexions destinées à être routées sont acceptées par défaut
${IPT} -P FORWARD ACCEPT
# Les connexions sortantes sont acceptées par défaut
${IPT} -P OUTPUT ACCEPT


######################
# Règles de filtrage #
######################
# Nous précisons ici des règles spécifiques pour les paquets vérifiant
# certaines conditions.
 
# Pas de filtrage sur l'interface de "loopback"
${IPT} -A INPUT -i lo -j ACCEPT
 
# Accepter le protocole ICMP (notamment le ping)
${IPT} -A INPUT -p icmp -j ACCEPT
  
# Accepter les packets entrants relatifs à des connexions déjà
# établies : cela va plus vite que de devoir réexaminer toutes
# les règles pour chaque paquet.
${IPT} -A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT

# ftp 
${IPT} -A INPUT -p tcp --dport 20 -j ACCEPT 
${IPT} -A INPUT -p tcp --dport 21 -j ACCEPT
# Préalabielemnt, pour pure-ftpd : echo "29700 29750" > /etc/pure-ftpd/conf/PassivePortRange ${IPT} -A INPUT -p tcp --dport 29700:29750 -j ACCEPT
# SSH
${IPT} -A INPUT -p tcp --dport 222 -j ACCEPT
# NTP
${IPT} -A INPUT -p udp --dport 123 -j ACCEPT
# smtp
${IPT} -A INPUT -p tcp --dport smtp -j ACCEPT
# Pour test bricolage smtp
${IPT} -A INPUT -p tcp --dport 587 -j ACCEPT
# imap(s)
${IPT} -A INPUT -p tcp --dport 143 -j ACCEPT
${IPT} -A INPUT -p tcp --dport 993 -j ACCEPT
# sieve
${IPT} -A INPUT -p tcp --dport 4190 -j ACCEPT
# dns
${IPT} -A INPUT -p tcp --dport domain -j ACCEPT
${IPT} -A INPUT -p udp --dport domain -j ACCEPT
# http
${IPT} -A INPUT -p tcp --dport http -j ACCEPT
# https
${IPT} -A INPUT -p tcp --dport https -j ACCEPT

# Maintenance 
if [ "$1" == "maintenance" ] ; then
	echo "Maintenance On"
	/usr/sbin/service lighttpd start
	${IPT} -A INPUT -p tcp --dport 81 -j ACCEPT
	${IPT} -t nat -A PREROUTING \! -s ${MOI} -p tcp --dport 80 -j DNAT --to-destination ${SRV1}:81
	${IPT} -t nat -A POSTROUTING -j MASQUERADE
elif [ -f "/var/run/lighttpd.pid" ] ; then
	echo "Maintenance Off"
	/usr/sbin/service lighttpd stop
fi

# Portsentry 
if [ -f ${PORTSENTRYCONF} ] ; then
	portsentryOpen ${IPT} ${PORTSENTRYCONF}
fi

# End
${IPT} -A INPUT -j LOG --log-prefix "iptables denied: "  --log-level 4
${IPT} -A INPUT -j REJECT

# Si vous utilisez fail2ban, relancé à la fin du script :
#/usr/sbin/service fail2ban restart

 

 

 

Service web : Un mode maintenance pour bricoler

2ème version de ce « truc », avec support SSL, à découvrir par là : Un mode maintenance avec SSL

By RRZEicons (Cc-sa)
By RRZEicons (Cc-sa)

Afin de réaliser des maintenances sur mon service web (ou les services attenant tel que Mysql) j’ai mis en place un petit mot d’excuse qui dit en substance : « maintenance en cours, merci de repasser plus tard ». C’est mieux que « La connexion a échoué » ou « Can’t Connect to MySQL Server on nian nian nian » non ?

La configuration de prod  :

  • Apache sur le port 80

La configuration de maintenance :

  • Apache sur le port 80 (le même)
  • Lighttpd sur le port 81
  • Iptables redirige le trafic arrivant sur le port 80 vers le port 81 en PREROUTING sauf pour mon IP (ça permet donc de bricoler)

Installation de lighttpd

Apeuté, et ayé :

aptitude -y install lighttpd
update-rc.d lighttpd remove
mkdir /var/www/maintenance
echo "Serveur en maintenance" > /var/www/maintenance/index.html # Vous pouvez faire une belle page html c'est mieux !

Configuration de lighttpd

Éditer le fichier /etc/lighttpd/lighttpd.conf

server.modules = (
	"mod_access",
)

server.document-root        = "/var/www/maintenance"
server.upload-dirs          = ( "/var/cache/lighttpd/uploads" )
server.errorlog             = "/var/log/lighttpd/error.log"
server.pid-file             = "/var/run/lighttpd.pid"
server.username             = "www-data"
server.groupname            = "www-data"
server.port                 = 81
server.error-handler-404    = "/index.html"

index-file.names            = ( "index.html", "index.lighttpd.html" )

include_shell "/usr/share/lighttpd/create-mime.assign.pl"

Il est certainement possible de faire plus propre sur ce point. Actuellement je gère le message dans le index.html que je met dans le error404. Des redirection 302 quelque soit l’URL serait à envisager.

Maintenance On/Off

Pour se mettre en mode maintenance :

# Démarrage du serveur d'excuse
service lighttpd start
# Redirection du port 80 vers 81 sauf pour mon IP
iptables -A INPUT -p tcp --dport 81 -j ACCEPT
iptables -t nat -A PREROUTING \! -s ${IPMAISONDEMOI} -p tcp --dport 80 -j DNAT --to-destination ${IPDUSERVEUR}:81
iptables -t nat -A POSTROUTING -j MASQUERADE

Et pour désactiver le mode maintenance :

# Si vous n'avez pas d'autres règles (c'est mal) :
iptables -F
iptables -t nat -F
# Sinon redémarrer votre service iptables 
# Lighttpd n'est plus utile
service lighttpd stop

Si vous avez des suggestions…

En continuant à utiliser le site, vous acceptez l’utilisation des cookies (au chocolat) Plus d’informations

Les cookies sont utilisés à des fin de statistique de visite du blog sur une plateforme indépendante que j'héberge moi même. Les statistiques sot faites avec un logiciel libre. Aucune information n'est redistribué à google ou autre. Je suis seul autorisé à lire ces informations

Fermer