[pve-devel] [RFC firewall] implement fail2ban in firewall

Oguz Bektas o.bektas at proxmox.com
Mon Aug 23 16:07:36 CEST 2021


only as POC/RFC

Signed-off-by: Oguz Bektas <o.bektas at proxmox.com>
---

known issues:
- see FIXME in generate_fail2ban_config. when update/compile is called
  the fail2ban service will be restarted, that leads to reload every 10s
  (also the jail conf file is re-written, which i'd like to avoid)
- no API integration yet. add to host.fw:

==============
[FAIL2BAN]
maxretry: N
bantime: N # minutes
==============


 debian/control      |  1 +
 src/PVE/Firewall.pm | 81 ++++++++++++++++++++++++++++++++++++++++++++-
 2 files changed, 81 insertions(+), 1 deletion(-)

diff --git a/debian/control b/debian/control
index 4684c5b..377c9ae 100644
--- a/debian/control
+++ b/debian/control
@@ -17,6 +17,7 @@ Package: pve-firewall
 Architecture: any
 Conflicts: ulogd,
 Depends: ebtables,
+         fail2ban,
          ipset,
          iptables,
          libpve-access-control,
diff --git a/src/PVE/Firewall.pm b/src/PVE/Firewall.pm
index edc5336..73ae396 100644
--- a/src/PVE/Firewall.pm
+++ b/src/PVE/Firewall.pm
@@ -1347,6 +1347,26 @@ our $host_option_properties = {
     },
 };
 
+our $fail2ban_option_properties = {
+	enable => {
+	    description => "Enable or disable fail2ban on a node.",
+	    type => 'boolean',
+	    default => 1,
+	},
+	maxretry => {
+	    description => "Amount of failed tries to ban after.",
+	    type => 'integer',
+	    minimum => 1,
+	    default => 3,
+	},
+	bantime => {
+	    description => "Minutes to ban suspicious IPs.",
+	    type => 'integer',
+	    minimum => 1,
+	    default => 5,
+	},
+};
+
 our $vm_option_properties = {
     enable => {
 	description => "Enable/disable firewall rules.",
@@ -2407,6 +2427,30 @@ sub ruleset_generate_vm_rules {
     }
 }
 
+sub generate_fail2ban_config {
+    my ($maxretry, $bantime) = @_;
+
+    my $fail2ban_filter = "
+[Definition]
+failregex = pvedaemon\\[.*authentication failure; rhost=<HOST> user=.* msg=.*
+ignoreregex =\n";
+    my $filter_path = '/etc/fail2ban/filter.d/proxmox.conf';
+    PVE::Tools::file_set_contents($filter_path, $fail2ban_filter);
+
+    my $fail2ban_config = "
+[proxmox]
+enabled = true
+port = https,http,8006
+filter = proxmox
+logpath = /var/log/daemon.log
+maxretry = $maxretry
+bantime = $bantime\n";
+
+    my $jail_path = '/etc/fail2ban/jail.d/proxmox.conf';
+    PVE::Tools::file_set_contents($jail_path, $fail2ban_config);
+    #run_command([qw(systemctl try-reload-or-restart fail2ban.service)]); # FIXME: excessive... gets called by update/compile every 10 seconds so disable for now
+}
+
 sub generate_nfqueue {
     my ($options) = @_;
 
@@ -2937,6 +2981,22 @@ sub parse_alias {
     return undef;
 }
 
+sub parse_fail2ban_option {
+    my ($line) = @_;
+
+    my ($opt, $value);
+
+    if ($line =~ m/^(maxretry):\s+(\d+)\S*$/) {
+	$opt = $1;
+	$value = $2 // $fail2ban_option_properties->{maxretry}->{default};
+    } elsif ($line =~ m/^(bantime):\s+(\d+)\S*$/) {
+	$opt = $1;
+	$value = $2 * 60 // $fail2ban_option_properties->{bantime}->{default} * 60;
+    }
+
+    return ($opt, $value);
+}
+
 sub generic_fw_config_parser {
     my ($filename, $cluster_conf, $empty_conf, $rule_env) = @_;
 
@@ -2965,6 +3025,11 @@ sub generic_fw_config_parser {
 
 	my $prefix = "$filename (line $linenr)";
 
+	if ($empty_conf->{fail2ban} && ($line =~ m/^\[fail2ban\]$/i)) {
+	    $section = 'fail2ban';
+	    next;
+	}
+
 	if ($empty_conf->{options} && ($line =~ m/^\[options\]$/i)) {
 	    $section = 'options';
 	    next;
@@ -3046,6 +3111,12 @@ sub generic_fw_config_parser {
 		$res->{aliases}->{lc($data->{name})} = $data;
 	    };
 	    warn "$prefix: $@" if $@;
+	} elsif ($section eq 'fail2ban') {
+	    my ($opt, $value);
+	    eval {
+		($opt, $value) = parse_fail2ban_option($line);
+		$res->{fail2ban}->{$opt} = $value;
+	    };
 	} elsif ($section eq 'rules') {
 	    my $rule;
 	    eval { $rule = parse_fw_rule($prefix, $line, $cluster_conf, $res, $rule_env); };
@@ -3620,7 +3691,7 @@ sub load_hostfw_conf {
 
     $filename = $hostfw_conf_filename if !defined($filename);
 
-    my $empty_conf = { rules => [], options => {}};
+    my $empty_conf = { rules => [], options => {}, fail2ban => {}};
     return generic_fw_config_parser($filename, $cluster_conf, $empty_conf, 'host');
 }
 
@@ -4575,10 +4646,15 @@ sub init {
 
     return if !$enable;
 
+
     # load required modules here
 }
 
 sub update {
+
+    use Sys::Syslog;
+    syslog('info', "\n\nupdating stuff again...\n");
+
     my $code = sub {
 
 	my $cluster_conf = load_clusterfw_conf();
@@ -4589,7 +4665,10 @@ sub update {
 	    return;
 	}
 
+
 	my $hostfw_conf = load_hostfw_conf($cluster_conf);
+	my $fail2ban_opts = $hostfw_conf->{fail2ban};
+	generate_fail2ban_config($fail2ban_opts->{maxretry}, $fail2ban_opts->{bantime});
 
 	my ($ruleset, $ipset_ruleset, $rulesetv6, $ebtables_ruleset) = compile($cluster_conf, $hostfw_conf);
 
-- 
2.30.2






More information about the pve-devel mailing list