Files
CISS.debian.installer/func/cdi_4400_hardening/4420_hardening_fail2ban.sh
Marc S. Weidner 82096f7b7d
All checks were successful
🛡️ Shell Script Linting / 🛡️ Shell Script Linting (push) Successful in 2m13s
V8.00.000.2025.06.17
Signed-off-by: Marc S. Weidner <msw@coresecret.dev>
2025-10-21 07:59:51 +01:00

330 lines
11 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/bash
# SPDX-Version: 3.0
# SPDX-CreationInfo: 2025-06-17; WEIDNER, Marc S.; <msw@coresecret.dev>
# 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.; <msw@coresecret.dev>
# 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]
usedns = yes
# 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)
# fc00::/7 IPv6 ULA (private LAN addresses)
# 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 fc00::/7 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"
maxretry = 3
findtime = 1d
bantime = 1h
bantime.increment = true
bantime.factor = 1
bantime.maxtime = 16d
bantime.overalljails = true
bantime.rndtime = 877s
[recidive]
enabled = true
filter = recidive
logpath = /var/log/fail2ban/fail2ban.log*
banaction = iptables-allports
maxretry = 3
findtime = 16d
bantime = 8d
bantime.increment = true
bantime.factor = 1
bantime.maxtime = 96d
bantime.multipliers = 1, 2, 4, 8
bantime.overalljails = true
bantime.rndtime = 877s
[sshd]
enabled = true
backend = systemd
filter = sshd
mode = normal
port = ${VAR_SSH_PORT}
protocol = tcp
logpath = /var/log/auth.log
maxretry = 4
#
# ufw 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.
#
[ufw]
enabled = true
filter = ciss.ufw
action = iptables-allports
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]
usedns = yes
# 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)
# fc00::/7 IPv6 ULA (private LAN addresses)
# 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 fc00::/7 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"
maxretry = 3
findtime = 1d
bantime = 1h
bantime.increment = true
bantime.factor = 1
bantime.maxtime = 16d
bantime.overalljails = true
bantime.rndtime = 877s
[recidive]
enabled = true
filter = recidive
logpath = /var/log/fail2ban/fail2ban.log*
banaction = iptables-allports
maxretry = 3
findtime = 16d
bantime = 8d
bantime.increment = true
bantime.factor = 1
bantime.maxtime = 96d
bantime.multipliers = 1, 2, 4, 8
bantime.overalljails = true
bantime.rndtime = 877s
[sshd]
enabled = true
backend = systemd
filter = sshd
mode = normal
port = ${VAR_SSH_PORT}
protocol = tcp
logpath = /var/log/auth.log
maxretry = 4
#
# ufw 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 8 attempts.
#
[ufw]
enabled = true
filter = ciss.ufw
action = iptables-allports
logpath = /var/log/ufw.log
maxretry = 4
# 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.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]
failregex = \[UFW BLOCK\].+SRC=<HOST> DST
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
compress
# Do not rotate if empty
notifempty
delaycompress
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