Différences entre versions de « Proxmox fail2ban »

De The Linux Craftsman
Aller à la navigation Aller à la recherche
 
(20 versions intermédiaires par le même utilisateur non affichées)
Ligne 21 : Ligne 21 :
 
*maxretry : correspond au nombre d'essais;
 
*maxretry : correspond au nombre d'essais;
 
*findtime : correspond à la période pendant laquelle les essais vont incrémenter maxretry;
 
*findtime : correspond à la période pendant laquelle les essais vont incrémenter maxretry;
*bantime  : correspond au temps ou l'adresse IP ne peut pas se connecter.
+
*bantime  : correspond au temps l'adresse IP ne peut pas se connecter.
  
 
= Configuration de base =
 
= Configuration de base =
Ligne 32 : Ligne 32 :
 
Cela signifie que si une adresse IP se trompe 3 fois en 1 journée (86400s) elle se fait bannir pendant 1 semaine (604800s).
 
Cela signifie que si une adresse IP se trompe 3 fois en 1 journée (86400s) elle se fait bannir pendant 1 semaine (604800s).
  
Un rapide calcule permet de trouver le nombre maximal de tentative durant une année:
+
Un rapide calcul permet de trouver le nombre maximal de tentatives durant une année:
  
 
365 jours / 7 jours par semaine * 3 tentatives = 156 essais, ce qui reste raisonnable.  
 
365 jours / 7 jours par semaine * 3 tentatives = 156 essais, ce qui reste raisonnable.  
Ligne 76 : Ligne 76 :
 
</pre>
 
</pre>
 
== Filtre pour PVEPROXY ==
 
== Filtre pour PVEPROXY ==
Il ne nous reste plus qu'a spécifier le filtre ''proxmox'' dans le fichier ''/etc/fail2ban/filter.d/proxmox.conf'' :
+
Il ne nous reste plus qu'à spécifier le filtre ''proxmox'' dans le fichier ''/etc/fail2ban/filter.d/proxmox.conf'' :
 
<pre>
 
<pre>
 
[Definition]
 
[Definition]
Ligne 83 : Ligne 83 :
 
journalmatch = _SYSTEMD_UNIT=pvedaemon.service
 
journalmatch = _SYSTEMD_UNIT=pvedaemon.service
 
</pre>
 
</pre>
 +
= Tests =
 +
Il faut maintenant tester le filtre et pour ça on peut utiliser un partage de connexion et réaliser le nombre d'échecs spécifié par la directive "maxretry" (ici 3) :
 +
<pre>
 +
# fail2ban-client status proxmox
 +
Status for the jail: proxmox
 +
|- Filter
 +
|  |- Currently failed: 0
 +
|  |- Total failed: 3
 +
|  `- Journal matches: _SYSTEMD_UNIT=pvedaemon.service
 +
`- Actions
 +
  |- Currently banned: 1
 +
  |- Total banned: 1
 +
  `- Banned IP list: 37.167.23.159
 +
</pre>
 +
On voit bien l'IP 37.167.23.159 qui est bannie !
 +
N'oubliez pas de l'ajouter à ''ignoreip'' dans le fichier ''jails.local'' ou de faire
 +
<pre>
 +
# fail2ban-client unban 37.167.23.159
 +
</pre>
 +
 
= Démarrage et enregistrement dans le chargeur de démarrage =
 
= Démarrage et enregistrement dans le chargeur de démarrage =
 
<pre>
 
<pre>
Ligne 91 : Ligne 111 :
  
 
= Proxification=
 
= Proxification=
Lorsque votre serveur Proxmox se trouve derrière un reverse-proxy (Apache, Nginx, Traefik, ...), au niveau 3 OSI, l'adresse IP du client est remplacée par celle du proxy et le serveur utilse l'entêtes ''X-Forwarded-For'' ou ''X-Real-IP'' pour transporter l'adresse du client  
+
Lorsque votre serveur Proxmox se trouve derrière un reverse-proxy (Apache, Nginx, Traefik, ...), au niveau 3 OSI, l'adresse IP du client est remplacée par celle du proxy et le serveur utilise l'entêtes ''X-Forwarded-For'' ou ''X-Real-IP'' pour transporter l'adresse du client  
 
[[Fichier:Reverse proxy proxmox.png|700px|centré]]
 
[[Fichier:Reverse proxy proxmox.png|700px|centré]]
 
Le problème est que la seule adresse que fail2ban va voir apparaître dans les logs est celle du reverse-proxy (''192.168.4.100'') :
 
Le problème est que la seule adresse que fail2ban va voir apparaître dans les logs est celle du reverse-proxy (''192.168.4.100'') :
 
[[Fichier:Reverse proxy ip logs.png|600px|centré]]
 
[[Fichier:Reverse proxy ip logs.png|600px|centré]]
 +
Même si le filtre ''fail2ban'' est configuré pour utiliser la commande ''journactl'' sur le démon ''pvedaemon'':
 +
<pre>
 +
...
 +
journalmatch = _SYSTEMD_UNIT=pvedaemon.service
 +
</pre>
 +
les adresses viennent de pveproxy qui journalise tout dans ''/var/log/pveproxy/access.log'' et ''pvedaemon'' ne journalise que les échecs d'authentification.
 +
Il est donc plus simple de faire un ''tail'' ou un ''cat'':
 +
<pre>
 +
# tail -f /var/log/pveproxy/access.log
 +
</pre>
 +
Que d'utiliser ''journalctl'' pour voir les IPs :
 +
<pre>
 +
# journalctl -fu pvedaemon
 +
Dec 14 10:43:01 labo unix_chkpwd[1326506]: password check failed for user (root)
 +
Dec 14 10:43:01 labo IPCC.xs[1320783]: pam_unix(proxmox-ve-auth:auth): authentication failure; logname= uid=0 euid=0 tty= ruser= rhost=37.167.23.159  user=root
 +
Dec 14 10:43:03 labo pvedaemon[1320783]: authentication failure; rhost=37.167.23.159 user=root@pam msg=Authentication failure
 +
</pre>
 
== Configuration de pveproxy ==
 
== Configuration de pveproxy ==
 
Si on veut voir l'adresse du client, il faut dire au [https://fr.wikipedia.org/wiki/Daemon_(informatique)| démon] ''pveproxy'' d'utiliser l'une des deux entêtes précédentes ainsi que préciser l'adresse du proxy.  
 
Si on veut voir l'adresse du client, il faut dire au [https://fr.wikipedia.org/wiki/Daemon_(informatique)| démon] ''pveproxy'' d'utiliser l'une des deux entêtes précédentes ainsi que préciser l'adresse du proxy.  
  
Préciser l'adresse du proxy est essentiel pour que ''pveproxy'' n'accepte de lire l'adresse du client présente dans l'entête ''uniquement'' si cela provient du proxy, qui est une machine de confiance. Le cas contraire, tout le monde pourrais ajouter une adresse IP dans l'entête pour la faire bannir...
+
Préciser l'adresse du proxy est essentiel pour que ''pveproxy'' n'accepte de lire l'adresse du client présente dans l'entête '''uniquement''' si cela provient du proxy, qui est une machine de confiance. Le cas contraire, tout le monde pourrait ajouter une adresse IP dans l'entête pour la faire bannir...
  
 
Nous allons créer le fichier ''/etc/default/pveproxy'' avec les lignes suivantes:
 
Nous allons créer le fichier ''/etc/default/pveproxy'' avec les lignes suivantes:
Ligne 110 : Ligne 147 :
 
# systemctl restart pveproxy
 
# systemctl restart pveproxy
 
</pre>
 
</pre>
Il ne reste plus qu'a vérifier dans les logs si les bonnes adresses s'affichent:
+
Il ne reste plus qu'à vérifier dans les logs si les bonnes adresses s'affichent:
 
<pre>
 
<pre>
 
# tail -f /var/log/pveproxy/access.log
 
# tail -f /var/log/pveproxy/access.log
Ligne 116 : Ligne 153 :
 
Si jamais ce n'est pas le cas, poursuivez avec la modification du fichier ''AnyEvent.pm''
 
Si jamais ce n'est pas le cas, poursuivez avec la modification du fichier ''AnyEvent.pm''
 
== Modification de AnyEvent.pm ==
 
== Modification de AnyEvent.pm ==
Au jours d'aujourd'hui (13/12/25) la proxification ne fonctionne plus et il faut apporter une modification dans le fichier ''/usr/share/perl5/PVE/APIServer/AnyEvent.pm'' pour que les bonnes adresses apparaissent !
+
=== Application de la modification ===
En fonction des versions cette modifications peut avoir lieu soit ligne '''1504''', soit ligne '''1554'''.  
+
Au jour d'aujourd'hui (13/12/25) la proxification ne fonctionne plus et il faut apporter une modification dans le fichier ''/usr/share/perl5/PVE/APIServer/AnyEvent.pm'' pour que les bonnes adresses apparaissent !
 +
En fonction des versions cette modification peut avoir lieu soit ligne '''1504''', soit ligne '''1554'''.  
  
 
Il faut repérer la fonction suivante:
 
Il faut repérer la fonction suivante:
Ligne 135 : Ligne 173 :
  
 
  ### INSERTION ICI ###
 
  ### INSERTION ICI ###
 
    if ($request->header('X-Forwarded-For')) {
 
        $reqstate->{peer_host} = $request->header('X-Forwarded-For');
 
    }
 
  
 
     if ($self->{spiceproxy}) {
 
     if ($self->{spiceproxy}) {
Ligne 144 : Ligne 178 :
  
 
</source>
 
</source>
 +
Il faut modifier l'attribut ''peer_host'' de l'objet ''reqstate'' si l'une des deux entêtes est présente :
 +
<source lang=perl>
 +
if ($request->header('X-Forwarded-For')) {
 +
  $reqstate->{peer_host} = $request->header('X-Forwarded-For');
 +
} elsif ($request->header('X-Real-IP')) {
 +
  $reqstate->{peer_host} = $request->header('X-Real-IP');
 +
}
 +
</source>
 +
=== Double IPv4 ou préfixe IPv6 ===
 +
Après redémarrage, cela permet de voir l'adresse du client dans les logs :
 +
<pre>
 +
# tail -f /var/log/pveproxy/access.log
 +
37.167.182.207, 37.167.182.207 - - [13/12/2025:18:48:18 +0100] "GET /pve2/ext6/ext-all.js?ver=7.0.0 HTTP/1.1" 200 683505
 +
37.167.182.207, 37.167.182.207 - - [13/12/2025:18:48:18 +0100] "GET /pve2/js/u2f-api.js HTTP/1.1" 200 4898
 +
::ffff:192.168.20.22 - - [14/12/2025:09:34:42 +0100] "GET /api2/json/cluster/resources HTTP/1.1" 200 1226
 +
::ffff:192.168.20.22 - - [14/12/2025:09:34:42 +0100] "GET /api2/json/cluster/tasks HTTP/1.1" 200 1090
 +
37.167.182.207, 37.167.182.207 - - [13/12/2025:18:48:18 +0100] "GET /proxmoxlib.js?ver=v5.0.4-t1754316706 HTTP/1.1" 200 157086
 +
37.167.182.207, 37.167.182.207 - - [13/12/2025:18:48:18 +0100] "GET /pve2/ext6/theme-crisp/resources/theme-crisp-all_1.css HTTP/1.1" 200 32919
 +
::ffff:192.168.20.22 - root@pam [14/12/2025:09:34:42 +0100] "GET /api2/json/nodes/labo/lxc/100/status/current HTTP/1.1" 200 523
 +
</pre>
 +
Il nous reste un peu de nettoyage à faire, dans certains cas :
 +
* l'adresse apparaît non pas une fois mais deux, séparées par une virgule
 +
<pre>
 +
37.167.182.207, 37.167.182.207
 +
</pre>
 +
* l'adresse IPv4 (32 bits) est affichée dans sa représentation IPv6 (128 bits), avec le préfixe ''::ffff:
 +
<pre>
 +
::ffff:192.168.20.22
 +
</pre>
 +
 +
On a deux possibilités pour faire le nettoyage :
 +
*Soit on ajoute les lignes suivantes dans ''AnyEvent.pm'':
 +
<source lang=perl>
 +
if (index($reqstate->{peer_host}, ',') != -1) {
 +
  # Remove trailing ip
 +
  my @fields = split /,/, $reqstate->{peer_host};
 +
  $reqstate->{peer_host} = $fields[0];
 +
}
 +
if($reqstate->{peer_host} =~ /.*\:([\d]*\..*)/) {
 +
  # Remove IPv6 subnet for IPv4
 +
  $reqstate->{peer_host} = $1;
 +
}
 +
</source>
 +
*Soit on modifie le filtre /etc/fail2ban/filter.d/proxmox.conf :
 +
<pre>
 +
...
 +
failregex = pvedaemon\[.*authentication failure; rhost=.*:?<HOST>,? user=.* msg=.*
 +
...
 +
</pre>
 +
 +
=== Automatisation ===
 +
On peut automatiser le processus précédent avec le script suivant, que l'on peut créer dans /root/remoteip_fix.sh :
 +
 +
<source lang=bash>
 +
#!/bin/bash
 +
 +
ANYEVENT_PATH="/usr/share/perl5/PVE/APIServer/AnyEvent.pm"
 +
ANYEVENT_REGEX='$self->{spiceproxy}'
 +
TMP_FILE="/tmp/AnyEvent.pm.tmp"
 +
 +
if [ ! -f $ANYEVENT_PATH ]; then
 +
  echo "$ANYEVENT_PATH does not exists !"
 +
  exit 1
 +
fi
 +
 +
if [ $(grep '### BUGFIX REMOTE_IP FOR FAIL2BAN ###' $ANYEVENT_PATH | wc -l) -eq 1 ]; then
 +
  echo "AnyEvent.pm already patched !"
 +
  exit 0
 +
fi
 +
 +
LINE_NUMBER_TOP=$(nl -ba $ANYEVENT_PATH | grep $ANYEVENT_REGEX | head -n 1 | awk -F ' ' '{print $1}')
 +
let LINE_NUMBER_TOP-=1
 +
 +
if [ $LINE_NUMBER_TOP -lt 0 ]; then
 +
  echo "Did not find the $ANYEVENT_REGEX expr in $ANYEVENT_PATH"
 +
  exit 1
 +
fi
 +
 +
echo -n "Applying bugfix: "
 +
trap "rm -f $TMP_FILE" 0 1 2 5 13 15
 +
head -n $LINE_NUMBER_TOP $ANYEVENT_PATH >$TMP_FILE
 +
cat >>$TMP_FILE <<EOF
 +
### BUGFIX REMOTE_IP FOR FAIL2BAN ###
 +
    if (\$request->header('X-Forwarded-For')) {
 +
        \$reqstate->{peer_host} = \$request->header('X-Forwarded-For');
 +
    } elsif (\$request->header('X-Real-IP')) {
 +
        \$reqstate->{peer_host} = \$request->header('X-Real-IP');
 +
    }
 +
    if (index(\$reqstate->{peer_host}, ',') != -1) {
 +
        my @fields = split /,/, \$reqstate->{peer_host};
 +
        \$reqstate->{peer_host} = \$fields[0];
 +
    }
 +
    if(\$reqstate->{peer_host} =~ /.*\:([\d]*\..*)/) {
 +
        # Remove IPv6 subnet for IPv4
 +
        \$reqstate->{peer_host} = \$1;
 +
    }
 +
#####################################
 +
EOF
 +
echo "$ANYEVENT_MODIF" >>$TMP_FILE
 +
cp $ANYEVENT_PATH $ANYEVENT_PATH.ori
 +
LINE_NUMBER_BOTTOM=$(cat $ANYEVENT_PATH | wc -l)
 +
let LINE_NUMBER_BOTTOM-=LINE_NUMBER_TOP
 +
tail -n $LINE_NUMBER_BOTTOM $ANYEVENT_PATH >>$TMP_FILE
 +
cat $TMP_FILE >$ANYEVENT_PATH
 +
rm -f $TMP_FILE
 +
trap 0
 +
echo "done."
 +
</source>
 +
Il ne reste plus qu'à le rendre exécutable et le lancer :
 +
<pre>
 +
# chmod +x /root/remoteip_fix.sh
 +
# /root/remoteip_fix.sh
 +
</pre>
 +
Il ne faudra pas oublier de redémarrer ''pveproxy'' pour appliquer les changements !

Version actuelle datée du 14 décembre 2025 à 11:06

Introduction

Proxmox utilise une interface web d'administration qui utilise le port TCP 8006 et c'est un vecteur possible d'attaque par force brut. Nous allons donc dresser fail2ban pour agir en cas d'attaque !

Proxmox utilise Debian, nous allons principalement reproduire les étapes décrites dans le tutoriel de Fail2ban pour Rocky et adapter à Debian.

Avant d'aller plus loin, assurez-vous d'avoir correctement installé et configuré iptables sur Proxmox

Installation

# apt -y install fail2ban

Configuration

Tout d'abord il faut copier le fichier /etc/fail2ban/jail.conf en /etc/fail2ban/jail.local

cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local

Dans ce fichier, on va s'intéresser aux variables suivantes:

  • ignoreip : correspond à la suite d'adresses IP qui ne se feront jamais bannir;
  • maxretry : correspond au nombre d'essais;
  • findtime : correspond à la période pendant laquelle les essais vont incrémenter maxretry;
  • bantime : correspond au temps où l'adresse IP ne peut pas se connecter.

Configuration de base

Choix de la punition

Il faut choisir quelque chose de cohérent (une punition suffisante) pour ne pas permettre de se faire cracker son mot de passe:

  • maxretry = 3
  • findtime = 86400 (correspond à 1 journée)
  • bantime = 604800 (correspond à 1 semaine)

Cela signifie que si une adresse IP se trompe 3 fois en 1 journée (86400s) elle se fait bannir pendant 1 semaine (604800s).

Un rapide calcul permet de trouver le nombre maximal de tentatives durant une année:

365 jours / 7 jours par semaine * 3 tentatives = 156 essais, ce qui reste raisonnable.

Si le pirate possède un botnet, il faut bien sûr multiplier ce nombre par le nombre de machines dans le botnet...

Attention : fail2ban parcours les logs de connexion pour connaître le numéro de la tentative, ce qui a pour conséquence, si le findtime est grand, de prendre un certain temps...

Jail pour SSH

Il suffit d'ajouter la ligne enabled=true dans la section à activer !

[sshd]

# To use more aggressive sshd modes set filter parameter "mode" in jail.local:
# normal (default), ddos, extra or aggressive (combines all).
# See "tests/files/logs/sshd" or "filter.d/sshd.conf" for usage example and details.
#mode   = normal
enabled = true
port    = ssh
logpath = %(sshd_log)s
backend = %(sshd_backend)s

Configuration spécifique

Jail pour PVEPROXY

Pveproxy écoute sur le port 8006 et ne fonctionne pas exactement comme un serveur Apache httpd qui mettrait ces logs dans le fichier error_log.

Nous allons mettre cette configuration spécifique dans le fichier /etc/fail2ban/jail.d/proxmox.conf :

[proxmox]
enabled = true
port = https,http,8006
filter = proxmox
backend = systemd
maxretry = 3
findtime = 2d
bantime = 1h

Le backend utilisé est systemd, exactement comme si vous utilisiez la commande :

# journalctl -fu pvedaemon

Filtre pour PVEPROXY

Il ne nous reste plus qu'à spécifier le filtre proxmox dans le fichier /etc/fail2ban/filter.d/proxmox.conf :

[Definition]
failregex = pvedaemon\[.*authentication failure; rhost=<HOST> user=.* msg=.*
ignoreregex =
journalmatch = _SYSTEMD_UNIT=pvedaemon.service

Tests

Il faut maintenant tester le filtre et pour ça on peut utiliser un partage de connexion et réaliser le nombre d'échecs spécifié par la directive "maxretry" (ici 3) :

# fail2ban-client status proxmox
Status for the jail: proxmox
|- Filter
|  |- Currently failed:	0
|  |- Total failed:	3
|  `- Journal matches:	_SYSTEMD_UNIT=pvedaemon.service
`- Actions
   |- Currently banned:	1
   |- Total banned:	1
   `- Banned IP list:	37.167.23.159

On voit bien l'IP 37.167.23.159 qui est bannie ! N'oubliez pas de l'ajouter à ignoreip dans le fichier jails.local ou de faire

# fail2ban-client unban 37.167.23.159

Démarrage et enregistrement dans le chargeur de démarrage

# systemctl start fail2ban.service
# systemctl enable fail2ban.service

Vous pouvez maintenant démarrer fail2ban

Proxification

Lorsque votre serveur Proxmox se trouve derrière un reverse-proxy (Apache, Nginx, Traefik, ...), au niveau 3 OSI, l'adresse IP du client est remplacée par celle du proxy et le serveur utilise l'entêtes X-Forwarded-For ou X-Real-IP pour transporter l'adresse du client

Reverse proxy proxmox.png

Le problème est que la seule adresse que fail2ban va voir apparaître dans les logs est celle du reverse-proxy (192.168.4.100) :

Reverse proxy ip logs.png

Même si le filtre fail2ban est configuré pour utiliser la commande journactl sur le démon pvedaemon:

...
journalmatch = _SYSTEMD_UNIT=pvedaemon.service

les adresses viennent de pveproxy qui journalise tout dans /var/log/pveproxy/access.log et pvedaemon ne journalise que les échecs d'authentification. Il est donc plus simple de faire un tail ou un cat:

# tail -f /var/log/pveproxy/access.log

Que d'utiliser journalctl pour voir les IPs :

# journalctl -fu pvedaemon
Dec 14 10:43:01 labo unix_chkpwd[1326506]: password check failed for user (root)
Dec 14 10:43:01 labo IPCC.xs[1320783]: pam_unix(proxmox-ve-auth:auth): authentication failure; logname= uid=0 euid=0 tty= ruser= rhost=37.167.23.159  user=root
Dec 14 10:43:03 labo pvedaemon[1320783]: authentication failure; rhost=37.167.23.159 user=root@pam msg=Authentication failure

Configuration de pveproxy

Si on veut voir l'adresse du client, il faut dire au démon pveproxy d'utiliser l'une des deux entêtes précédentes ainsi que préciser l'adresse du proxy.

Préciser l'adresse du proxy est essentiel pour que pveproxy n'accepte de lire l'adresse du client présente dans l'entête uniquement si cela provient du proxy, qui est une machine de confiance. Le cas contraire, tout le monde pourrait ajouter une adresse IP dans l'entête pour la faire bannir...

Nous allons créer le fichier /etc/default/pveproxy avec les lignes suivantes:

PROXY_REAL_IP_HEADER="X-Forwarded-For"
PROXY_REAL_IP_HEADER="X-Real-IP"
PROXY_REAL_IP_ALLOW_FROM="192.168.4.100"

Il faut maintenant redémarrer pveproxy pour que les changements s'appliquent. Si vous êtes connecté via l'interface web, cela va vous déconnecter, le temps que le démon redémarre, il faudra ensuite certainement rafraîchir la page web.

# systemctl restart pveproxy

Il ne reste plus qu'à vérifier dans les logs si les bonnes adresses s'affichent:

# tail -f /var/log/pveproxy/access.log

Si jamais ce n'est pas le cas, poursuivez avec la modification du fichier AnyEvent.pm

Modification de AnyEvent.pm

Application de la modification

Au jour d'aujourd'hui (13/12/25) la proxification ne fonctionne plus et il faut apporter une modification dans le fichier /usr/share/perl5/PVE/APIServer/AnyEvent.pm pour que les bonnes adresses apparaissent ! En fonction des versions cette modification peut avoir lieu soit ligne 1504, soit ligne 1554.

Il faut repérer la fonction suivante:

sub authenticate_and_handle_request {
    my ($self, $reqstate) = @_;

    my $request = $reqstate->{request};
    my $method = $request->method();

...

C'est un peu plus loin qu'il faut insérer le code :

        }
    }

 ### INSERTION ICI ###

    if ($self->{spiceproxy}) {
        my $connect_str = $request->header('Host');

Il faut modifier l'attribut peer_host de l'objet reqstate si l'une des deux entêtes est présente :

if ($request->header('X-Forwarded-For')) {
   $reqstate->{peer_host} = $request->header('X-Forwarded-For');
} elsif ($request->header('X-Real-IP')) {
   $reqstate->{peer_host} = $request->header('X-Real-IP');
}

Double IPv4 ou préfixe IPv6

Après redémarrage, cela permet de voir l'adresse du client dans les logs :

# tail -f /var/log/pveproxy/access.log
37.167.182.207, 37.167.182.207 - - [13/12/2025:18:48:18 +0100] "GET /pve2/ext6/ext-all.js?ver=7.0.0 HTTP/1.1" 200 683505
37.167.182.207, 37.167.182.207 - - [13/12/2025:18:48:18 +0100] "GET /pve2/js/u2f-api.js HTTP/1.1" 200 4898
::ffff:192.168.20.22 - - [14/12/2025:09:34:42 +0100] "GET /api2/json/cluster/resources HTTP/1.1" 200 1226
::ffff:192.168.20.22 - - [14/12/2025:09:34:42 +0100] "GET /api2/json/cluster/tasks HTTP/1.1" 200 1090
37.167.182.207, 37.167.182.207 - - [13/12/2025:18:48:18 +0100] "GET /proxmoxlib.js?ver=v5.0.4-t1754316706 HTTP/1.1" 200 157086
37.167.182.207, 37.167.182.207 - - [13/12/2025:18:48:18 +0100] "GET /pve2/ext6/theme-crisp/resources/theme-crisp-all_1.css HTTP/1.1" 200 32919
::ffff:192.168.20.22 - root@pam [14/12/2025:09:34:42 +0100] "GET /api2/json/nodes/labo/lxc/100/status/current HTTP/1.1" 200 523

Il nous reste un peu de nettoyage à faire, dans certains cas :

  • l'adresse apparaît non pas une fois mais deux, séparées par une virgule
37.167.182.207, 37.167.182.207
  • l'adresse IPv4 (32 bits) est affichée dans sa représentation IPv6 (128 bits), avec le préfixe ::ffff:
::ffff:192.168.20.22

On a deux possibilités pour faire le nettoyage :

  • Soit on ajoute les lignes suivantes dans AnyEvent.pm:
if (index($reqstate->{peer_host}, ',') != -1) {
   # Remove trailing ip 
   my @fields = split /,/, $reqstate->{peer_host};
   $reqstate->{peer_host} = $fields[0];
}
if($reqstate->{peer_host} =~ /.*\:([\d]*\..*)/) {
   # Remove IPv6 subnet for IPv4
   $reqstate->{peer_host} = $1;
}
  • Soit on modifie le filtre /etc/fail2ban/filter.d/proxmox.conf :
...
failregex = pvedaemon\[.*authentication failure; rhost=.*:?<HOST>,? user=.* msg=.*
...

Automatisation

On peut automatiser le processus précédent avec le script suivant, que l'on peut créer dans /root/remoteip_fix.sh :

#!/bin/bash

ANYEVENT_PATH="/usr/share/perl5/PVE/APIServer/AnyEvent.pm"
ANYEVENT_REGEX='$self->{spiceproxy}'
TMP_FILE="/tmp/AnyEvent.pm.tmp"

if [ ! -f $ANYEVENT_PATH ]; then
  echo "$ANYEVENT_PATH does not exists !"
  exit 1
fi

if [ $(grep '### BUGFIX REMOTE_IP FOR FAIL2BAN ###' $ANYEVENT_PATH | wc -l) -eq 1 ]; then
  echo "AnyEvent.pm already patched !"
  exit 0
fi

LINE_NUMBER_TOP=$(nl -ba $ANYEVENT_PATH | grep $ANYEVENT_REGEX | head -n 1 | awk -F ' ' '{print $1}')
let LINE_NUMBER_TOP-=1

if [ $LINE_NUMBER_TOP -lt 0 ]; then
  echo "Did not find the $ANYEVENT_REGEX expr in $ANYEVENT_PATH"
  exit 1
fi

echo -n "Applying bugfix: "
trap "rm -f $TMP_FILE" 0 1 2 5 13 15
head -n $LINE_NUMBER_TOP $ANYEVENT_PATH >$TMP_FILE
cat >>$TMP_FILE <<EOF
### BUGFIX REMOTE_IP FOR FAIL2BAN ###
    if (\$request->header('X-Forwarded-For')) {
        \$reqstate->{peer_host} = \$request->header('X-Forwarded-For');
    } elsif (\$request->header('X-Real-IP')) {
        \$reqstate->{peer_host} = \$request->header('X-Real-IP');
    }
    if (index(\$reqstate->{peer_host}, ',') != -1) {
        my @fields = split /,/, \$reqstate->{peer_host};
        \$reqstate->{peer_host} = \$fields[0];
    }
    if(\$reqstate->{peer_host} =~ /.*\:([\d]*\..*)/) {
        # Remove IPv6 subnet for IPv4
        \$reqstate->{peer_host} = \$1;
    }
#####################################
EOF
echo "$ANYEVENT_MODIF" >>$TMP_FILE
cp $ANYEVENT_PATH $ANYEVENT_PATH.ori
LINE_NUMBER_BOTTOM=$(cat $ANYEVENT_PATH | wc -l)
let LINE_NUMBER_BOTTOM-=LINE_NUMBER_TOP
tail -n $LINE_NUMBER_BOTTOM $ANYEVENT_PATH >>$TMP_FILE
cat $TMP_FILE >$ANYEVENT_PATH
rm -f $TMP_FILE
trap 0
echo "done."

Il ne reste plus qu'à le rendre exécutable et le lancer :

# chmod +x /root/remoteip_fix.sh
# /root/remoteip_fix.sh

Il ne faudra pas oublier de redémarrer pveproxy pour appliquer les changements !