From 402a6c9e8960ede3323491f8732b8170165891e6 Mon Sep 17 00:00:00 2001 From: Christoph Date: Mon, 2 Nov 2026 22:01:28 +0100 Subject: [PATCH] Role ipt-server: sync with ipt-server repo (93d9af8); adapt extract-fw-host-vars.py. - Update firewall scripts and shared conf files from firewall/ipt-server (HTTP connection limits, IPv6 TCP MSS clamping, ban_ipv6_mask, mattermost TCP fallback, rename 'stansard_*' -> 'standard_*'). - New role variables: fw_set_max_new_http_conn_per_ip_per_second, fw_max_http_new_conn_per_ip_per_second, fw_set_max_concurrent_http_conn_per_ip, fw_max_concurrent_http_conn_per_ip, fw_set_max_new_http_conn_per_second, fw_max_new_http_conn_per_second, fw_throttle_tcp6_mss_for_https, fw_tcp6_mss_for_https_val, fw_mm_tcp_ports_in, fw_ban_ipv6_mask. - Split fw_blocked_ips into fw_blocked_ips_v4 / fw_blocked_ips_v6 and rename it in host_vars accordingly. - Restart firewall when scripts or shared conf files change. - extract-fw-host-vars.py: map new variables, normalize '${var}' and renamed bash variables, fall back to IPv6 for shared variables and warn if IPv4 and IPv6 values differ. - README.md / MIGRATION.md: document host_vars naming (-ipt-server.yml) and new variables. Co-Authored-By: Claude Opus 5.5 --- extract-fw-host-vars.py | 80 +++++- host_vars/a.ns.oopen.de/a.ns-ipt-server.yml | 2 +- host_vars/b.ns.oopen.de/b.ns-ipt-server.yml | 2 +- .../backup.oopen.de/backup-ipt-server.yml | 2 +- .../backup.warenform.de/backup-ipt-server.yml | 2 +- host_vars/cl-01.oopen.de/cl-01-ipt-server.yml | 2 +- host_vars/cl-02.oopen.de/cl-02-ipt-server.yml | 2 +- host_vars/cl-fm.oopen.de/cl-fm-ipt-server.yml | 2 +- .../cl-lubax.oopen.de/cl-lubax-ipt-server.yml | 2 +- .../test.mx.oopen.de/test.mx-ipt-server.yml | 2 +- roles/ipt-server/MIGRATION.md | 12 +- roles/ipt-server/README.md | 34 ++- roles/ipt-server/defaults/main.yml | 24 +- .../etc/ipt-firewall/ban_ipv6.list.sample | 4 +- .../etc/ipt-firewall/default_settings.conf | 50 +++- .../etc/ipt-firewall/include_functions.conf | 48 ++++ .../etc/ipt-firewall/post_declarations.conf | 2 +- .../files/usr/local/sbin/ip6t-firewall-server | 257 +++++++++++++++++- .../files/usr/local/sbin/ipt-firewall-server | 149 +++++++++- roles/ipt-server/tasks/main.yml | 9 + .../etc/ipt-firewall/main_ipv4.conf.j2 | 13 +- .../etc/ipt-firewall/main_ipv6.conf.j2 | 21 +- 22 files changed, 666 insertions(+), 55 deletions(-) diff --git a/extract-fw-host-vars.py b/extract-fw-host-vars.py index 6b9b5eb..aa5f538 100755 --- a/extract-fw-host-vars.py +++ b/extract-fw-host-vars.py @@ -21,7 +21,7 @@ from pathlib import Path # --------------------------------------------------------------------------- -# Defaults matching roles/ipt-firewall/defaults/main.yml +# Defaults matching roles/ipt-server/defaults/main.yml # Only values that differ from these will be emitted. # --------------------------------------------------------------------------- DEFAULTS = { @@ -73,8 +73,19 @@ DEFAULTS = { "fw_http_ports": "$standard_http_ports", "fw_log_cgi_traffic_out": False, "fw_cgi_script_users": "", + "fw_set_max_new_http_conn_per_ip_per_second": True, + "fw_max_http_new_conn_per_ip_per_second": "$default_max_http_new_conn_per_ip_per_second", + "fw_set_max_concurrent_http_conn_per_ip": True, + "fw_max_concurrent_http_conn_per_ip": "$default_max_concurrent_http_conn_per_ip", + "fw_set_max_new_http_conn_per_second": True, + "fw_max_new_http_conn_per_second": "$default_max_new_http_conn_per_second", + "fw_throttle_tcp6_mss_for_https": False, + "fw_tcp6_mss_for_https_val": "$min_tcp6_mss_for_https_val", "fw_mm_server_ips": "", "fw_forward_mm_server_ips": "", + "fw_mm_udp_ports_in": "$standard_mattermost_udp_ports_in", + "fw_mm_tcp_ports_in": "$standard_mattermost_tcp_ports_in", + "fw_mm_udp_ports_out": "$standard_mattermost_udp_ports_out", "fw_smtpd_ips": "", "fw_forward_smtpd_ips": "", "fw_smtpd_additional_listen_ports": "", @@ -141,7 +152,9 @@ DEFAULTS = { "fw_portforward_udp_v4": "", "fw_portforward_tcp_v6": "", "fw_portforward_udp_v6": "", - "fw_blocked_ips": "", + "fw_blocked_ips_v4": "", + "fw_blocked_ips_v6": "", + "fw_ban_ipv6_mask": "$default_ban_ipv6_mask", "fw_block_tcp_ports": "111 113 135 137:139 445", "fw_block_udp_ports": "111 137:139", "fw_create_traffic_counter": True, @@ -207,8 +220,17 @@ MAIN_SHARED = { "http_ports": "fw_http_ports", "log_cgi_traffic_out": "fw_log_cgi_traffic_out", "cgi_script_users": "fw_cgi_script_users", + "set_max_new_http_conn_per_ip_per_second": "fw_set_max_new_http_conn_per_ip_per_second", + "max_http_new_conn_per_ip_per_second": "fw_max_http_new_conn_per_ip_per_second", + "set_max_concurrent_http_conn_per_ip": "fw_set_max_concurrent_http_conn_per_ip", + "max_concurrent_http_conn_per_ip": "fw_max_concurrent_http_conn_per_ip", + "set_max_new_http_conn_per_second": "fw_set_max_new_http_conn_per_second", + "max_new_http_conn_per_second": "fw_max_new_http_conn_per_second", "mm_server_ips": "fw_mm_server_ips", "forward_mm_server_ips": "fw_forward_mm_server_ips", + "mm_udp_ports_in": "fw_mm_udp_ports_in", + "mm_tcp_ports_in": "fw_mm_tcp_ports_in", + "mm_udp_ports_out": "fw_mm_udp_ports_out", "smtpd_ips": "fw_smtpd_ips", "forward_smtpd_ips": "fw_forward_smtpd_ips", "smtpd_additional_listen_ports": "fw_smtpd_additional_listen_ports", @@ -265,7 +287,6 @@ MAIN_SHARED = { "forward_tcp_out_ports": "fw_forward_tcp_out_ports", "udp_out_ports": "fw_udp_out_ports", "forward_udp_out_ports": "fw_forward_udp_out_ports", - "blocked_ips": "fw_blocked_ips", "block_tcp_ports": "fw_block_tcp_ports", "block_udp_ports": "fw_block_udp_ports", "create_traffic_counter": "fw_create_traffic_counter", @@ -307,6 +328,7 @@ MAIN_V4_ONLY = { "kernel_log_martians": "fw_kernel_log_martians", "kernel_deactivate_source_route": "fw_kernel_deactivate_source_route", "kernel_dont_accept_redirects": "fw_kernel_dont_accept_redirects", + "blocked_ips": "fw_blocked_ips_v4", } # IPv6-only variables (from main_ipv6.conf) @@ -325,6 +347,16 @@ MAIN_V6_ONLY = { "xmmp_remote_out_services": "fw_xmmp_remote_out_services_v6", "resolver_allowed_networks": "fw_resolver_allowed_networks_v6", "kernel_forward_between_interfaces": "fw_kernel_forward_between_interfaces", + "throttle_tcp6_mss_for_https": "fw_throttle_tcp6_mss_for_https", + "tcp6_mss_for_https_val": "fw_tcp6_mss_for_https_val", + "ban_ipv6_mask": "fw_ban_ipv6_mask", + "blocked_ips": "fw_blocked_ips_v6", +} + +# Renamed bash variables: old name → new name (applied to $var references) +RENAMED_BASH_VARS = { + "stansard_mattermost_udp_ports_in": "standard_mattermost_udp_ports_in", + "stansard_mattermost_udp_ports_out": "standard_mattermost_udp_ports_out", } @@ -430,6 +462,14 @@ def ssh_cat(host, user, port, path, sudo_password=None): return None +def normalize_value(value): + """Normalize bash variable references: ${var} → $var, renamed vars → new name.""" + value = re.sub(r'\$\{([A-Za-z_][A-Za-z0-9_]*)\}', r'$\1', value) + for old, new in RENAMED_BASH_VARS.items(): + value = re.sub(r'\$' + old + r'\b', '$' + new, value) + return value + + def coerce_bool(value): """Convert bash true/false string to Python bool, or return string.""" if value.lower() in ("true", "yes", "1"): @@ -456,9 +496,11 @@ def yaml_value(v): def build_host_vars(parsed_iface_v4, parsed_iface_v6, parsed_main_v4, parsed_main_v6): """ Map parsed bash variables to Ansible fw_* variables. - Returns dict of {ansible_var: value} containing only non-default values. + Returns (dict of {ansible_var: value} containing only non-default values, + list of warnings). """ result = {} + warnings = [] # --- Interfaces: extract lists from numbered vars --- def extract_list(parsed, prefix, suffix="", count=3): @@ -501,11 +543,17 @@ def build_host_vars(parsed_iface_v4, parsed_iface_v6, parsed_main_v4, parsed_mai if v and v != DEFAULTS.get(ansible_var, ""): result[ansible_var] = v - # --- Shared main variables (read from ipv4) --- + # --- Shared main variables (read from ipv4, fall back to ipv6) --- for bash_var, ansible_var in MAIN_SHARED.items(): - raw = parsed_main_v4.get(bash_var) + raw_v4 = parsed_main_v4.get(bash_var) + raw_v6 = parsed_main_v6.get(bash_var) + if raw_v4 is not None and raw_v6 is not None \ + and normalize_value(raw_v4) != normalize_value(raw_v6): + warnings.append(f" # {ansible_var}: IPv4 '{raw_v4}' != IPv6 '{raw_v6}' — IPv4 value used") + raw = raw_v4 if raw_v4 is not None else raw_v6 if raw is None: continue + raw = normalize_value(raw) v = coerce_bool(raw) if raw.lower() in ("true", "false") else raw default = DEFAULTS.get(ansible_var) if v != default: @@ -516,6 +564,7 @@ def build_host_vars(parsed_iface_v4, parsed_iface_v6, parsed_main_v4, parsed_mai raw = parsed_main_v4.get(bash_var) if raw is None: continue + raw = normalize_value(raw) v = coerce_bool(raw) if raw.lower() in ("true", "false") else raw default = DEFAULTS.get(ansible_var) if v != default: @@ -526,12 +575,13 @@ def build_host_vars(parsed_iface_v4, parsed_iface_v6, parsed_main_v4, parsed_mai raw = parsed_main_v6.get(bash_var) if raw is None: continue + raw = normalize_value(raw) v = coerce_bool(raw) if raw.lower() in ("true", "false") else raw default = DEFAULTS.get(ansible_var) if v != default: result[ansible_var] = v - return result + return result, warnings def render_yaml(hostname, host_vars, all_warnings): @@ -540,7 +590,7 @@ def render_yaml(hostname, host_vars, all_warnings): "---", f"# ipt-firewall configuration for {hostname}", "# Generated by extract-fw-host-vars.py - review before committing!", - "# Place in: host_vars//ipt_firewall.yml", + "# Place in: host_vars//-ipt-server.yml", "", ] @@ -574,7 +624,11 @@ def render_yaml(hostname, host_vars, all_warnings): "fw_allow_local_service_v6", "fw_allow_local_service_from_networks_v6"]), ("SSH", ["fw_ssh_server_ips", "fw_forward_ssh_server_ips", "fw_ssh_ports"]), ("HTTP", ["fw_http_server_ips", "fw_forward_http_server_ips", "fw_http_ports", - "fw_log_cgi_traffic_out", "fw_cgi_script_users"]), + "fw_log_cgi_traffic_out", "fw_cgi_script_users", + "fw_set_max_new_http_conn_per_ip_per_second", "fw_max_http_new_conn_per_ip_per_second", + "fw_set_max_concurrent_http_conn_per_ip", "fw_max_concurrent_http_conn_per_ip", + "fw_set_max_new_http_conn_per_second", "fw_max_new_http_conn_per_second", + "fw_throttle_tcp6_mss_for_https", "fw_tcp6_mss_for_https_val"]), ("Mail", ["fw_smtpd_ips", "fw_forward_smtpd_ips", "fw_smtpd_additional_listen_ports", "fw_smtpd_additional_outgoing_ports", "fw_mail_server_ips", "fw_forward_mail_server_ips", "fw_mail_user_ports", "fw_mail_client_ips", "fw_forward_mail_client_ips", @@ -601,7 +655,8 @@ def render_yaml(hostname, host_vars, all_warnings): "fw_jibri_server_ips", "fw_forward_jibri_server_ips", "fw_jibri_remote_jitsi_server"]), ("TURN / STUN", ["fw_nc_turn_server_ips", "fw_forward_nc_turn_server_ips", "fw_nc_turn_ports", "fw_nc_turn_udp_ports"]), - ("Mattermost", ["fw_mm_server_ips", "fw_forward_mm_server_ips"]), + ("Mattermost", ["fw_mm_server_ips", "fw_forward_mm_server_ips", + "fw_mm_udp_ports_in", "fw_mm_tcp_ports_in", "fw_mm_udp_ports_out"]), ("Prometheus", ["fw_prometheus_local_server_ips", "fw_prometheus_local_client_ips", "fw_prometheus_remote_server_ips"]), ("Xymon", ["fw_xymon_server_ips", "fw_local_xymon_client", "fw_xymon_port"]), @@ -610,7 +665,7 @@ def render_yaml(hostname, host_vars, all_warnings): "fw_udp_out_ports", "fw_forward_udp_out_ports"]), ("Portforwarding", ["fw_portforward_tcp_v4", "fw_portforward_udp_v4", "fw_portforward_tcp_v6", "fw_portforward_udp_v6"]), - ("Block", ["fw_blocked_ips", "fw_block_tcp_ports", "fw_block_udp_ports"]), + ("Block", ["fw_blocked_ips_v4", "fw_blocked_ips_v6", "fw_ban_ipv6_mask", "fw_block_tcp_ports", "fw_block_udp_ports"]), ("Protection / limits", ["fw_protection_against_syn_flooding", "fw_protection_against_port_scanning", "fw_protection_against_ssh_brute_force_attacks", @@ -712,10 +767,11 @@ def main(): if warnings: all_warnings.extend([f" # [{key}] {w}" for w in warnings]) - host_vars = build_host_vars( + host_vars, map_warnings = build_host_vars( parsed["iface_v4"], parsed["iface_v6"], parsed["main_v4"], parsed["main_v6"], ) + all_warnings.extend(map_warnings) yaml_text = render_yaml(hostname, host_vars, all_warnings) diff --git a/host_vars/a.ns.oopen.de/a.ns-ipt-server.yml b/host_vars/a.ns.oopen.de/a.ns-ipt-server.yml index 4593dd3..9ce9388 100644 --- a/host_vars/a.ns.oopen.de/a.ns-ipt-server.yml +++ b/host_vars/a.ns.oopen.de/a.ns-ipt-server.yml @@ -24,4 +24,4 @@ fw_dns_server_ips: $ext_ips fw_wireguard_server_ips: $ext_ips # --- Block -fw_blocked_ips: 222.184.0.0/13 61.160.0.0/16 116.8.0.0/14 +fw_blocked_ips_v4: 222.184.0.0/13 61.160.0.0/16 116.8.0.0/14 diff --git a/host_vars/b.ns.oopen.de/b.ns-ipt-server.yml b/host_vars/b.ns.oopen.de/b.ns-ipt-server.yml index 86fc548..87f4145 100644 --- a/host_vars/b.ns.oopen.de/b.ns-ipt-server.yml +++ b/host_vars/b.ns.oopen.de/b.ns-ipt-server.yml @@ -24,4 +24,4 @@ fw_dns_server_ips: $ext_ips fw_wireguard_server_ips: $ext_ips # --- Block -fw_blocked_ips: 222.184.0.0/13 61.160.0.0/16 116.8.0.0/14 +fw_blocked_ips_v4: 222.184.0.0/13 61.160.0.0/16 116.8.0.0/14 diff --git a/host_vars/backup.oopen.de/backup-ipt-server.yml b/host_vars/backup.oopen.de/backup-ipt-server.yml index f3f3f96..90f0f3a 100644 --- a/host_vars/backup.oopen.de/backup-ipt-server.yml +++ b/host_vars/backup.oopen.de/backup-ipt-server.yml @@ -36,4 +36,4 @@ fw_http_server_ips: $ext_ips fw_tcp_out_ports: 51372 51374 # --- Block -fw_blocked_ips: 222.184.0.0/13 61.160.0.0/16 116.8.0.0/14 +fw_blocked_ips_v4: 222.184.0.0/13 61.160.0.0/16 116.8.0.0/14 diff --git a/host_vars/backup.warenform.de/backup-ipt-server.yml b/host_vars/backup.warenform.de/backup-ipt-server.yml index b589fdb..1b43ec2 100644 --- a/host_vars/backup.warenform.de/backup-ipt-server.yml +++ b/host_vars/backup.warenform.de/backup-ipt-server.yml @@ -21,4 +21,4 @@ munin_remote_ipv6: "2a01:4f9:3070:2bda::227" fw_http_server_ips: $ext_1_ip # --- Block -fw_blocked_ips: 222.184.0.0/13 61.160.0.0/16 116.8.0.0/14 +fw_blocked_ips_v4: 222.184.0.0/13 61.160.0.0/16 116.8.0.0/14 diff --git a/host_vars/cl-01.oopen.de/cl-01-ipt-server.yml b/host_vars/cl-01.oopen.de/cl-01-ipt-server.yml index bdb2f8a..7904fce 100644 --- a/host_vars/cl-01.oopen.de/cl-01-ipt-server.yml +++ b/host_vars/cl-01.oopen.de/cl-01-ipt-server.yml @@ -24,4 +24,4 @@ fw_http_server_ips: $ext_1_ip fw_mail_client_ips: $ext_1_ip # --- Block -fw_blocked_ips: 222.184.0.0/13 61.160.0.0/16 116.8.0.0/14 +fw_blocked_ips_v4: 222.184.0.0/13 61.160.0.0/16 116.8.0.0/14 diff --git a/host_vars/cl-02.oopen.de/cl-02-ipt-server.yml b/host_vars/cl-02.oopen.de/cl-02-ipt-server.yml index a3d706e..08730d0 100644 --- a/host_vars/cl-02.oopen.de/cl-02-ipt-server.yml +++ b/host_vars/cl-02.oopen.de/cl-02-ipt-server.yml @@ -24,4 +24,4 @@ fw_http_server_ips: $ext_1_ip fw_mail_client_ips: $ext_1_ip # --- Block -fw_blocked_ips: 222.184.0.0/13 61.160.0.0/16 116.8.0.0/14 +fw_blocked_ips_v4: 222.184.0.0/13 61.160.0.0/16 116.8.0.0/14 diff --git a/host_vars/cl-fm.oopen.de/cl-fm-ipt-server.yml b/host_vars/cl-fm.oopen.de/cl-fm-ipt-server.yml index 31dd78c..d6fde72 100644 --- a/host_vars/cl-fm.oopen.de/cl-fm-ipt-server.yml +++ b/host_vars/cl-fm.oopen.de/cl-fm-ipt-server.yml @@ -24,4 +24,4 @@ fw_http_server_ips: $ext_1_ip fw_mail_client_ips: $ext_1_ip # --- Block -fw_blocked_ips: 222.184.0.0/13 61.160.0.0/16 116.8.0.0/14 +fw_blocked_ips_v4: 222.184.0.0/13 61.160.0.0/16 116.8.0.0/14 diff --git a/host_vars/cl-lubax.oopen.de/cl-lubax-ipt-server.yml b/host_vars/cl-lubax.oopen.de/cl-lubax-ipt-server.yml index 8751119..e7f0578 100644 --- a/host_vars/cl-lubax.oopen.de/cl-lubax-ipt-server.yml +++ b/host_vars/cl-lubax.oopen.de/cl-lubax-ipt-server.yml @@ -24,4 +24,4 @@ fw_http_server_ips: $ext_1_ip fw_mail_client_ips: $ext_1_ip # --- Block -# fw_blocked_ips: 222.184.0.0/13 61.160.0.0/16 116.8.0.0/14 +# fw_blocked_ips_v4: 222.184.0.0/13 61.160.0.0/16 116.8.0.0/14 diff --git a/host_vars/test.mx.oopen.de/test.mx-ipt-server.yml b/host_vars/test.mx.oopen.de/test.mx-ipt-server.yml index 738368a..4058368 100644 --- a/host_vars/test.mx.oopen.de/test.mx-ipt-server.yml +++ b/host_vars/test.mx.oopen.de/test.mx-ipt-server.yml @@ -36,4 +36,4 @@ fw_mumble_server_ips: 138.201.33.54 fw_rsync_out_ips: $ext_1_ip # --- Block -fw_blocked_ips: 222.184.0.0/13 61.160.0.0/16 116.8.0.0/14 +fw_blocked_ips_v4: 222.184.0.0/13 61.160.0.0/16 116.8.0.0/14 diff --git a/roles/ipt-server/MIGRATION.md b/roles/ipt-server/MIGRATION.md index 8018053..ef50e27 100644 --- a/roles/ipt-server/MIGRATION.md +++ b/roles/ipt-server/MIGRATION.md @@ -17,7 +17,7 @@ Das alte Verfahren: Das neue Verfahren: -- Alle Firewall-Einstellungen liegen in `host_vars/${HOSTNAME}/ipt-server.yml`. +- Alle Firewall-Einstellungen liegen in `host_vars/${HOSTNAME}/${HOSTNAME%.*.*}-ipt-server.yml`. - Ansible deployt die Config-Dateien aus Jinja2-Templates. - Direktes Editieren auf dem Host ist nicht mehr vorgesehen. @@ -57,7 +57,7 @@ cd /path/to/ansible/oopen-server HOSTNAME= ./extract-fw-host-vars.py ${HOSTNAME} --sudo \ - -o host_vars/${HOSTNAME}/ipt-server.yml + -o host_vars/${HOSTNAME}/${HOSTNAME%.*.*}-ipt-server.yml ``` Das Skript fragt einmalig nach dem `sudo`-Passwort. @@ -65,7 +65,7 @@ Das Skript fragt einmalig nach dem `sudo`-Passwort. **Ergebnis prüfen:** ```bash -cat host_vars/${HOSTNAME}/ipt-server.yml +cat host_vars/${HOSTNAME}/${HOSTNAME%.*.*}-ipt-server.yml ``` Kontrollpunkte: @@ -114,7 +114,7 @@ Jetzt wird `fw_manage_config: true` gesetzt, damit Ansible die vier host-spezifischen Config-Dateien aus den Templates schreibt: ```yaml -# host_vars/${HOSTNAME}/ipt-server.yml +# host_vars/${HOSTNAME}/${HOSTNAME%.*.*}-ipt-server.yml --- fw_manage_config: true # ← hinzufügen / auf true setzen @@ -159,7 +159,7 @@ Config-Dateien produzieren exakt dieselben Rules wie die bisher händisch verwalteten. Falls Unterschiede erscheinen: die abweichenden Rules identifizieren, die -entsprechenden Variablen in `host_vars/${HOSTNAME}/ipt-server.yml` nachpflegen, +entsprechenden Variablen in `host_vars/${HOSTNAME}/${HOSTNAME%.*.*}-ipt-server.yml` nachpflegen, erneut ausrollen und den Diff wiederholen. Ab jetzt: @@ -204,7 +204,7 @@ Hinweis (via `{{ ansible_managed }}`): ```ini # Ansible managed # DO NOT EDIT - changes will be overwritten on the next Ansible run. -# Edit host_vars/${HOSTNAME}/ipt-server.yml instead. +# Edit host_vars/${HOSTNAME}/${HOSTNAME%.*.*}-ipt-server.yml instead. ``` --- diff --git a/roles/ipt-server/README.md b/roles/ipt-server/README.md index 374c3de..99f195d 100644 --- a/roles/ipt-server/README.md +++ b/roles/ipt-server/README.md @@ -42,9 +42,14 @@ roles/ipt-server/ Host-spezifische Konfiguration liegt ausschließlich in: ``` -host_vars/${HOSTNAME}/ipt-server.yml +host_vars/${HOSTNAME}/${HOSTNAME%.*.*}-ipt-server.yml ``` +Der Dateiname beginnt mit dem Hostnamen ohne Domain, z.B. +`host_vars/a.mx.oopen.de/a.mx-ipt-server.yml` oder +`host_vars/backup.oopen.de/backup-ipt-server.yml` +(`${HOSTNAME%.*.*}` schneidet die letzten zwei Labels ab). + --- ## Neuen Host aufnehmen @@ -61,7 +66,7 @@ cd /path/to/ansible/oopen-server HOSTNAME= mkdir -p host_vars/${HOSTNAME} -cat > host_vars/${HOSTNAME}/ipt-server.yml << 'EOF' +cat > host_vars/${HOSTNAME}/${HOSTNAME%.*.*}-ipt-server.yml << 'EOF' --- fw_manage_config: true @@ -111,7 +116,7 @@ Was passiert: Alle Änderungen erfolgen ausschließlich in der host_vars-Datei des Hosts: ``` -host_vars/${HOSTNAME}/ipt-server.yml +host_vars/${HOSTNAME}/${HOSTNAME%.*.*}-ipt-server.yml ``` Danach: @@ -132,7 +137,7 @@ tatsächlichen Änderungen wird die Firewall neu gestartet. ### Beispiel: HTTP-Server aktivieren ```yaml -# host_vars/${HOSTNAME}/ipt-server.yml +# host_vars/${HOSTNAME}/${HOSTNAME%.*.*}-ipt-server.yml fw_http_server_ips: "$ext_ips" # oder konkrete IP ``` @@ -172,8 +177,17 @@ chmod 750 $DST/ipt-firewall-server $DST/ip6t-firewall-server Ebenso für geteilte Conf-Dateien in `roles/ipt-server/files/etc/ipt-firewall/`. +Kommen dabei neue Variablen in `main_ipv4.conf.sample` / `main_ipv6.conf.sample` +hinzu, müssen sie zusätzlich nachgezogen werden in: + +- `defaults/main.yml` (als `fw_*`-Variable mit Default), +- `templates/etc/ipt-firewall/main_ipv{4,6}.conf.j2`, +- `extract-fw-host-vars.py` (`DEFAULTS`, `MAIN_SHARED` bzw. `MAIN_V4_ONLY`/`MAIN_V6_ONLY`, + Sektion in `render_yaml`). + Nach dem Commit werden die neuen Skripte beim nächsten Ansible-Run auf alle -Hosts deployed. +Hosts deployed. Geänderte Skripte oder geteilte Conf-Dateien lösen einen +Neustart der Firewall aus. --- @@ -188,6 +202,16 @@ Hosts deployed. | `fw_ssh_server_ips` | `"$ext_ips"` | IPs auf denen SSH erlaubt ist | | `fw_ssh_ports` | `"$standard_ssh_port"` | SSH-Port(s) | | `fw_http_server_ips` | `""` | IPs auf denen HTTP/HTTPS erlaubt ist | +| `fw_set_max_new_http_conn_per_ip_per_second` | `true` | Neue HTTP(S)-Verbindungen/s pro Quell-IP begrenzen | +| `fw_max_http_new_conn_per_ip_per_second` | `"$default_max_http_new_conn_per_ip_per_second"` | Limit dazu | +| `fw_set_max_concurrent_http_conn_per_ip` | `true` | Gleichzeitige HTTP(S)-Verbindungen pro Quell-IP begrenzen | +| `fw_max_concurrent_http_conn_per_ip` | `"$default_max_concurrent_http_conn_per_ip"` | Limit dazu | +| `fw_set_max_new_http_conn_per_second` | `true` | SYN-Rate auf 80/443 insgesamt begrenzen | +| `fw_max_new_http_conn_per_second` | `"$default_max_new_http_conn_per_second"` | Limit dazu | +| `fw_throttle_tcp6_mss_for_https` | `false` | Nur IPv6: MSS eingehender HTTPS-SYNs clampen | +| `fw_tcp6_mss_for_https_val` | `"$min_tcp6_mss_for_https_val"` | Nur IPv6: MSS-Wert (z.B. `1360`) | +| `fw_mm_tcp_ports_in` | `"$standard_mattermost_tcp_ports_in"` | Mattermost TCP-Fallback (STUN) | +| `fw_ban_ipv6_mask` | `"$default_ban_ipv6_mask"` | Nur IPv6: Präfixlänge für `ban_ipv6.list`-Einträge ohne Maske | | `munin_remote_ipv4` | `""` | Munin-Server IPv4 | | `munin_remote_ipv6` | `""` | Munin-Server IPv6 | diff --git a/roles/ipt-server/defaults/main.yml b/roles/ipt-server/defaults/main.yml index d043198..4f6a1ce 100644 --- a/roles/ipt-server/defaults/main.yml +++ b/roles/ipt-server/defaults/main.yml @@ -2,7 +2,7 @@ # --- # ipt-firewall role defaults -# Override per host in host_vars//ipt_firewall.yml +# Override per host in host_vars//-ipt-server.yml # --- @@ -156,6 +156,19 @@ fw_http_ports: "$standard_http_ports" fw_log_cgi_traffic_out: false fw_cgi_script_users: "" +# Restrictions on HTTP(S) connections (ports 80,443) to counteract attacks. +# Values default to the 'default_*' settings in default_settings.conf. +fw_set_max_new_http_conn_per_ip_per_second: true +fw_max_http_new_conn_per_ip_per_second: "$default_max_http_new_conn_per_ip_per_second" +fw_set_max_concurrent_http_conn_per_ip: true +fw_max_concurrent_http_conn_per_ip: "$default_max_concurrent_http_conn_per_ip" +fw_set_max_new_http_conn_per_second: true +fw_max_new_http_conn_per_second: "$default_max_new_http_conn_per_second" + +# IPv6 only: clamp the advertised MSS of incoming HTTPS SYN packets. +fw_throttle_tcp6_mss_for_https: false +fw_tcp6_mss_for_https_val: "$min_tcp6_mss_for_https_val" + # --- # Services: Mattermost @@ -163,8 +176,9 @@ fw_cgi_script_users: "" fw_mm_server_ips: "" fw_forward_mm_server_ips: "" -fw_mm_udp_ports_in: "$stansard_mattermost_udp_ports_in" -fw_mm_udp_ports_out: "$stansard_mattermost_udp_ports_out" +fw_mm_udp_ports_in: "$standard_mattermost_udp_ports_in" +fw_mm_tcp_ports_in: "$standard_mattermost_tcp_ports_in" +fw_mm_udp_ports_out: "$standard_mattermost_udp_ports_out" # --- @@ -320,7 +334,9 @@ fw_portforward_udp_v6: "" # Blocked IPs / ports # --- -fw_blocked_ips: "" +fw_blocked_ips_v4: "" +fw_blocked_ips_v6: "" +fw_ban_ipv6_mask: "$default_ban_ipv6_mask" # IPv6 only: prefix length for ban_ipv6.list entries without mask fw_block_tcp_ports: "111 113 135 137:139 445" fw_block_udp_ports: "111 137:139" diff --git a/roles/ipt-server/files/etc/ipt-firewall/ban_ipv6.list.sample b/roles/ipt-server/files/etc/ipt-firewall/ban_ipv6.list.sample index bbc3a73..5719910 100644 --- a/roles/ipt-server/files/etc/ipt-firewall/ban_ipv6.list.sample +++ b/roles/ipt-server/files/etc/ipt-firewall/ban_ipv6.list.sample @@ -10,8 +10,10 @@ # - # - # - Note: -# - - If no mask is given mask will be set to '64' +# - - If no mask is given mask will be set to 'ban_ipv6_mask' (main_ipv6.conf), +# - default is '128' # - - wrong addresses like '2g01::1' or '2a01::1/129' will be ignored +# - - entries covering an own (global) address of this host will be ignored # - # - Example: # - 240e:ec:4ab1:feba:e8b4:4fb1:7984:4c diff --git a/roles/ipt-server/files/etc/ipt-firewall/default_settings.conf b/roles/ipt-server/files/etc/ipt-firewall/default_settings.conf index d212560..c6a0f24 100644 --- a/roles/ipt-server/files/etc/ipt-firewall/default_settings.conf +++ b/roles/ipt-server/files/etc/ipt-firewall/default_settings.conf @@ -4,7 +4,7 @@ # --- Default Parameter / Options # ------------- -default_per_IP_connection_limit=111 +default_per_IP_connection_limit=250 # ------------- @@ -56,8 +56,9 @@ standard_prometheus_ports="9100,9256" # - Mattermost (MM) Service # - -stansard_mattermost_udp_ports_in="8443" -stansard_mattermost_udp_ports_out="3478" +standard_mattermost_udp_ports_in="8443" +standard_mattermost_tcp_ports_in="8443" +standard_mattermost_udp_ports_out="3478" # - IPsec - Internet Security Association and # - Key Management Protocol @@ -70,6 +71,37 @@ standard_ipsec_nat_t=4500 standard_http_ports="80,443" standard_mailuser_ports="587,465,110,995,143,993" + +# ip6t-firewall-server: +# Clamp the advertised MSS of incoming HTTPS SYN packets to 1360. +# +throttle_tcp6_mss_for_https=false +min_tcp6_mss_for_https_val=1280 + +# ip6t-firewall-server,ipt-firewall-server: +# some restrictions on HTTP connections to counteract attacks. +# +# The default value used to be set to '30' +# +set_max_new_http_conn_per_ip_per_second=true +default_max_http_new_conn_per_ip_per_second=200 + +# The default value used to be set to '50' +# +set_max_concurrent_http_conn_per_ip=true +default_max_concurrent_http_conn_per_ip=100 + +# The default value used to be set to '200' +# +set_max_new_http_conn_per_second=true +default_max_new_http_conn_per_second=400 + +# ip6t-firewall-server: +# Prefix length for entries in 'ban_ipv6.list' given without mask. +# +default_ban_ipv6_mask=128 + + # - Dovecot Service # - dovecot_external_auth_port="44444" @@ -85,20 +117,20 @@ default_jitsi_dovecout_auth_port="$dovecot_external_auth_port" default_jibri_out_port=5222 # default_outbound_streaming_tcp_ports # -# - outbound port 1935/TCP : outbound streaming over RTMP to most +# - outbound port 1935/TCP : outbound streaming over RTMP to most # streaming providers such as YouTube Live, Vimeo or Twitch # -# - outbound port 1936/TCP : outbound streaming over RTMP to LinkedIn +# - outbound port 1936/TCP : outbound streaming over RTMP to LinkedIn # Live (port 1935 is also used for RTMP streaming to LinkedIn) # -# - outbound ports 2935/TCP and 2396/TCP : outbound streaming over +# - outbound ports 2935/TCP and 2396/TCP : outbound streaming over # RTMPS to LinkedIn Live # -# - outbound port 443/TCP (HTTPS) : used for authentication with the -# built-in providers such as YouTube Live, Facebook Live, Ustream, +# - outbound port 443/TCP (HTTPS) : used for authentication with the +# built-in providers such as YouTube Live, Facebook Live, Ustream, # Livestream, and Twitch # -# - outbound port 53/UDP (DNS) used for DNS lookups converting +# - outbound port 53/UDP (DNS) used for DNS lookups converting # hostnames to IP addresses # default_outbound_streaming_tcp_ports="1935,1936,2935,2396" diff --git a/roles/ipt-server/files/etc/ipt-firewall/include_functions.conf b/roles/ipt-server/files/etc/ipt-firewall/include_functions.conf index ae7182c..8c15646 100644 --- a/roles/ipt-server/files/etc/ipt-firewall/include_functions.conf +++ b/roles/ipt-server/files/etc/ipt-firewall/include_functions.conf @@ -73,6 +73,54 @@ containsElement () { return 1 } +## - Expand a given IPv6 address (parameter 1) to 32 hex digits without colons. +## - Returns 1 if the address cannot be parsed. +## - +ipv6_expand() { + local _addr="${1,,}" + local _head _tail _group _n _out="" + local -a _h=() _t=() + + if [[ "$_addr" == *::* ]] ; then + _head="${_addr%%::*}" + _tail="${_addr#*::}" + [[ "$_tail" == *::* ]] && return 1 + [[ -n "$_head" ]] && IFS=':' read -ra _h <<< "$_head" + [[ -n "$_tail" ]] && IFS=':' read -ra _t <<< "$_tail" + _n=$(( 8 - ${#_h[@]} - ${#_t[@]} )) + [[ $_n -lt 1 ]] && return 1 + for (( ; _n > 0; _n-- )) ; do + _h+=("0") + done + _h+=("${_t[@]}") + else + IFS=':' read -ra _h <<< "$_addr" + fi + + [[ ${#_h[@]} -eq 8 ]] || return 1 + for _group in "${_h[@]}" ; do + [[ "$_group" =~ ^[0-9a-f]{1,4}$ ]] || return 1 + _out+="$(printf '%04x' "0x$_group")" + done + + echo "$_out" +} + +## - Check if a given IPv6 address (parameter 1) lies within the network +## - given by address (parameter 2) and prefix length (parameter 3). +## - +ipv6_in_net() { + local _a _n _full _rest + _a="$(ipv6_expand "$1")" || return 1 + _n="$(ipv6_expand "$2")" || return 1 + _full=$(( $3 / 4 )) + _rest=$(( $3 % 4 )) + + [[ "${_a:0:_full}" == "${_n:0:_full}" ]] || return 1 + [[ $_rest -eq 0 ]] && return 0 + (( (0x${_a:_full:1} >> (4 - _rest)) == (0x${_n:_full:1} >> (4 - _rest)) )) +} + is_number() { return $(test ! -z "${1##*[!0-9]*}" > /dev/null 2>&1); diff --git a/roles/ipt-server/files/etc/ipt-firewall/post_declarations.conf b/roles/ipt-server/files/etc/ipt-firewall/post_declarations.conf index e6a8137..160f339 100644 --- a/roles/ipt-server/files/etc/ipt-firewall/post_declarations.conf +++ b/roles/ipt-server/files/etc/ipt-firewall/post_declarations.conf @@ -60,7 +60,7 @@ done # --- declare -a ext_ip_arr for _ip in $ext_ips ; do - host_ip_arr+=("$_ip") + ext_ip_arr+=("$_ip") done # --- diff --git a/roles/ipt-server/files/usr/local/sbin/ip6t-firewall-server b/roles/ipt-server/files/usr/local/sbin/ip6t-firewall-server index 052a608..9ddd69d 100755 --- a/roles/ipt-server/files/usr/local/sbin/ip6t-firewall-server +++ b/roles/ipt-server/files/usr/local/sbin/ip6t-firewall-server @@ -448,6 +448,29 @@ fi +# --- +# - ICMPv6 Neighbor Discovery and Packet Too Big +# --- +# Accept these before any rule blocking by source address (blocked_ips, +# ban_ipv6.list). Otherwise a blocked network containing a router of the +# own link breaks neighbor discovery - the router can no longer reach this host. +# +# 2 = packet-too-big +# 133= router-solicitation +# 134= router-advertisement +# 135= neighbor-solicitation +# 136= neighbor-advertisement +# --- +echononl "\tICMPv6 neighbor discovery / packet-too-big (before blocking rules).." +for t in 2 133 134 135 136 ; do + $ip6t -A INPUT -p ipv6-icmp --icmpv6-type $t -j ACCEPT +done +if $kernel_forward_between_interfaces ; then + $ip6t -A FORWARD -p ipv6-icmp --icmpv6-type 2 -j ACCEPT +fi +echo_done + + # ------------- # --- Block IPs / Networks / Interfaces # ------------- @@ -509,6 +532,19 @@ if [[ -f "$conf_ban_ipv6_list" ]] ; then declare -a ban_ipv6_arr=() declare -a no_valid_ipv6=() + declare -a own_ipv6_banned_arr=() + + if [[ -z "$ban_ipv6_mask" || ! "$ban_ipv6_mask" =~ ^[0-9]+$ ]] || [[ $ban_ipv6_mask -gt 128 ]] ; then + ban_ipv6_mask=${default_ban_ipv6_mask:-128} + fi + + # Own global IPv6 addresses (read at runtime, also covers SLAAC addresses). + # Entries covering one of them would block the own prefix including its routers. + # + declare -a own_ipv6_arr=() + for _own_ip in $(ip -6 -o addr show scope global | awk '{split($4, a, "/"); print a[1]}') ; do + own_ipv6_arr+=("$_own_ip") + done # Regex valid ipv6 address # @@ -568,7 +604,7 @@ if [[ -f "$conf_ban_ipv6_list" ]] ; then fi fi else - mask=64 + mask=$ban_ipv6_mask is_valid_mask=true fi @@ -586,6 +622,22 @@ if [[ -f "$conf_ban_ipv6_list" ]] ; then continue fi + # Never ban a network containing an own address + # + _covers_own_ip=false + for _own_ip in "${own_ipv6_arr[@]}" ; do + if ipv6_in_net "$_own_ip" "$ipv6" "$mask" ; then + _covers_own_ip=true + break + fi + done + if $_covers_own_ip ; then + if ! containsElement "$_ip" "${own_ipv6_banned_arr[@]}" ; then + own_ipv6_banned_arr+=("$_ip") + fi + continue + fi + for _dev in ${ext_if_arr[@]} ; do if $log_blocked_ip || $log_all ; then $ip6t -A INPUT -i $_dev -s $_ip -j $LOG_TARGET $tag_log_prefix "$log_prefix Blocked by ban_ipv6.list: " @@ -614,6 +666,9 @@ if [[ -f "$conf_ban_ipv6_list" ]] ; then if [[ ${#no_valid_ipv6_arr[@]} -gt 0 ]]; then warn "Ignored: ${no_valid_ipv6_arr[@]}" fi + if [[ ${#own_ipv6_banned_arr[@]} -gt 0 ]]; then + warn "Ignored (covers own address): ${own_ipv6_banned_arr[@]}" + fi else echo_skipped fi @@ -644,6 +699,59 @@ echo_skipped #echo_done +# --- +# - IPv6 TCP MSS adjustment for incoming HTTPS connections +# --- +# The IPv6 path MTU can be smaller than the interface MTU. +# Clamp the advertised MSS of incoming HTTPS SYN packets to 1360. +# This prevents oversized TCP segments in server responses. +# +# Tested on oolm-shop on 2026-10-26. +# --- + +echononl "\tAdjust IPv6 TCP MSS for HTTPS connections" + +if $throttle_tcp6_mss_for_https ; then + + if [[ -n "$tcp6_mss_for_https_val" ]] ; then + + # Notice: + # + # - take care 'tcp6_mss_for_https_val' consists only of digits (is a natural number) + # + # - 1280 is the minimum IPv6 packet size that all routers must be able to process + # + if [[ "$tcp6_mss_for_https_val" =~ ^[0-9]+$ ]] ; then + + if [[ "$tcp6_mss_for_https_val" -lt 1280 ]]; then + + tcp6_mss_for_https_val=$min_tcp6_mss_for_https_val + + fi + + fi + + else + + tcp6_mss_for_https_val=$min_tcp6_mss_for_https_val + fi + + for _dev in ${ext_if_arr[@]} ; do + $ip6t -t mangle -A PREROUTING -i $_dev \ + -p tcp --dport 443 \ + --tcp-flags SYN,RST SYN \ + -j TCPMSS --set-mss $tcp6_mss_for_https_val + done + + echo_done + +else + + echo_skipped + +fi + + # --- # Drop TCP packets that are new and are not SYN # --- @@ -709,13 +817,18 @@ echo_done # --- echononl "\tBlock spoofed (own ip) packets" + +# Note: +# Exclude loopback - local traffic to an own address (e.g. a web application +# requesting its own domain) passes PREROUTING via 'lo' with src = dst. +# if $log_spoofed || $log_all ; then for _ip in ${ext_ip_arr[@]} ; do - $ip6t -t mangle -A PREROUTING -s $_ip -d $_ip -j $LOG_TARGET $tag_log_prefix "$log_prefix Spoofed (own ip): " + $ip6t -t mangle -A PREROUTING ! -i lo -s $_ip -d $_ip -j $LOG_TARGET $tag_log_prefix "$log_prefix Spoofed (own ip): " done fi for _ip in ${ext_ip_arr[@]} ; do - $ip6t -t mangle -A PREROUTING -s $_ip -d $_ip -j DROP + $ip6t -t mangle -A PREROUTING ! -i lo -s $_ip -d $_ip -j DROP done echo_done @@ -2057,6 +2170,134 @@ echo_done # - HTTP(S) (local) Webserver # --- +echo "" + +# Note: +# The following rules only DROP/REJECT excess connections - they never ACCEPT. +# Ports are opened exclusively by the rules for 'http_server_ips' below. +# Without local webserver (empty 'http_server_ips') no rules are created. +# +echononl "\t\tA maximum number new connections per second per IP" + +if [[ ${#http_server_ip_arr[@]} -gt 0 ]] && $set_max_new_http_conn_per_ip_per_second ; then + + if [[ -z "$max_http_new_conn_per_ip_per_second" || ! "$max_http_new_conn_per_ip_per_second" =~ ^[0-9]+$ ]]; then + + max_http_new_conn_per_ip_per_second=$default_max_http_new_conn_per_ip_per_second + + fi + + burst_http_new_conn_per_ip_per_second=$((max_http_new_conn_per_ip_per_second * 2)) + + if $log_rejected || $log_all ; then + $ip6t -A INPUT -p tcp -m multiport --dports $http_ports -m conntrack --ctstate NEW \ + -m hashlimit --hashlimit-name http_v6_log --hashlimit-mode srcip \ + --hashlimit-above ${max_http_new_conn_per_ip_per_second}/second \ + --hashlimit-burst $burst_http_new_conn_per_ip_per_second \ + --hashlimit-htable-expire 600000 \ + -m limit --limit 10/second --limit-burst 20 \ + -j $LOG_TARGET $tag_log_prefix "$log_prefix more then $max_http_new_conn_per_ip_per_second new connections per second" + fi + + # HTTP/HTTPS: neue Verbindungen pro Quell-IP begrenzen + # Über 'max_http_new_conn_per_ip_per_second' neue Verbindungen/s pro IP werden gedroppt + # Eintrag wird nach 600000 ms = 10 Minuten gelöscht (--hashlimit-htable-expire 600000). + $ip6t -A INPUT -p tcp -m multiport --dports $http_ports -m conntrack --ctstate NEW \ + -m hashlimit --hashlimit-name http_v6 --hashlimit-mode srcip \ + --hashlimit-above ${max_http_new_conn_per_ip_per_second}/second \ + --hashlimit-burst $burst_http_new_conn_per_ip_per_second \ + --hashlimit-htable-expire 600000 -j DROP + + echo_done + +else + + echo_skipped + +fi + + +echononl "\t\tA maximum number of concurrent TCP connections per IP" + +if [[ ${#http_server_ip_arr[@]} -gt 0 ]] && $set_max_concurrent_http_conn_per_ip ; then + + if [[ -z "$max_concurrent_http_conn_per_ip" || ! "$max_concurrent_http_conn_per_ip" =~ ^[0-9]+$ ]]; then + + max_concurrent_http_conn_per_ip=$default_max_concurrent_http_conn_per_ip + + fi + + if $log_rejected || $log_all ; then + $ip6t -A INPUT -p tcp -m multiport --dports $http_ports --syn \ + -m connlimit --connlimit-above $max_concurrent_http_conn_per_ip --connlimit-mask 128 \ + -m limit --limit 10/second --limit-burst 20 \ + -j $LOG_TARGET $tag_log_prefix "$log_prefix more then $max_concurrent_http_conn_per_ip concurrent connections per IP." + fi + + # HTTP/HTTPS: gleichzeitige Verbindungen pro IP begrenzen + # Mehr als 'max_concurrent_http_conn_per_ip' parallele TCP-Verbindungen pro IP ablehnen + # + # Limit gilt pro einzelner IPv6-Adresse (--connlimit-mask 128) (z.bsp. 2a01:599:108:40b0:e1a9:170b:ba18:fd58/128) + $ip6t -A INPUT -p tcp -m multiport --dports $http_ports --syn \ + -m connlimit --connlimit-above $max_concurrent_http_conn_per_ip --connlimit-mask 128 \ + -j REJECT --reject-with tcp-reset + + echo_done + +else + + echo_skipped + +fi + + +echononl "\t\tA maximum number of new TCP handshakes per second." + +if [[ ${#http_server_ip_arr[@]} -gt 0 ]] && $set_max_new_http_conn_per_second ; then + + if [[ -z "$max_new_http_conn_per_second" || ! "$max_new_http_conn_per_second" =~ ^[0-9]+$ ]]; then + + max_new_http_conn_per_second=$default_max_new_http_conn_per_second + + fi + + burst_max_new_http_conn_per_second=$((2 * max_new_http_conn_per_second)) + + # SYN-Rate begrenzen (global, alle Quell-IPs zusammen) + # + # hashlimit ohne '--hashlimit-mode' verwendet einen einzigen Bucket für alle + # Pakete. SYN-Pakete oberhalb des Limits werden gedroppt; alles darunter + # läuft weiter zu den Regeln für 'http_server_ips' (kein ACCEPT hier!). + # + # Kurzfristig auch bis zu 'burst_max_new_http_conn_per_second' SYN-Pakete erlaubt. + if $log_rejected || $log_all ; then + $ip6t -A INPUT -p tcp -m multiport --dports $http_ports \ + --syn \ + -m hashlimit --hashlimit-name http_v6_syn_log \ + --hashlimit-above ${max_new_http_conn_per_second}/second \ + --hashlimit-burst $burst_max_new_http_conn_per_second \ + -m limit --limit 10/second --limit-burst 20 \ + -j $LOG_TARGET $tag_log_prefix "$log_prefix SYN-Overflow." + fi + + $ip6t -A INPUT -p tcp -m multiport --dports $http_ports \ + --syn \ + -m hashlimit --hashlimit-name http_v6_syn \ + --hashlimit-above ${max_new_http_conn_per_second}/second \ + --hashlimit-burst $burst_max_new_http_conn_per_second \ + -j DROP + + echo_done + +else + + echo_skipped + +fi + +echo "" + + echononl "\t\tHTTP(S) (local) Webserver" if [[ ${#http_server_ip_arr[@]} -gt 0 ]] || [[ ${#forward_http_server_ip_arr[@]} -gt 0 ]] ; then @@ -2089,12 +2330,22 @@ if [[ ${#mm_server_ip_arr[@]} -gt 0 ]] || [[ ${#forward_mm_server_ip_arr[@]} -gt if [[ ${#mm_server_ip_arr[@]} -gt 0 ]] ; then for _ip in ${mm_server_ip_arr[@]} ; do $ip6t -A INPUT -p udp -d $_ip -m multiport --dports $mm_udp_ports_in -m conntrack --ctstate NEW -j ACCEPT + if [[ -n "$mm_tcp_ports_in" ]] ; then + $ip6t -A INPUT -p tcp -d $_ip -m multiport --dports $mm_tcp_ports_in -m conntrack --ctstate NEW -j ACCEPT + else + $ip6t -A INPUT -p tcp -d $_ip -m multiport --dports $mm_udp_ports_in -m conntrack --ctstate NEW -j ACCEPT + fi $ip6t -A OUTPUT -p udp -s $_ip -m multiport --dports $mm_udp_ports_out -m conntrack --ctstate NEW -j ACCEPT done if [[ ${#forward_mm_server_ip_arr[@]} -gt 0 ]] && $kernel_forward_between_interfaces ; then for _ip in ${forward_mm_server_ip_arr[@]} ; do $ip6t -A FORWARD -p udp -d $_ip -m multiport --dports $mm_udp_ports_in -m conntrack --ctstate NEW -j ACCEPT + if [[ -n "$mm_tcp_ports_in" ]] ; then + $ip6t -A FORWARD -p tcp -d $_ip -m multiport --dports $mm_tcp_ports_in -m conntrack --ctstate NEW -j ACCEPT + else + $ip6t -A FORWARD -p tcp -d $_ip -m multiport --dports $mm_udp_ports_in -m conntrack --ctstate NEW -j ACCEPT + fi $ip6t -A FORWARD -p udp -s $_ip -m multiport --dports $mm_udp_ports_out -m conntrack --ctstate NEW -j ACCEPT done fi diff --git a/roles/ipt-server/files/usr/local/sbin/ipt-firewall-server b/roles/ipt-server/files/usr/local/sbin/ipt-firewall-server index ea05b4b..57b311e 100755 --- a/roles/ipt-server/files/usr/local/sbin/ipt-firewall-server +++ b/roles/ipt-server/files/usr/local/sbin/ipt-firewall-server @@ -814,13 +814,18 @@ echo_done # --- echononl "\tBlock spoofed (own ip) packets" + +# Note: +# Exclude loopback - local traffic to an own address (e.g. a web application +# requesting its own domain) passes PREROUTING via 'lo' with src = dst. +# if $log_spoofed || $log_all ; then for _ip in ${ext_ip_arr[@]} ; do - $ipt -t mangle -A PREROUTING -s $_ip -d $_ip -j $LOG_TARGET $tag_log_prefix "$log_prefix Spoofed (own ip): " + $ipt -t mangle -A PREROUTING ! -i lo -s $_ip -d $_ip -j $LOG_TARGET $tag_log_prefix "$log_prefix Spoofed (own ip): " done fi for _ip in ${ext_ip_arr[@]} ; do - $ipt -t mangle -A PREROUTING -s $_ip -d $_ip -j DROP + $ipt -t mangle -A PREROUTING ! -i lo -s $_ip -d $_ip -j DROP done echo_done @@ -2241,6 +2246,134 @@ echo_done # - HTTP(S) (local) Webserver # --- +echo "" + +# Note: +# The following rules only DROP/REJECT excess connections - they never ACCEPT. +# Ports are opened exclusively by the rules for 'http_server_ips' below. +# Without local webserver (empty 'http_server_ips') no rules are created. +# +echononl "\t\tA maximum number new connections per second per IP" + +if [[ ${#http_server_ip_arr[@]} -gt 0 ]] && $set_max_new_http_conn_per_ip_per_second ; then + + if [[ -z "$max_http_new_conn_per_ip_per_second" || ! "$max_http_new_conn_per_ip_per_second" =~ ^[0-9]+$ ]]; then + + max_http_new_conn_per_ip_per_second=$default_max_http_new_conn_per_ip_per_second + + fi + + burst_http_new_conn_per_ip_per_second=$((max_http_new_conn_per_ip_per_second * 2)) + + if $log_rejected || $log_all ; then + $ipt -A INPUT -p tcp -m multiport --dports $http_ports -m conntrack --ctstate NEW \ + -m hashlimit --hashlimit-name http_v4_log --hashlimit-mode srcip \ + --hashlimit-above ${max_http_new_conn_per_ip_per_second}/second \ + --hashlimit-burst $burst_http_new_conn_per_ip_per_second \ + --hashlimit-htable-expire 600000 \ + -m limit --limit 10/second --limit-burst 20 \ + -j $LOG_TARGET $tag_log_prefix "$log_prefix more then $max_http_new_conn_per_ip_per_second new connections per second" + fi + + # HTTP/HTTPS: neue Verbindungen pro Quell-IP begrenzen + # Über 'max_http_new_conn_per_ip_per_second' neue Verbindungen/s pro IP werden gedroppt + # Eintrag wird nach 600000 ms = 10 Minuten gelöscht (--hashlimit-htable-expire 600000). + $ipt -A INPUT -p tcp -m multiport --dports $http_ports -m conntrack --ctstate NEW \ + -m hashlimit --hashlimit-name http_v4 --hashlimit-mode srcip \ + --hashlimit-above ${max_http_new_conn_per_ip_per_second}/second \ + --hashlimit-burst $burst_http_new_conn_per_ip_per_second \ + --hashlimit-htable-expire 600000 -j DROP + + echo_done + +else + + echo_skipped + +fi + + +echononl "\t\tA maximum number of concurrent TCP connections per IP" + +if [[ ${#http_server_ip_arr[@]} -gt 0 ]] && $set_max_concurrent_http_conn_per_ip ; then + + if [[ -z "$max_concurrent_http_conn_per_ip" || ! "$max_concurrent_http_conn_per_ip" =~ ^[0-9]+$ ]]; then + + max_concurrent_http_conn_per_ip=$default_max_concurrent_http_conn_per_ip + + fi + + if $log_rejected || $log_all ; then + $ipt -A INPUT -p tcp -m multiport --dports $http_ports --syn \ + -m connlimit --connlimit-above $max_concurrent_http_conn_per_ip --connlimit-mask 32 \ + -m limit --limit 10/second --limit-burst 20 \ + -j $LOG_TARGET $tag_log_prefix "$log_prefix more then $max_concurrent_http_conn_per_ip concurrent connections per IP." + fi + + # HTTP/HTTPS: gleichzeitige Verbindungen pro IP begrenzen + # Mehr als 'max_concurrent_http_conn_per_ip' parallele TCP-Verbindungen pro IP ablehnen + # + # Limit gilt pro einzelner IPv4-Adresse (--connlimit-mask 32) + $ipt -A INPUT -p tcp -m multiport --dports $http_ports --syn \ + -m connlimit --connlimit-above $max_concurrent_http_conn_per_ip --connlimit-mask 32 \ + -j REJECT --reject-with tcp-reset + + echo_done + +else + + echo_skipped + +fi + + +echononl "\t\tA maximum number of new TCP handshakes per second." + +if [[ ${#http_server_ip_arr[@]} -gt 0 ]] && $set_max_new_http_conn_per_second ; then + + if [[ -z "$max_new_http_conn_per_second" || ! "$max_new_http_conn_per_second" =~ ^[0-9]+$ ]]; then + + max_new_http_conn_per_second=$default_max_new_http_conn_per_second + + fi + + burst_max_new_http_conn_per_second=$((2 * max_new_http_conn_per_second)) + + # SYN-Rate begrenzen (global, alle Quell-IPs zusammen) + # + # hashlimit ohne '--hashlimit-mode' verwendet einen einzigen Bucket für alle + # Pakete. SYN-Pakete oberhalb des Limits werden gedroppt; alles darunter + # läuft weiter zu den Regeln für 'http_server_ips' (kein ACCEPT hier!). + # + # Kurzfristig auch bis zu 'burst_max_new_http_conn_per_second' SYN-Pakete erlaubt. + if $log_rejected || $log_all ; then + $ipt -A INPUT -p tcp -m multiport --dports $http_ports \ + --syn \ + -m hashlimit --hashlimit-name http_v4_syn_log \ + --hashlimit-above ${max_new_http_conn_per_second}/second \ + --hashlimit-burst $burst_max_new_http_conn_per_second \ + -m limit --limit 10/second --limit-burst 20 \ + -j $LOG_TARGET $tag_log_prefix "$log_prefix SYN-Overflow." + fi + + $ipt -A INPUT -p tcp -m multiport --dports $http_ports \ + --syn \ + -m hashlimit --hashlimit-name http_v4_syn \ + --hashlimit-above ${max_new_http_conn_per_second}/second \ + --hashlimit-burst $burst_max_new_http_conn_per_second \ + -j DROP + + echo_done + +else + + echo_skipped + +fi + +echo "" + + echononl "\t\tHTTP(S) (local) Webserver" if [[ ${#http_server_ip_arr[@]} -gt 0 ]] || [[ ${#forward_http_server_ip_arr[@]} -gt 0 ]] ; then @@ -2273,12 +2406,22 @@ if [[ ${#mm_server_ip_arr[@]} -gt 0 ]] || [[ ${#forward_mm_server_ip_arr[@]} -gt if [[ ${#mm_server_ip_arr[@]} -gt 0 ]] ; then for _ip in ${mm_server_ip_arr[@]} ; do $ipt -A INPUT -p udp -d $_ip -m multiport --dports $mm_udp_ports_in -m conntrack --ctstate NEW -j ACCEPT - $ipt -A OUTPUT -p udp -s $_ip -m multiport --dports $mm_udp_ports_out -m conntrack --ctstate NEW -j ACCEPT + if [[ -n "$mm_tcp_ports_in" ]] ; then + $ipt -A INPUT -p tcp -d $_ip -m multiport --dports $mm_tcp_ports_in -m conntrack --ctstate NEW -j ACCEPT + else + $ipt -A INPUT -p tcp -d $_ip -m multiport --dports $mm_udp_ports_in -m conntrack --ctstate NEW -j ACCEPT + fi + $ipt -A OUTPUT -p udp -s $_ip -m multiport --dports $mm_udp_ports_out -m conntrack --ctstate NEW -j ACCEPT done if [[ ${#forward_mm_server_ip_arr[@]} -gt 0 ]] && $kernel_activate_forwarding ; then for _ip in ${forward_mm_server_ip_arr[@]} ; do $ipt -A FORWARD -p udp -d $_ip -m multiport --dports $mm_udp_ports_in -m conntrack --ctstate NEW -j ACCEPT + if [[ -n "$mm_tcp_ports_in" ]] ; then + $ipt -A FORWARD -p tcp -d $_ip -m multiport --dports $mm_tcp_ports_in -m conntrack --ctstate NEW -j ACCEPT + else + $ipt -A FORWARD -p tcp -d $_ip -m multiport --dports $mm_udp_ports_in -m conntrack --ctstate NEW -j ACCEPT + fi $ipt -A FORWARD -p udp -s $_ip -m multiport --dports $mm_udp_ports_out -m conntrack --ctstate NEW -j ACCEPT done fi diff --git a/roles/ipt-server/tasks/main.yml b/roles/ipt-server/tasks/main.yml index db234ff..96e8d9f 100644 --- a/roles/ipt-server/tasks/main.yml +++ b/roles/ipt-server/tasks/main.yml @@ -107,6 +107,8 @@ owner: root group: root mode: "0750" + notify: + - Restart IPv4 Firewall - name: Deploy ip6t-firewall-server copy: @@ -115,6 +117,8 @@ owner: root group: root mode: "0750" + notify: + - Restart IPv6 Firewall # === @@ -134,6 +138,11 @@ - logging_ipv4.conf - logging_ipv6.conf - post_declarations.conf + # Shared files are sourced by both scripts; logging_ipv4/6 only by one, + # but restarting both keeps this simple. + notify: + - Restart IPv4 Firewall + - Restart IPv6 Firewall # === diff --git a/roles/ipt-server/templates/etc/ipt-firewall/main_ipv4.conf.j2 b/roles/ipt-server/templates/etc/ipt-firewall/main_ipv4.conf.j2 index d14be6c..6ab590a 100644 --- a/roles/ipt-server/templates/etc/ipt-firewall/main_ipv4.conf.j2 +++ b/roles/ipt-server/templates/etc/ipt-firewall/main_ipv4.conf.j2 @@ -137,6 +137,16 @@ http_ports="{{ fw_http_ports }}" log_cgi_traffic_out={{ fw_log_cgi_traffic_out | lower }} cgi_script_users="{{ fw_cgi_script_users }}" +# Restrictions on HTTP(S) connections to counteract attacks +set_max_new_http_conn_per_ip_per_second={{ fw_set_max_new_http_conn_per_ip_per_second | lower }} +max_http_new_conn_per_ip_per_second="{{ fw_max_http_new_conn_per_ip_per_second }}" + +set_max_concurrent_http_conn_per_ip={{ fw_set_max_concurrent_http_conn_per_ip | lower }} +max_concurrent_http_conn_per_ip="{{ fw_max_concurrent_http_conn_per_ip }}" + +set_max_new_http_conn_per_second={{ fw_set_max_new_http_conn_per_second | lower }} +max_new_http_conn_per_second="{{ fw_max_new_http_conn_per_second }}" + # ------------- # --- Services: Mattermost @@ -145,6 +155,7 @@ cgi_script_users="{{ fw_cgi_script_users }}" mm_server_ips="{{ fw_mm_server_ips }}" forward_mm_server_ips="{{ fw_forward_mm_server_ips }}" mm_udp_ports_in="{{ fw_mm_udp_ports_in }}" +mm_tcp_ports_in="{{ fw_mm_tcp_ports_in }}" mm_udp_ports_out="{{ fw_mm_udp_ports_out }}" @@ -305,7 +316,7 @@ portforward_udp="{{ fw_portforward_udp_v4 }}" # --- Blocked IPs / Ports # ------------- -blocked_ips="{{ fw_blocked_ips }}" +blocked_ips="{{ fw_blocked_ips_v4 }}" block_tcp_ports="{{ fw_block_tcp_ports }}" block_udp_ports="{{ fw_block_udp_ports }}" diff --git a/roles/ipt-server/templates/etc/ipt-firewall/main_ipv6.conf.j2 b/roles/ipt-server/templates/etc/ipt-firewall/main_ipv6.conf.j2 index 9ad08ba..b885c27 100644 --- a/roles/ipt-server/templates/etc/ipt-firewall/main_ipv6.conf.j2 +++ b/roles/ipt-server/templates/etc/ipt-firewall/main_ipv6.conf.j2 @@ -127,6 +127,20 @@ http_ports="{{ fw_http_ports }}" log_cgi_traffic_out={{ fw_log_cgi_traffic_out | lower }} cgi_script_users="{{ fw_cgi_script_users }}" +# Restrictions on HTTP(S) connections to counteract attacks +set_max_new_http_conn_per_ip_per_second={{ fw_set_max_new_http_conn_per_ip_per_second | lower }} +max_http_new_conn_per_ip_per_second="{{ fw_max_http_new_conn_per_ip_per_second }}" + +set_max_concurrent_http_conn_per_ip={{ fw_set_max_concurrent_http_conn_per_ip | lower }} +max_concurrent_http_conn_per_ip="{{ fw_max_concurrent_http_conn_per_ip }}" + +set_max_new_http_conn_per_second={{ fw_set_max_new_http_conn_per_second | lower }} +max_new_http_conn_per_second="{{ fw_max_new_http_conn_per_second }}" + +# Clamp the advertised MSS of incoming HTTPS SYN packets +throttle_tcp6_mss_for_https={{ fw_throttle_tcp6_mss_for_https | lower }} +tcp6_mss_for_https_val="{{ fw_tcp6_mss_for_https_val }}" + # ------------- # --- Services: Mattermost @@ -135,6 +149,7 @@ cgi_script_users="{{ fw_cgi_script_users }}" mm_server_ips="{{ fw_mm_server_ips }}" forward_mm_server_ips="{{ fw_forward_mm_server_ips }}" mm_udp_ports_in="{{ fw_mm_udp_ports_in }}" +mm_tcp_ports_in="{{ fw_mm_tcp_ports_in }}" mm_udp_ports_out="{{ fw_mm_udp_ports_out }}" @@ -295,7 +310,11 @@ portforward_udp="{{ fw_portforward_udp_v6 }}" # --- Blocked IPs / Ports # ------------- -blocked_ips="{{ fw_blocked_ips }}" +blocked_ips="{{ fw_blocked_ips_v6 }}" + +# Prefix length for entries in ban_ipv6.list given without mask +ban_ipv6_mask="{{ fw_ban_ipv6_mask }}" + block_tcp_ports="{{ fw_block_tcp_ports }}" block_udp_ports="{{ fw_block_udp_ports }}"