#!/bin/bash # SPDX-Version: 3.0 # SPDX-CreationInfo: 2025-06-17; WEIDNER, Marc S.; # SPDX-ExternalRef: GIT https://git.coresecret.dev/msw/CISS.debian.installer.git # SPDX-FileContributor: WEIDNER, Marc S.; Centurion Intelligence Consulting Agency # SPDX-FileCopyrightText: 2024-2025; WEIDNER, Marc S.; # SPDX-FileType: SOURCE # SPDX-License-Identifier: EUPL-1.2 OR LicenseRef-CCLA-1.0 # SPDX-LicenseComment: This file is part of the CISS.debian.installer.secure framework. # SPDX-PackageName: CISS.debian.installer # SPDX-Security-Contact: security@coresecret.eu guard_sourcing ####################################### # Hardening 'fail2ban'. # Globals: # ARY_ALLOW_IPV4 # ARY_ALLOW_IPV6 # RECOVERY # TARGET # VAR_FINAL_FQDN # VAR_FINAL_IPV4 # VAR_FINAL_IPV6 # VAR_LINK_IPV6 # VAR_PROVIDER # VAR_RUN_RECOVERY # VAR_SSH_PORT # Arguments: # None # Returns: # 0: on success ####################################### hardening_fail2ban() { ### Declare Arrays, HashMaps, and Variables. declare -r var_logfile="/root/.ciss/cdi/log/4420_hardening_fail2ban.log" declare var_target="${TARGET}" ### Check for TARGET / RECOVERY. [[ "${VAR_RUN_RECOVERY}" == "true" ]] && var_target="${RECOVERY}" chroot_logger "${var_target}${var_logfile}" mkdir -p "${var_target}/root/.ciss/cdi/backup/etc/fail2ban/jail.d" cp "${var_target}/etc/fail2ban/fail2ban.conf" "${var_target}/root/.ciss/cdi/backup/etc/fail2ban/fail2ban.conf.bak" mv "${var_target}/etc/fail2ban/jail.d/defaults-debian.conf" "${var_target}/root/.ciss/cdi/backup/etc/fail2ban/jail.d/defaults-debian.conf.bak" # https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1024305 insert_header "${var_target}/etc/fail2ban/fail2ban.local" insert_comments "${var_target}/etc/fail2ban/fail2ban.local" cat << 'EOF' >> "${var_target}/etc/fail2ban/fail2ban.local" [DEFAULT] allowipv6 = auto EOF insert_header "${var_target}/etc/fail2ban/jail.d/ciss-default.conf" insert_comments "${var_target}/etc/fail2ban/jail.d/ciss-default.conf" if [[ "${#ARY_ALLOW_IPV4[@]}" -gt 0 ]]; then ### fail2ban ufw aggressive mode, one attempt for jumphost configuration. cat << EOF >> "${var_target}/etc/fail2ban/jail.d/ciss-default.conf" [DEFAULT] dbpurgeage = 384d # 127.0.0.1/8 - IPv4 loopback range (local host) # ::1/128 - IPv6 loopback # fe80::/10 - IPv6 link-local (on-link only; NDP/RA/DAD) # ff00::/8 - IPv6 multicast (not an unicast host) # ::/128 - IPv6 unspecified (all zeros; never a real peer) ignoreip = 127.0.0.1/8 ::1/128 fe80::/10 ff00::/8 ::/128 # ${VAR_FINAL_FQDN} ${VAR_FINAL_IPV4} EOF if [[ "${VAR_LINK_IPV6}" == "true" ]]; then cat << EOF >> "${var_target}/etc/fail2ban/jail.d/ciss-default.conf" ${VAR_FINAL_IPV6}/64 EOF fi cat << EOF >> "${var_target}/etc/fail2ban/jail.d/ciss-default.conf" # Jumphost ${ARY_ALLOW_IPV4[*]} EOF if [[ "${VAR_LINK_IPV6}" == "true" ]]; then cat << EOF >> "${var_target}/etc/fail2ban/jail.d/ciss-default.conf" ${ARY_ALLOW_IPV6[*]} EOF fi cat << EOF >> "${var_target}/etc/fail2ban/jail.d/ciss-default.conf" usedns = yes [recidive] enabled = true banaction = ufw[blocktype=deny] bantime = 8d bantime.increment = true bantime.factor = 1 bantime.maxtime = 128d bantime.multipliers = 1 2 4 8 16 bantime.overalljails = true bantime.rndtime = 877s filter = recidive findtime = 16d logpath = /var/log/fail2ban/fail2ban.log* maxretry = 3 [sshd] enabled = true backend = systemd bantime = 1h bantime.increment = true bantime.factor = 1 bantime.maxtime = 16d bantime.multipliers = 1 2 4 8 16 32 64 128 256 384 bantime.overalljails = true bantime.rndtime = 877s filter = sshd findtime = 16m mode = aggressive port = ${VAR_SSH_PORT} protocol = tcp maxretry = 4 # # CISS aggressive approach: # Any valid client communicating with our server should be going directly to the service ports opened in ufw (ssh, 80, ...). # Any client touching other ports is treated as malicious and therefore should be blocked access to ALL ports after 1 attempt. # There is no necessity to ping our servers excessively. Any client pinging us more than 1 times will be blocked. # [icmp] enabled = true banaction = ufw[blocktype=deny] bantime = 1h bantime.increment = true bantime.factor = 1 bantime.maxtime = 16d bantime.multipliers = 1 2 4 8 16 32 64 128 256 384 bantime.overalljails = true bantime.rndtime = 877s filter = ciss-icmp findtime = 16m logpath = /var/log/ufw.log maxretry = 1 [ufw] enabled = true banaction = ufw[blocktype=deny] bantime = 1h bantime.increment = true bantime.factor = 1 bantime.maxtime = 16d bantime.multipliers = 1 2 4 8 16 32 64 128 256 384 bantime.overalljails = true bantime.rndtime = 877s filter = ciss-ufw findtime = 16m logpath = /var/log/ufw.log maxretry = 1 # vim: number et ts=2 sw=2 sts=2 ai tw=128 ft=conf EOF else ### fail2ban ufw aggressive mode, 32 attempts for NO jumphost configuration. cat << EOF >> "${var_target}/etc/fail2ban/jail.d/ciss-default.conf" [DEFAULT] dbpurgeage = 384d # 127.0.0.1/8 - IPv4 loopback range (local host) # ::1/128 - IPv6 loopback # fe80::/10 - IPv6 link-local (on-link only; NDP/RA/DAD) # ff00::/8 - IPv6 multicast (not an unicast host) # ::/128 - IPv6 unspecified (all zeros; never a real peer) ignoreip = 127.0.0.1/8 ::1/128 fe80::/10 ff00::/8 ::/128 # ${VAR_FINAL_FQDN} ${VAR_FINAL_IPV4} EOF if [[ "${VAR_LINK_IPV6}" == "true" ]]; then cat << EOF >> "${var_target}/etc/fail2ban/jail.d/ciss-default.conf" ${VAR_FINAL_IPV6}/64 EOF fi cat << EOF >> "${var_target}/etc/fail2ban/jail.d/ciss-default.conf" usedns = yes [recidive] enabled = true banaction = ufw[blocktype=deny] bantime = 8d bantime.increment = true bantime.factor = 1 bantime.maxtime = 128d bantime.multipliers = 1 2 4 8 16 bantime.overalljails = true bantime.rndtime = 877s filter = recidive findtime = 16d logpath = /var/log/fail2ban/fail2ban.log* maxretry = 3 [sshd] enabled = true backend = systemd bantime = 1h bantime.increment = true bantime.factor = 1 bantime.maxtime = 16d bantime.multipliers = 1 2 4 8 16 32 64 128 256 384 bantime.overalljails = true bantime.rndtime = 877s filter = sshd findtime = 16m mode = normal port = ${VAR_SSH_PORT} protocol = tcp maxretry = 4 # # CISS aggressive approach: # Any valid client communicating with our server should be going directly to the service ports opened in ufw (ssh, 80, ...). # Any client touching other ports is treated as malicious and therefore should be blocked access to ALL ports after 3 attempts. # There is no necessity to ping our servers excessively. Any client pinging us more than 3 times will be blocked. # [icmp] enabled = true banaction = ufw[blocktype=deny] bantime = 1h bantime.increment = true bantime.factor = 1 bantime.maxtime = 16d bantime.multipliers = 1 2 4 8 16 32 64 128 256 384 bantime.overalljails = true bantime.rndtime = 877s filter = ciss-icmp findtime = 16m logpath = /var/log/ufw.log maxretry = 3 [ufw] enabled = true banaction = ufw[blocktype=deny] bantime = 1h bantime.increment = true bantime.factor = 1 bantime.maxtime = 16d bantime.multipliers = 1 2 4 8 16 32 64 128 256 384 bantime.overalljails = true bantime.rndtime = 877s filter = ciss-ufw findtime = 16m logpath = /var/log/ufw.log maxretry = 3 # vim: number et ts=2 sw=2 sts=2 ai tw=128 ft=conf EOF fi ### Provider Hetzner needs special ignoreip rules. if [[ "${VAR_PROVIDER}" == "hetzner" ]]; then sed -i '0,/^maxretry/{s/^maxretry/# Hetzner Intern\n 172.31.1.1\/16\n&/}' "${var_target}/etc/fail2ban/jail.d/ciss-default.conf" fi insert_header "${var_target}/etc/fail2ban/filter.d/ciss-icmp.conf" insert_comments "${var_target}/etc/fail2ban/filter.d/ciss-icmp.conf" cat << EOF >> "${var_target}/etc/fail2ban/filter.d/ciss-icmp.conf" [Definition] # Generic ICMP/ICMPv6 blocks failregex = ^.*UFW (?:BLOCK|REJECT).*?\bSRC=\b.*?\bPROTO=ICMP\b.*$ ^.*UFW (?:BLOCK|REJECT).*?\bSRC=\b.*?\bPROTO=ICMPv6\b.*$ # vim: number et ts=2 sw=2 sts=2 ai tw=128 ft=conf EOF insert_header "${var_target}/etc/fail2ban/filter.d/ciss-ufw.conf" insert_comments "${var_target}/etc/fail2ban/filter.d/ciss-ufw.conf" cat << EOF >> "${var_target}/etc/fail2ban/filter.d/ciss-ufw.conf" [Definition] # Match UFW BLOCK/REJECT with a source IP and *any* port field (SPT or DPT), protocol may be missing. failregex = ^.*UFW (?:BLOCK|REJECT).*?\bSRC=\b.*?(?:\bDPT=\d+\b|\bSPT=\d+\b).*$ ignoreregex = # vim: number et ts=2 sw=2 sts=2 ai tw=128 ft=conf EOF # Hardening of fail2ban systemd: https://wiki.archlinux.org/title/fail2ban#Service_hardening # The 'CapabilityBoundingSet' parameters 'CAP_DAC_READ_SEARCH' will allow fail2ban full read access to every directory and # file. "CAP_NET_ADMIN" and "CAP_NET_RAW" allow fail2ban to operate on any firewall that has a command-line shell interface. # By using 'ProtectSystem=strict' the filesystem hierarchy will only be read-only; 'ReadWritePaths' allows Fail2ban to have # write access on required paths. mkdir -p "${var_target}/etc/systemd/system/fail2ban.service.d" mkdir -p "${var_target}/var/log/fail2ban" insert_header "${var_target}/etc/systemd/system/fail2ban.service.d/override.conf" insert_comments "${var_target}/etc/systemd/system/fail2ban.service.d/override.conf" cat << EOF >> "${var_target}/etc/systemd/system/fail2ban.service.d/override.conf" [Service] PrivateDevices=yes PrivateTmp=yes ProtectHome=read-only ProtectSystem=strict ReadWritePaths=-/var/run/fail2ban ReadWritePaths=-/var/lib/fail2ban ReadWritePaths=-/var/log/fail2ban ReadWritePaths=-/var/spool/postfix/maildrop ReadWritePaths=-/run/xtables.lock CapabilityBoundingSet=CAP_AUDIT_READ CAP_DAC_READ_SEARCH CAP_NET_ADMIN CAP_NET_RAW ProtectClock=true ProtectHostname=true EOF cat << 'EOF' >> "${var_target}/etc/fail2ban/fail2ban.local" [Definition] logtarget = /var/log/fail2ban/fail2ban.log [Database] # Keep entries for at least 384 days to cover recidive findtime. dbpurgeage = 384d # vim: number et ts=2 sw=2 sts=2 ai tw=128 ft=conf EOF ### Logrotate must be updated too. mkdir -p "${var_target}/root/.ciss/cdi/backup/etc/logrotate.d" cp "${var_target}/etc/logrotate.d/fail2ban" "${var_target}/root/.ciss/cdi/backup/etc/logrotate.d/fail2ban.bak" cat << EOF >| "${var_target}/etc/logrotate.d/fail2ban" /var/log/fail2ban/fail2ban.log { daily rotate 384 maxage 384 notifempty dateext dateyesterday compress compresscmd /usr/bin/zstd compressext .zst compressoptions -20 uncompresscmd /usr/bin/unzstd delaycompress shred missingok postrotate fail2ban-client flushlogs 1>/dev/null endscript # If fail2ban runs as non-root it still needs to have write access # to logfiles. # create 640 fail2ban adm create 640 root adm } EOF touch "${var_target}/var/log/fail2ban/fail2ban.log" chmod 0640 "${var_target}/var/log/fail2ban/fail2ban.log" if [[ ! -f "${var_target}/var/log/ufw.log" ]]; then install -d -m 0755 "${var_target}/var/log" : >| "${var_target}/var/log/ufw.log" chmod 0640 "${var_target}/var/log/ufw.log" fi ### Merge / Dump-Parse via 'fail2ban-client -d'. All '*.conf', '*.local', and 'jail.*'-files are read, inherited, and merged. ### Syntax, path, and key errors result in a non-zero exit. chroot_script "${var_target}" " fail2ban-client -d >> ${var_logfile} && echo "OK: config parsed" >> ${var_logfile} || echo "ERROR: config invalid" >> ${var_logfile} " guard_dir && return 0 } ### Prevents accidental 'unset -f'. # shellcheck disable=SC2034 readonly -f hardening_fail2ban # vim: number et ts=2 sw=2 sts=2 ai tw=128 ft=sh