Files
wissen/IT Security/CIS Benchmark L1 RHEL8 und RHEL9.md

52 KiB
Raw Blame History

Berechtigung geben

chmod +x cis_level1_hardening.sh

Erst Dry-Run:

sudo ./cis_level1_hardening.sh --dry-run

Dann produktiv:

sudo ./cis_level1_hardening.sh

Quellcode

#!/usr/bin/env bash #===============================================================================

CIS Benchmark Level 1 - Server Hardening Script

Unterstützt: Red Hat Enterprise Linux 8 & 9 (und kompatible Derivate)

Version: 1.0

Modus: Automatisch für unkritische Maßnahmen

Interaktiv (ja/nein) für kritische Maßnahmen

#=============================================================================== set -euo pipefail

── Farben & Formatierung ─────────────────────────────────────────────────────

RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; BLUE='\033[0;34m' CYAN='\033[0;36m'; BOLD='\033[1m'; NC='\033[0m'

── Globale Variablen ─────────────────────────────────────────────────────────

LOGFILE="/var/log/cis_hardening_$(date +%Y%m%d_%H%M%S).log" BACKUP_DIR="/root/cis_backup_$(date +%Y%m%d_%H%M%S)" RHEL_VERSION="" CHANGES_MADE=0 SKIPPED=0 ALREADY_OK=0 ERRORS=0 DRY_RUN=false

── Hilfsfunktionen ──────────────────────────────────────────────────────────

log() { echo -e "$(date '+%Y-%m-%d %H:%M:%S') $" | tee -a "$LOGFILE"; } info() { log "${BLUE}[INFO]${NC} $"; } ok() { log "${GREEN}[OK]${NC} $"; ((ALREADY_OK++)) || true; } fixed() { log "${GREEN}[FIXED]${NC} $"; ((CHANGES_MADE++)) || true; } warn() { log "${YELLOW}[WARN]${NC} $"; } error() { log "${RED}[ERROR]${NC} $"; ((ERRORS++)) || true; } skip() { log "${YELLOW}[SKIP]${NC} $"; ((SKIPPED++)) || true; } section(){ echo "" | tee -a "$LOGFILE"; log "${BOLD}${CYAN}═══ $ ═══${NC}"; }

ask_critical() { local desc="$1" echo -e "${YELLOW}[KRITISCH]${NC} ${BOLD}$desc${NC}" echo -en " Umsetzen? (j/n/info): " while true; do read -r answer case "${answer,,}" in j|ja|y|yes) return 0 ;; n|nein|no) skip "Übersprungen: $desc"; return 1 ;; i|info) echo -e " ${CYAN}$2${NC}"; echo -en " Umsetzen? (j/n): " ;; *) echo -en " Bitte j oder n eingeben: " ;; esac done }

backup_file() { local file="$1" if -f "$file" ; then local target="${BACKUP_DIR}${file}" mkdir -p "$(dirname "$target")" cp -a "$file" "$target" 2>/dev/null || true fi }

sysctl_set() { local param="$1" value="$2" local current current=$(sysctl -n "$param" 2>/dev/null || echo "NICHT_VERFÜGBAR") if ; then ok "$param = $value (bereits gesetzt)" else if ! $DRY_RUN; then sysctl -w "${param}=${value}" >/dev/null 2>&1 || { error "sysctl $param fehlgeschlagen"; return; } grep -qxF "${param} = ${value}" /etc/sysctl.d/99-cis-hardening.conf 2>/dev/null ||
echo "${param} = ${value}" >> /etc/sysctl.d/99-cis-hardening.conf fi fixed "$param = $value" fi }

ensure_config_line() { local file="$1" line="$2" desc="$3" backup_file "$file" if grep -qxF "$line" "$file" 2>/dev/null; then ok "$desc (bereits konfiguriert)" else if ! $DRY_RUN; then echo "$line" >> "$file" fi fixed "$desc" fi }

disable_kernel_module() { local mod="$1" local conf="/etc/modprobe.d/cis-disable-${mod}.conf" if -f "$conf" && grep -q "install ${mod} /bin/false" "$conf" 2>/dev/null; then ok "Kernel-Modul $mod bereits deaktiviert" else if ! $DRY_RUN; then echo "install ${mod} /bin/false" > "$conf" echo "blacklist ${mod}" >> "$conf" modprobe -r "$mod" 2>/dev/null || true rmmod "$mod" 2>/dev/null || true fi fixed "Kernel-Modul $mod deaktiviert" fi }

disable_service() { local svc="$1" desc="$2" if systemctl is-enabled "$svc" 2>/dev/null | grep -q "enabled"; then if ! $DRY_RUN; then systemctl stop "$svc" 2>/dev/null || true systemctl disable "$svc" 2>/dev/null || true fi fixed "$desc: $svc deaktiviert" else ok "$desc: $svc bereits deaktiviert/nicht vorhanden" fi }

mask_service() { local svc="$1" desc="$2" if ! systemctl is-masked "$svc" 2>/dev/null | grep -q "masked"; then if ! $DRY_RUN; then systemctl stop "$svc" 2>/dev/null || true systemctl mask "$svc" 2>/dev/null || true fi fixed "$desc: $svc maskiert" else ok "$desc: $svc bereits maskiert" fi }

remove_package() { local pkg="$1" desc="$2" if rpm -q "$pkg" >/dev/null 2>&1; then if ! $DRY_RUN; then dnf remove -y "$pkg" >/dev/null 2>&1 || { error "Konnte $pkg nicht entfernen"; return; } fi fixed "$desc: $pkg entfernt" else ok "$desc: $pkg nicht installiert" fi }

── Voraussetzungen prüfen ───────────────────────────────────────────────────

preflight() { if $EUID -ne 0 ; then echo -e "${RED}Dieses Script muss als root ausgeführt werden.${NC}" exit 1 fi

if [[ "${1:-}" == "--dry-run" ]]; then
    DRY_RUN=true
    echo -e "${YELLOW}=== DRY-RUN MODUS  es werden keine Änderungen vorgenommen ===${NC}"
fi

if [[ -f /etc/redhat-release ]]; then
    RHEL_VERSION=$(rpm -E %{rhel} 2>/dev/null || grep -oP '(?<=release )\d' /etc/redhat-release)
fi

if [[ "$RHEL_VERSION" != "8" && "$RHEL_VERSION" != "9" ]]; then
    echo -e "${RED}Nicht unterstütztes System. RHEL 8 oder 9 erforderlich.${NC}"
    echo "Erkannt: $(cat /etc/redhat-release 2>/dev/null || echo 'unbekannt')"
    exit 1
fi

mkdir -p "$BACKUP_DIR"
touch "$LOGFILE"

echo -e "${BOLD}${CYAN}"
echo "╔══════════════════════════════════════════════════════════════╗"
echo "║    CIS Level 1 Server Hardening  RHEL $RHEL_VERSION                    ║"
echo "║    $(date '+%Y-%m-%d %H:%M:%S')                                    ║"
echo "║    Backup-Dir: $BACKUP_DIR     ║"
echo "║    Logfile:    $LOGFILE     ║"
echo "╚══════════════════════════════════════════════════════════════╝"
echo -e "${NC}"

info "System: $(cat /etc/redhat-release)"
info "Kernel: $(uname -r)"
info "Backup-Verzeichnis: $BACKUP_DIR"
echo ""
echo -e "${YELLOW}Das Script wird jetzt CIS Level 1 Server Hardening durchführen.${NC}"
echo -e "Unkritische Maßnahmen werden automatisch umgesetzt."
echo -e "Kritische Maßnahmen erfordern Bestätigung."
echo ""
echo -en "Fortfahren? (j/n): "
read -r confirm
if [[ "${confirm,,}" != "j" && "${confirm,,}" != "ja" ]]; then
    echo "Abgebrochen."
    exit 0
fi

}

══════════════════════════════════════════════════════════════════════════════

SEKTION 1: Initial Setup

══════════════════════════════════════════════════════════════════════════════

harden_filesystem_modules() { section "1.1.1 Dateisystem-Kernel-Module deaktivieren"

local modules_safe=(cramfs freevxfs hfs hfsplus jffs2 udf)
for mod in "${modules_safe[@]}"; do
    disable_kernel_module "$mod"
done

# squashfs  kritisch wegen Container
if lsmod | grep -q squashfs || mount | grep -q squashfs; then
    warn "squashfs ist aktiv (Container/Snap). Wird NICHT deaktiviert."
else
    disable_kernel_module "squashfs"
fi

# usb-storage  kritisch
if ask_critical "1.1.1.8/10: USB-Storage Kernel-Modul deaktivieren" \
    "USB-Speichergeräte werden komplett blockiert. Alternative: USBGuard für granulare Kontrolle."; then
    disable_kernel_module "usb-storage"
fi

}

harden_filesystem_partitions() { section "1.1.2 Dateisystem-Partitionen & Mountoptionen"

# /tmp Mountoptionen prüfen/setzen
if findmnt -n /tmp >/dev/null 2>&1; then
    local tmp_opts
    tmp_opts=$(findmnt -n -o OPTIONS /tmp)
    for opt in nosuid nodev noexec; do
        if echo "$tmp_opts" | grep -q "$opt"; then
            ok "/tmp hat $opt"
        else
            warn "/tmp fehlt $opt  manuelle Anpassung in /etc/fstab empfohlen"
        fi
    done
else
    warn "/tmp ist keine separate Partition  empfohlen: tmpfs-Eintrag in /etc/fstab"
fi

# /dev/shm prüfen
if findmnt -n /dev/shm >/dev/null 2>&1; then
    local shm_opts
    shm_opts=$(findmnt -n -o OPTIONS /dev/shm)
    for opt in nosuid nodev noexec; do
        if echo "$shm_opts" | grep -q "$opt"; then
            ok "/dev/shm hat $opt"
        else
            if ! $DRY_RUN; then
                mount -o remount,"$opt" /dev/shm 2>/dev/null || true
                # Persistenz in fstab
                if ! grep -q "/dev/shm" /etc/fstab; then
                    echo "tmpfs /dev/shm tmpfs defaults,nosuid,nodev,noexec 0 0" >> /etc/fstab
                fi
            fi
            fixed "/dev/shm: $opt gesetzt"
        fi
    done
fi

# /home, /var, /var/tmp, /var/log, /var/log/audit  nur Status prüfen
for mp in /home /var /var/tmp /var/log /var/log/audit; do
    if findmnt -n "$mp" >/dev/null 2>&1; then
        ok "$mp ist separate Partition"
    else
        warn "$mp ist KEINE separate Partition (empfohlen für CIS Compliance)"
    fi
done

}

harden_package_management() { section "1.2 Paketverwaltung"

# gpgcheck global
if grep -q "^gpgcheck=1" /etc/dnf/dnf.conf 2>/dev/null; then
    ok "gpgcheck ist global aktiviert"
else
    backup_file /etc/dnf/dnf.conf
    if ! $DRY_RUN; then
        sed -i 's/^gpgcheck=.*/gpgcheck=1/' /etc/dnf/dnf.conf 2>/dev/null || \
            echo "gpgcheck=1" >> /etc/dnf/dnf.conf
    fi
    fixed "gpgcheck global aktiviert"
fi

# Repo-gpgcheck
local bad_repos
bad_repos=$(grep -rl "^gpgcheck=0" /etc/yum.repos.d/ 2>/dev/null || true)
if [[ -n "$bad_repos" ]]; then
    warn "Repos ohne gpgcheck gefunden: $bad_repos"
else
    ok "Alle Repos haben gpgcheck aktiviert"
fi

}

harden_selinux() { section "1.3 SELinux / Mandatory Access Control"

# SELinux Bootloader-Parameter
local grub_selinux
grub_selinux=$(grubby --info=ALL 2>/dev/null | grep -E "selinux=0|enforcing=0" || true)
if [[ -n "$grub_selinux" ]]; then
    if ! $DRY_RUN; then
        grubby --update-kernel ALL --remove-args "selinux=0 enforcing=0"
    fi
    fixed "SELinux-Deaktivierung aus Bootloader entfernt"
else
    ok "SELinux ist nicht im Bootloader deaktiviert"
fi

# SELinux Policy
local current_policy
current_policy=$(grep -E "^SELINUXTYPE=" /etc/selinux/config 2>/dev/null | cut -d= -f2 || echo "unknown")
if [[ "$current_policy" == "targeted" ]]; then
    ok "SELinux Policy: targeted"
else
    warn "SELinux Policy ist '$current_policy'  sollte 'targeted' sein"
fi

# SELinux Enforcing
local current_mode
current_mode=$(getenforce 2>/dev/null || echo "Disabled")
if [[ "$current_mode" == "Enforcing" ]]; then
    ok "SELinux ist Enforcing"
else
    if ask_critical "SELinux auf Enforcing setzen (aktuell: $current_mode)" \
        "Relabeling dauert 30-60 Min beim nächsten Boot. Erst Permissive testen empfohlen. audit2why hilft bei Policy-Fehlern."; then
        backup_file /etc/selinux/config
        if ! $DRY_RUN; then
            sed -i 's/^SELINUX=.*/SELINUX=enforcing/' /etc/selinux/config
            if [[ "$current_mode" == "Permissive" ]]; then
                setenforce 1 2>/dev/null || warn "setenforce 1 fehlgeschlagen  wird nach Reboot aktiv"
            else
                warn "SELinux war Disabled  Reboot mit fixfiles -F onboot erforderlich"
                fixfiles -F onboot 2>/dev/null || true
            fi
        fi
        fixed "SELinux auf Enforcing konfiguriert"
    fi
fi

# setroubleshoot und mcstrans entfernen
remove_package "setroubleshoot" "1.3.1.6: setroubleshoot"
remove_package "mcstrans" "1.3.1.7: mcstrans"

}

harden_bootloader() { section "1.4 Bootloader absichern"

# Bootloader-Passwort
if grep -q "^GRUB2_PASSWORD=" /boot/grub2/user.cfg 2>/dev/null; then
    ok "Bootloader-Passwort ist gesetzt"
else
    warn "Bootloader-Passwort ist NICHT gesetzt  manuell setzen: grub2-setpassword"
fi

# Bootloader-Config Permissions
local grub_cfg="/boot/grub2/grub.cfg"
if [[ -f "$grub_cfg" ]]; then
    local perms
    perms=$(stat -c "%a" "$grub_cfg")
    if [[ "$perms" == "600" ]] || [[ "$perms" == "700" ]]; then
        ok "grub.cfg Berechtigungen: $perms"
    else
        if ! $DRY_RUN; then
            chmod 600 "$grub_cfg"
            chown root:root "$grub_cfg"
        fi
        fixed "grub.cfg Berechtigungen auf 600 gesetzt"
    fi
fi

}

harden_process_hardening() { section "1.5 Prozess-Härtung"

# core dumps
ensure_config_line "/etc/security/limits.d/cis-hardening.conf" "* hard core 0" "1.5.1: Core Dumps limitiert"
sysctl_set "fs.suid_dumpable" "0"

# ASLR
sysctl_set "kernel.randomize_va_space" "2"

# ptrace
sysctl_set "kernel.yama.ptrace_scope" "1"

}

harden_crypto_policy() { section "1.6 System-weite Crypto-Policy"

local current_policy
current_policy=$(update-crypto-policies --show 2>/dev/null || echo "UNKNOWN")

if [[ "$current_policy" == "LEGACY" ]]; then
    if ask_critical "Crypto-Policy von LEGACY auf DEFAULT ändern" \
        "Verbindungen zu Systemen mit TLS 1.0/1.1 oder schwachen Ciphern schlagen danach fehl."; then
        if ! $DRY_RUN; then
            update-crypto-policies --set DEFAULT >/dev/null 2>&1
        fi
        fixed "Crypto-Policy auf DEFAULT gesetzt"
    fi
elif [[ "$current_policy" == "DEFAULT" || "$current_policy" == "FUTURE" || "$current_policy" == "FIPS" ]]; then
    ok "Crypto-Policy: $current_policy"
else
    warn "Crypto-Policy: $current_policy  prüfen ob angemessen"
fi

# CBC für SSH deaktivieren
local cbc_rec="1.6.4"
[[ "$RHEL_VERSION" == "9" ]] && cbc_rec="1.6.5"
if sshd -T 2>/dev/null | grep -qi "aes.*cbc"; then
    if ask_critical "$cbc_rec: CBC-Cipher für SSH deaktivieren" \
        "Ältere SSH-Clients unterstützen evtl. nur CBC. Prüfe vorher Verbindungen zu Legacy-Systemen."; then
        if ! $DRY_RUN; then
            update-crypto-policies --set "${current_policy}:NO-CBC" >/dev/null 2>&1 || \
                warn "Subpolicy NO-CBC konnte nicht gesetzt werden"
        fi
        fixed "CBC-Cipher für SSH deaktiviert"
    fi
else
    ok "$cbc_rec: Keine CBC-Cipher in SSH aktiv"
fi

}

harden_banners() { section "1.7 Login-Banner konfigurieren"

local banner_text="Autorisierter Zugriff erforderlich. Alle Aktivitaeten werden protokolliert. Unbefugter Zugriff wird strafrechtlich verfolgt."

for f in /etc/issue /etc/issue.net; do
    backup_file "$f"
    # Prüfe ob OS-Infos im Banner stehen
    if grep -qEi '\\r|\\s|\\m|\\v|kernel' "$f" 2>/dev/null; then
        if ! $DRY_RUN; then
            echo "$banner_text" > "$f"
            chmod 644 "$f"
            chown root:root "$f"
        fi
        fixed "$(basename $f): OS-Informationen entfernt, Banner gesetzt"
    elif [[ ! -s "$f" ]]; then
        if ! $DRY_RUN; then
            echo "$banner_text" > "$f"
            chmod 644 "$f"
            chown root:root "$f"
        fi
        fixed "$(basename $f): Banner gesetzt"
    else
        ok "$(basename $f): Banner vorhanden ohne OS-Infos"
    fi
done

# /etc/motd  keine OS-Infos
if [[ -s /etc/motd ]] && grep -qEi '\\r|\\s|\\m|\\v|kernel' /etc/motd 2>/dev/null; then
    backup_file /etc/motd
    if ! $DRY_RUN; then
        > /etc/motd
    fi
    fixed "/etc/motd: OS-Informationen entfernt"
else
    ok "/etc/motd: OK"
fi

}

══════════════════════════════════════════════════════════════════════════════

SEKTION 2: Dienste

══════════════════════════════════════════════════════════════════════════════

harden_services() { section "2.1 Server-Dienste deaktivieren"

# Unkritische Dienste  immer deaktivieren/entfernen
local safe_services=(
    "telnet-server:Telnet-Server"
    "ypserv:NIS-Server"
    "xinetd:xinetd"
)
for entry in "${safe_services[@]}"; do
    local pkg="${entry%%:*}" desc="${entry##*:}"
    remove_package "$pkg" "2.1: $desc"
done

# Dienste die meist nicht benötigt werden aber interaktiv abfragen
local critical_services=(
    "autofs:autofs:Automatisches Mounten von Wechselmedien und NFS-Shares wird deaktiviert."
    "avahi-daemon:avahi:mDNS/Bonjour Discovery wird deaktiviert. Kann Service-Discovery beeinträchtigen."
    "cups:cups:Drucken wird deaktiviert. Auf Servern meist unproblematisch."
    "vsftpd:vsftpd:FTP-Server wird entfernt. Alternative: SFTP über SSH."
    "samba:samba:Windows-Filesharing wird deaktiviert."
    "snmpd:net-snmp:SNMP-Monitoring wird deaktiviert. Alternative: Prometheus/node_exporter."
    "squid:squid:Web-Proxy wird deaktiviert."
    "rsync:rsync-daemon:rsync-Daemon wird deaktiviert. rsync als Client bleibt nutzbar."
)

for entry in "${critical_services[@]}"; do
    IFS=':' read -r svc pkg impact <<< "$entry"
    if systemctl is-enabled "$svc" 2>/dev/null | grep -q "enabled" || rpm -q "$pkg" >/dev/null 2>&1; then
        if ask_critical "2.1: $svc deaktivieren/entfernen" "$impact"; then
            disable_service "$svc" "2.1"
        fi
    else
        ok "2.1: $svc nicht aktiv/installiert"
    fi
done

# NFS/rpcbind  besonders kritisch wegen libvirt
section "2.1  NFS/rpcbind (libvirt-Abhängigkeit)"

if rpm -q nfs-utils >/dev/null 2>&1 || rpm -q rpcbind >/dev/null 2>&1; then
    local libvirt_installed=false
    rpm -q libvirt >/dev/null 2>&1 && libvirt_installed=true

    if $libvirt_installed; then
        warn "libvirt ist installiert  NFS/rpcbind werden als Abhängigkeit benötigt"
        warn "Empfehlung: nfs-server deaktivieren, Pakete NICHT entfernen"
        if ask_critical "NFS-Server-Dienst deaktivieren (Pakete bleiben für libvirt)" \
            "nfs-server wird gestoppt, nfs-utils/rpcbind bleiben installiert für KVM/QEMU."; then
            disable_service "nfs-server" "2.1.9"
        fi
    else
        if ask_critical "NFS und rpcbind komplett entfernen" \
            "Kein libvirt installiert. NFS-Mounts, NFS-Shares und RPC-basierte Dienste werden nicht mehr funktionieren."; then
            disable_service "nfs-server" "2.1.9"
            disable_service "rpcbind.socket" "2.1.12"
            disable_service "rpcbind" "2.1.12"
        fi
    fi
else
    ok "2.1: NFS/rpcbind nicht installiert"
fi

# TFTP  kritisch wegen PXE
if rpm -q tftp-server >/dev/null 2>&1; then
    if ask_critical "TFTP-Server entfernen" \
        "TFTP wird für PXE/Netzwerk-Boot verwendet. Wenn PXE-Deployment genutzt wird: NICHT entfernen."; then
        remove_package "tftp-server" "2.1.16/17"
    fi
else
    ok "2.1: TFTP-Server nicht installiert"
fi

# Web-Server prüfen
for websrv in httpd nginx; do
    if rpm -q "$websrv" >/dev/null 2>&1; then
        if ask_critical "Web-Server $websrv deaktivieren" \
            "Web-Services werden gestoppt. Wenn dieser Server Web-Dienste hostet: NICHT deaktivieren."; then
            disable_service "$websrv" "2.1.18/19"
        fi
    fi
done

}

harden_client_services() { section "2.2 Client-Dienste"

remove_package "ypbind" "2.2.1: NIS-Client"
remove_package "telnet" "2.2.4: Telnet-Client"

# LDAP-Client
if rpm -q openldap-clients >/dev/null 2>&1; then
    warn "2.2.2: openldap-clients installiert  prüfen ob benötigt"
fi

}

harden_time_sync() { section "2.3 Zeitsynchronisation"

if systemctl is-enabled chronyd 2>/dev/null | grep -q "enabled"; then
    ok "chronyd ist aktiviert"
elif systemctl is-enabled ntpd 2>/dev/null | grep -q "enabled"; then
    ok "ntpd ist aktiviert (chronyd bevorzugt)"
else
    if ! $DRY_RUN; then
        dnf install -y chrony >/dev/null 2>&1 || true
        systemctl enable --now chronyd 2>/dev/null || true
    fi
    fixed "chronyd installiert und aktiviert"
fi

}

harden_cron() { section "2.4 Cron & at absichern"

# crontab Berechtigungen
for f in /etc/crontab; do
    if [[ -f "$f" ]]; then
        local perms owner
        perms=$(stat -c "%a" "$f")
        owner=$(stat -c "%U:%G" "$f")
        if [[ "$perms" != "600" || "$owner" != "root:root" ]]; then
            if ! $DRY_RUN; then
                chmod 600 "$f"
                chown root:root "$f"
            fi
            fixed "$(basename $f): Berechtigungen auf 600 root:root"
        else
            ok "$(basename $f): Berechtigungen OK"
        fi
    fi
done

for d in /etc/cron.d /etc/cron.daily /etc/cron.hourly /etc/cron.weekly /etc/cron.monthly; do
    if [[ -d "$d" ]]; then
        local perms
        perms=$(stat -c "%a" "$d")
        if [[ "$perms" != "700" ]]; then
            if ! $DRY_RUN; then
                chmod 700 "$d"
                chown root:root "$d"
            fi
            fixed "$d: Berechtigungen auf 700"
        else
            ok "$d: Berechtigungen OK"
        fi
    fi
done

# cron.allow / at.allow
for f in /etc/cron.allow /etc/at.allow; do
    if [[ ! -f "$f" ]]; then
        if ! $DRY_RUN; then
            touch "$f"
            chmod 600 "$f"
            chown root:root "$f"
            echo "root" > "$f"
        fi
        fixed "$(basename $f) erstellt (nur root)"
    else
        ok "$(basename $f) existiert"
    fi
done

# .deny-Dateien entfernen
for f in /etc/cron.deny /etc/at.deny; do
    if [[ -f "$f" ]]; then
        if ! $DRY_RUN; then
            backup_file "$f"
            rm -f "$f"
        fi
        fixed "$(basename $f) entfernt"
    fi
done

}

══════════════════════════════════════════════════════════════════════════════

SEKTION 3: Netzwerk

══════════════════════════════════════════════════════════════════════════════

harden_network_devices() { section "3.1 Netzwerk-Geräte"

# WLAN  kritisch
if nmcli radio wifi 2>/dev/null | grep -qi "enabled"; then
    if ask_critical "WLAN deaktivieren" \
        "Wireless-Interfaces werden komplett deaktiviert. Nur auf Servern empfohlen, NICHT auf Laptops."; then
        if ! $DRY_RUN; then
            nmcli radio wifi off 2>/dev/null || true
            disable_kernel_module "cfg80211"
        fi
        fixed "WLAN deaktiviert"
    fi
else
    ok "WLAN bereits deaktiviert oder nicht vorhanden"
fi

# Bluetooth
if systemctl is-enabled bluetooth 2>/dev/null | grep -q "enabled"; then
    if ask_critical "Bluetooth deaktivieren" \
        "Kabellose Peripherie funktioniert nicht mehr. Auf Servern empfohlen."; then
        disable_service "bluetooth" "3.1.3"
        disable_kernel_module "bluetooth"
    fi
else
    ok "Bluetooth bereits deaktiviert oder nicht vorhanden"
fi

}

harden_network_modules() { section "3.2 Netzwerk-Kernel-Module"

local net_modules=(dccp sctp tipc)
for mod in "${net_modules[@]}"; do
    disable_kernel_module "$mod"
done

}

harden_network_params() { section "3.3 Netzwerk-Kernel-Parameter"

# IP-Forwarding  kritisch
local ip_fwd
ip_fwd=$(sysctl -n net.ipv4.ip_forward 2>/dev/null)
if [[ "$ip_fwd" == "1" ]]; then
    if ask_critical "IP-Forwarding deaktivieren (aktuell: aktiv)" \
        "Router/NAT/VPN/Container-Hosts benötigen Forwarding. Cloud-VMs oft ebenfalls. NICHT deaktivieren wenn System routet."; then
        sysctl_set "net.ipv4.ip_forward" "0"
        sysctl_set "net.ipv6.conf.all.forwarding" "0"
    fi
else
    sysctl_set "net.ipv4.ip_forward" "0"
fi

# Unkritische Netzwerk-Parameter  automatisch setzen
info "Setze sichere Netzwerk-Parameter..."

# ICMP Redirects
sysctl_set "net.ipv4.conf.all.send_redirects" "0"
sysctl_set "net.ipv4.conf.default.send_redirects" "0"
sysctl_set "net.ipv4.conf.all.accept_redirects" "0"
sysctl_set "net.ipv4.conf.default.accept_redirects" "0"
sysctl_set "net.ipv6.conf.all.accept_redirects" "0"
sysctl_set "net.ipv6.conf.default.accept_redirects" "0"

# Source Routing
sysctl_set "net.ipv4.conf.all.accept_source_route" "0"
sysctl_set "net.ipv4.conf.default.accept_source_route" "0"
sysctl_set "net.ipv6.conf.all.accept_source_route" "0"
sysctl_set "net.ipv6.conf.default.accept_source_route" "0"

# Logging Martians
sysctl_set "net.ipv4.conf.all.log_martians" "1"
sysctl_set "net.ipv4.conf.default.log_martians" "1"

# ICMP Protection
sysctl_set "net.ipv4.icmp_echo_ignore_broadcasts" "1"
sysctl_set "net.ipv4.icmp_ignore_bogus_error_responses" "1"

# SYN Cookies
sysctl_set "net.ipv4.tcp_syncookies" "1"

# Reverse Path Filtering
local rp_current
rp_current=$(sysctl -n net.ipv4.conf.all.rp_filter 2>/dev/null)
if [[ "$rp_current" == "0" ]]; then
    if ask_critical "Reverse Path Filtering aktivieren" \
        "Bricht asymmetrisches Routing. Wenn asymmetrisches Routing genutzt wird: rp_filter=2 (loose) statt 1 (strict)."; then
        sysctl_set "net.ipv4.conf.all.rp_filter" "1"
        sysctl_set "net.ipv4.conf.default.rp_filter" "1"
    fi
else
    sysctl_set "net.ipv4.conf.all.rp_filter" "1"
    sysctl_set "net.ipv4.conf.default.rp_filter" "1"
fi

# IPv6 Router Advertisements
sysctl_set "net.ipv6.conf.all.accept_ra" "0"
sysctl_set "net.ipv6.conf.default.accept_ra" "0"

}

══════════════════════════════════════════════════════════════════════════════

SEKTION 4: Firewall

══════════════════════════════════════════════════════════════════════════════

harden_firewall() { section "4 Host-basierte Firewall"

if ask_critical "Firewall konfigurieren" \
    "Drop-Policy ohne SSH-Regel sperrt Remote-Zugang! Script fügt SSH-Regel VOR Drop-Policy hinzu. Trotzdem: IPMI/iDRAC-Zugang als Fallback empfohlen."; then

    if [[ "$RHEL_VERSION" == "8" ]]; then
        # RHEL 8: firewalld
        if ! rpm -q firewalld >/dev/null 2>&1; then
            if ! $DRY_RUN; then
                dnf install -y firewalld >/dev/null 2>&1
            fi
            fixed "firewalld installiert"
        fi

        if ! $DRY_RUN; then
            systemctl enable --now firewalld 2>/dev/null || true
            firewall-cmd --permanent --add-service=ssh 2>/dev/null || true
            firewall-cmd --reload 2>/dev/null || true
        fi
        fixed "firewalld aktiviert, SSH erlaubt"

        # Default Zone prüfen
        local zone
        zone=$(firewall-cmd --get-default-zone 2>/dev/null || echo "unknown")
        info "Default Firewall-Zone: $zone"
        if [[ "$zone" == "public" ]]; then
            ok "Default Zone ist 'public' (sinnvoller Default)"
        fi

    elif [[ "$RHEL_VERSION" == "9" ]]; then
        # RHEL 9: nftables (oder firewalld)
        if systemctl is-enabled firewalld 2>/dev/null | grep -q "enabled"; then
            info "firewalld ist aktiv (nutzt nftables als Backend)"
            if ! $DRY_RUN; then
                firewall-cmd --permanent --add-service=ssh 2>/dev/null || true
                firewall-cmd --reload 2>/dev/null || true
            fi
            fixed "SSH in firewalld erlaubt"
        else
            if ! rpm -q nftables >/dev/null 2>&1; then
                if ! $DRY_RUN; then
                    dnf install -y nftables >/dev/null 2>&1
                fi
            fi

            if ! $DRY_RUN; then
                # SSH-Regel ZUERST, dann Drop-Policy
                cat > /etc/nftables/cis-base.nft << 'NFTEOF'

table inet filter { chain input { type filter hook input priority 0; policy drop; iif lo accept ct state established,related accept ct state invalid drop tcp dport 22 accept icmp type echo-request accept } chain forward { type filter hook forward priority 0; policy drop; } chain output { type filter hook output priority 0; policy accept; } } NFTEOF # In nftables.conf einbinden if ! grep -q "cis-base.nft" /etc/sysconfig/nftables.conf 2>/dev/null; then echo 'include "/etc/nftables/cis-base.nft"' >> /etc/sysconfig/nftables.conf fi systemctl enable --now nftables 2>/dev/null || true fi fixed "nftables mit Default Deny und SSH-Allow konfiguriert" fi fi fi }

══════════════════════════════════════════════════════════════════════════════

SEKTION 5: Zugriffskontrolle

══════════════════════════════════════════════════════════════════════════════

harden_ssh() { section "5.1 SSH-Server absichern"

local ssh_conf="/etc/ssh/sshd_config.d/99-cis-hardening.conf"
backup_file "$ssh_conf"

if ! $DRY_RUN; then
    mkdir -p /etc/ssh/sshd_config.d
    cat > "$ssh_conf" << 'SSHEOF'

CIS Level 1 SSH Hardening

PermitRootLogin no MaxAuthTries 4 MaxSessions 10 PermitEmptyPasswords no HostbasedAuthentication no IgnoreRhosts yes X11Forwarding no AllowTcpForwarding no ClientAliveInterval 300 ClientAliveCountMax 3 LoginGraceTime 60 Banner /etc/issue.net MaxStartups 10:30:60 PermitUserEnvironment no SSHEOF fi fixed "SSH-Härtung in $ssh_conf geschrieben"

# SSH-Daemon neu laden
if ! $DRY_RUN; then
    # Syntax-Check
    if sshd -t 2>/dev/null; then
        systemctl reload sshd 2>/dev/null || systemctl reload ssh 2>/dev/null || true
        ok "sshd Konfiguration validiert und neu geladen"
    else
        error "sshd Konfiguration fehlerhaft! Bitte manuell prüfen: sshd -t"
        # Rollback
        rm -f "$ssh_conf"
        warn "SSH-Konfiguration zurückgerollt"
    fi
fi

# SSH Private Key Berechtigungen
find /etc/ssh -name 'ssh_host_*_key' -exec chmod 600 {} \; 2>/dev/null || true
find /etc/ssh -name 'ssh_host_*_key.pub' -exec chmod 644 {} \; 2>/dev/null || true
ok "SSH Host-Key Berechtigungen geprüft"

}

harden_sudo() { section "5.2 sudo absichern"

# use_pty
if grep -rq "^Defaults.*use_pty" /etc/sudoers /etc/sudoers.d/ 2>/dev/null; then
    ok "sudo use_pty bereits konfiguriert"
else
    if ! $DRY_RUN; then
        echo "Defaults use_pty" > /etc/sudoers.d/cis-use-pty
        chmod 440 /etc/sudoers.d/cis-use-pty
        # Syntax-Check
        if ! visudo -cf /etc/sudoers.d/cis-use-pty >/dev/null 2>&1; then
            rm -f /etc/sudoers.d/cis-use-pty
            error "sudo use_pty Syntax-Fehler  zurückgerollt"
        fi
    fi
    fixed "sudo use_pty konfiguriert"
fi

# sudo Logfile
if grep -rq "^Defaults.*logfile=" /etc/sudoers /etc/sudoers.d/ 2>/dev/null; then
    ok "sudo Logfile bereits konfiguriert"
else
    if ! $DRY_RUN; then
        echo 'Defaults logfile="/var/log/sudo.log"' > /etc/sudoers.d/cis-logfile
        chmod 440 /etc/sudoers.d/cis-logfile
        if ! visudo -cf /etc/sudoers.d/cis-logfile >/dev/null 2>&1; then
            rm -f /etc/sudoers.d/cis-logfile
            error "sudo logfile Syntax-Fehler  zurückgerollt"
        fi
    fi
    fixed "sudo Logfile konfiguriert"
fi

}

harden_pam() { section "5.3 PAM / Authentifizierung"

# Passwort-Qualität
local pwquality="/etc/security/pwquality.conf"
if [[ -f "$pwquality" ]]; then
    backup_file "$pwquality"
    local settings=(
        "minlen = 14"
        "minclass = 4"
        "maxrepeat = 3"
        "maxclassrepeat = 3"
        "dcredit = -1"
        "ucredit = -1"
        "lcredit = -1"
        "ocredit = -1"
    )
    for setting in "${settings[@]}"; do
        local key="${setting%% =*}"
        if grep -q "^${key}" "$pwquality" 2>/dev/null; then
            if ! $DRY_RUN; then
                sed -i "s/^${key}.*/${setting}/" "$pwquality"
            fi
        else
            if ! $DRY_RUN; then
                echo "$setting" >> "$pwquality"
            fi
        fi
    done
    fixed "Passwort-Qualitätsrichtlinien gesetzt (minlen=14, minclass=4)"
fi

# Faillock
local faillock="/etc/security/faillock.conf"
if [[ -f "$faillock" ]]; then
    backup_file "$faillock"
    local fl_settings=(
        "deny = 5"
        "unlock_time = 900"
        "fail_interval = 900"
    )
    for setting in "${fl_settings[@]}"; do
        local key="${setting%% =*}"
        if grep -q "^${key}" "$faillock" 2>/dev/null; then
            if ! $DRY_RUN; then
                sed -i "s/^${key}.*/${setting}/" "$faillock"
            fi
        else
            if ! $DRY_RUN; then
                echo "$setting" >> "$faillock"
            fi
        fi
    done
    fixed "Account-Lockout konfiguriert (deny=5, unlock=900s)"
fi

}

harden_password_policy() { section "5.4 Passwort-Policy & User-Accounts"

backup_file /etc/login.defs

# PASS_MAX_DAYS
local current_max
current_max=$(grep "^PASS_MAX_DAYS" /etc/login.defs 2>/dev/null | awk '{print $2}')
if [[ "${current_max:-99999}" -gt 365 ]]; then
    if ! $DRY_RUN; then
        sed -i 's/^PASS_MAX_DAYS.*/PASS_MAX_DAYS\t365/' /etc/login.defs
    fi
    fixed "PASS_MAX_DAYS auf 365 gesetzt"
else
    ok "PASS_MAX_DAYS: ${current_max}"
fi

# PASS_MIN_DAYS
local current_min
current_min=$(grep "^PASS_MIN_DAYS" /etc/login.defs 2>/dev/null | awk '{print $2}')
if [[ "${current_min:-0}" -lt 1 ]]; then
    if ! $DRY_RUN; then
        sed -i 's/^PASS_MIN_DAYS.*/PASS_MIN_DAYS\t1/' /etc/login.defs
    fi
    fixed "PASS_MIN_DAYS auf 1 gesetzt"
else
    ok "PASS_MIN_DAYS: ${current_min}"
fi

# PASS_WARN_AGE
local current_warn
current_warn=$(grep "^PASS_WARN_AGE" /etc/login.defs 2>/dev/null | awk '{print $2}')
if [[ "${current_warn:-0}" -lt 7 ]]; then
    if ! $DRY_RUN; then
        sed -i 's/^PASS_WARN_AGE.*/PASS_WARN_AGE\t7/' /etc/login.defs
    fi
    fixed "PASS_WARN_AGE auf 7 gesetzt"
else
    ok "PASS_WARN_AGE: ${current_warn}"
fi

# UMASK
local current_umask
current_umask=$(grep "^UMASK" /etc/login.defs 2>/dev/null | awk '{print $2}')
if [[ "$current_umask" != "027" ]]; then
    if ! $DRY_RUN; then
        sed -i 's/^UMASK.*/UMASK\t\t027/' /etc/login.defs
    fi
    fixed "UMASK auf 027 gesetzt"
else
    ok "UMASK: $current_umask"
fi

# Inaktive Accounts sperren
local inactive
inactive=$(useradd -D 2>/dev/null | grep INACTIVE | cut -d= -f2)
if [[ "${inactive}" == "-1" || -z "${inactive}" ]]; then
    if ! $DRY_RUN; then
        useradd -D -f 30 2>/dev/null || true
    fi
    fixed "Inaktive Accounts werden nach 30 Tagen gesperrt"
else
    ok "INACTIVE: ${inactive} Tage"
fi

# Root-Zugang kontrollieren
if ask_critical "Root-Account absichern (SSH-Root-Login ist bereits per SSH-Config deaktiviert)" \
    "Prüft ob automatisierte Prozesse root ohne Auth nutzen. Sperrt ggf. direkten Root-Login."; then
    # Shell für root prüfen
    local root_shell
    root_shell=$(grep "^root:" /etc/passwd | cut -d: -f7)
    info "Root-Shell: $root_shell"
    # Wir sperren root nicht komplett  nur Info
    ok "Root-SSH ist über sshd_config deaktiviert"
fi

# System-Accounts prüfen
info "Prüfe System-Accounts..."
local bad_shells=0
while IFS=: read -r user _ uid _ _ _ shell; do
    if [[ "$uid" -lt 1000 && "$user" != "root" && "$shell" != "/sbin/nologin" && "$shell" != "/usr/sbin/nologin" && "$shell" != "/bin/false" ]]; then
        warn "System-Account $user (UID $uid) hat Login-Shell: $shell"
        ((bad_shells++)) || true
    fi
done < /etc/passwd
if [[ "$bad_shells" -eq 0 ]]; then
    ok "Alle System-Accounts haben nologin-Shell"
fi

# Default User Shell Timeout
local timeout_conf="/etc/profile.d/cis-timeout.sh"
if [[ ! -f "$timeout_conf" ]]; then
    if ! $DRY_RUN; then
        cat > "$timeout_conf" << 'TMEOF'

CIS: Shell Timeout nach 900 Sekunden Inaktivität

readonly TMOUT=900 export TMOUT TMEOF chmod 644 "$timeout_conf" fi fixed "Shell-Timeout auf 900s gesetzt" else ok "Shell-Timeout bereits konfiguriert" fi }

══════════════════════════════════════════════════════════════════════════════

SEKTION 6: Logging & Auditing

══════════════════════════════════════════════════════════════════════════════

harden_integrity() { section "6.1 Integritätsprüfung (AIDE)"

if rpm -q aide >/dev/null 2>&1; then
    ok "AIDE ist installiert"
else
    if ! $DRY_RUN; then
        dnf install -y aide >/dev/null 2>&1 || { error "AIDE konnte nicht installiert werden"; return; }
    fi
    fixed "AIDE installiert"
fi

if [[ ! -f /var/lib/aide/aide.db.gz ]]; then
    info "AIDE-Datenbank wird initialisiert (kann einige Minuten dauern)..."
    if ! $DRY_RUN; then
        aide --init >/dev/null 2>&1 || { warn "AIDE init fehlgeschlagen"; return; }
        mv /var/lib/aide/aide.db.new.gz /var/lib/aide/aide.db.gz 2>/dev/null || true
    fi
    fixed "AIDE-Datenbank initialisiert"
else
    ok "AIDE-Datenbank existiert"
fi

# AIDE Cron
if ! crontab -l 2>/dev/null | grep -q "aide.*--check"; then
    if ! $DRY_RUN; then
        echo "0 5 * * * /usr/sbin/aide --check --config /etc/aide.conf" | \
            crontab -l 2>/dev/null | cat - | crontab - 2>/dev/null || \
            echo "0 5 * * * root /usr/sbin/aide --check" >> /etc/crontab
    fi
    fixed "AIDE täglicher Check als Cronjob konfiguriert"
else
    ok "AIDE Cronjob existiert"
fi

}

harden_journald() { section "6.2 journald konfigurieren"

backup_file /etc/systemd/journald.conf

# Persistente Logs
if [[ ! -d /var/log/journal ]]; then
    if ! $DRY_RUN; then
        mkdir -p /var/log/journal
        systemd-tmpfiles --create --prefix /var/log/journal 2>/dev/null || true
    fi
    fixed "Persistente Journal-Logs aktiviert"
else
    ok "Journal-Verzeichnis existiert"
fi

# Komprimierung und Speicherlimit
local jconf="/etc/systemd/journald.conf"
declare -A jparams=(
    ["Compress"]="yes"
    ["Storage"]="persistent"
    ["ForwardToSyslog"]="no"
)

for key in "${!jparams[@]}"; do
    local val="${jparams[$key]}"
    if grep -q "^${key}=${val}" "$jconf" 2>/dev/null; then
        ok "journald $key=$val"
    else
        if ! $DRY_RUN; then
            sed -i "s/^#*${key}=.*/${key}=${val}/" "$jconf" 2>/dev/null || \
                echo "${key}=${val}" >> "$jconf"
        fi
        fixed "journald $key=$val gesetzt"
    fi
done

if ! $DRY_RUN; then
    systemctl restart systemd-journald 2>/dev/null || true
fi

}

harden_auditd() { section "6.3 auditd konfigurieren"

# auditd installiert/aktiviert
if ! rpm -q audit >/dev/null 2>&1; then
    if ! $DRY_RUN; then
        dnf install -y audit >/dev/null 2>&1 || { error "auditd konnte nicht installiert werden"; return; }
    fi
    fixed "auditd installiert"
fi

if ! systemctl is-enabled auditd 2>/dev/null | grep -q "enabled"; then
    if ! $DRY_RUN; then
        systemctl enable --now auditd 2>/dev/null || true
    fi
    fixed "auditd aktiviert"
else
    ok "auditd ist aktiviert"
fi

# Audit-Regeln  CIS Level 1
local rules_file="/etc/audit/rules.d/99-cis-level1.rules"
if [[ ! -f "$rules_file" ]]; then
    if ! $DRY_RUN; then
        cat > "$rules_file" << 'AUDITEOF'

CIS Level 1 Audit Rules

Datei-Integritaet

-w /etc/passwd -p wa -k identity -w /etc/shadow -p wa -k identity -w /etc/group -p wa -k identity -w /etc/gshadow -p wa -k identity -w /etc/security/opasswd -p wa -k identity

Login-Events

-w /var/log/lastlog -p wa -k logins -w /var/run/faillock -p wa -k logins -w /var/log/tallylog -p wa -k logins

sudo Konfiguration

-w /etc/sudoers -p wa -k sudo_changes -w /etc/sudoers.d -p wa -k sudo_changes

Netzwerk-Konfiguration

-w /etc/hosts -p wa -k network_changes -w /etc/sysconfig/network -p wa -k network_changes -w /etc/sysconfig/network-scripts -p wa -k network_changes

SELinux

-w /etc/selinux -p wa -k selinux_changes

Zeitaenderungen

-a always,exit -F arch=b64 -S adjtimex -S settimeofday -k time_change -a always,exit -F arch=b32 -S adjtimex -S settimeofday -S stime -k time_change -a always,exit -F arch=b64 -S clock_settime -k time_change -a always,exit -F arch=b32 -S clock_settime -k time_change -w /etc/localtime -p wa -k time_change

User/Group-Aenderungen

-w /etc/login.defs -p wa -k login_changes -w /etc/securetty -p wa -k login_changes

Session-Tracking

-w /var/run/utmp -p wa -k session -w /var/log/wtmp -p wa -k session -w /var/log/btmp -p wa -k session

Kernel-Module

-w /sbin/insmod -p x -k kernel_modules -w /sbin/rmmod -p x -k kernel_modules -w /sbin/modprobe -p x -k kernel_modules -a always,exit -F arch=b64 -S init_module -S delete_module -k kernel_modules

MAC Policy

-w /etc/selinux/ -p wa -k MAC_policy

Finalize diese Regel muss am Ende stehen

-e 2 AUDITEOF # Regeln laden augenrules --load 2>/dev/null || auditctl -R "$rules_file" 2>/dev/null || true fi fixed "CIS Level 1 Audit-Regeln konfiguriert" else ok "CIS Audit-Regeln existieren bereits" fi

# Audit-Konfiguration
local audit_conf="/etc/audit/auditd.conf"
if [[ -f "$audit_conf" ]]; then
    backup_file "$audit_conf"
    # max_log_file_action
    if ! grep -q "^max_log_file_action = keep_logs" "$audit_conf"; then
        if ! $DRY_RUN; then
            sed -i 's/^max_log_file_action.*/max_log_file_action = keep_logs/' "$audit_conf"
        fi
        fixed "auditd max_log_file_action = keep_logs"
    else
        ok "auditd max_log_file_action OK"
    fi

    # space_left_action
    if ! grep -q "^space_left_action = email" "$audit_conf"; then
        if ! $DRY_RUN; then
            sed -i 's/^space_left_action.*/space_left_action = email/' "$audit_conf"
        fi
        fixed "auditd space_left_action = email"
    fi
fi

}

harden_logfile_permissions() { section "6.2/6.3 Logfile-Berechtigungen"

# /var/log Berechtigungen
find /var/log -type f -perm /g+wx,o+rwx -exec chmod g-wx,o-rwx {} + 2>/dev/null || true
ok "Logfile-Berechtigungen geprüft und korrigiert"

}

══════════════════════════════════════════════════════════════════════════════

#SEKTION 7: System Maintenance

══════════════════════════════════════════════════════════════════════════════

harden_system_files() { section "7.1 Systemdatei-Berechtigungen"

# Kritische Dateien
declare -A file_perms=(
    ["/etc/passwd"]="644"
    ["/etc/shadow"]="000"
    ["/etc/group"]="644"
    ["/etc/gshadow"]="000"
    ["/etc/passwd-"]="644"
    ["/etc/shadow-"]="000"
    ["/etc/group-"]="644"
    ["/etc/gshadow-"]="000"
)

for file in "${!file_perms[@]}"; do
    local expected="${file_perms[$file]}"
    if [[ -f "$file" ]]; then
        local current
        current=$(stat -c "%a" "$file")
        if [[ "$current" != "$expected" ]]; then
            if ! $DRY_RUN; then
                chmod "$expected" "$file"
                chown root:root "$file"
            fi
            fixed "$file: $current -> $expected"
        else
            ok "$file: $current"
        fi
    fi
done

# SUID/SGID Binaries prüfen (nur Warnung)
info "Suche SUID/SGID-Binaries (nur Info)..."
local suid_count
suid_count=$(find / -xdev \( -perm -4000 -o -perm -2000 \) -type f 2>/dev/null | wc -l)
info "SUID/SGID-Binaries gefunden: $suid_count (manuell prüfen empfohlen)"

}

harden_user_groups() { section "7.2 User & Group Einstellungen"

# Prüfe auf Accounts ohne Passwort
local no_pw
no_pw=$(awk -F: '($2 == "" ) { print $1 }' /etc/shadow 2>/dev/null || true)
if [[ -n "$no_pw" ]]; then
    warn "Accounts OHNE Passwort gefunden: $no_pw"
    warn "Bitte manuell sperren: passwd -l <user>"
else
    ok "Keine Accounts ohne Passwort"
fi

# root UID 0 einzig
local uid0_users
uid0_users=$(awk -F: '($3 == 0 && $1 != "root") { print $1 }' /etc/passwd)
if [[ -n "$uid0_users" ]]; then
    warn "Nicht-root Accounts mit UID 0: $uid0_users"
else
    ok "Nur root hat UID 0"
fi

# root GID prüfen
local root_gid
root_gid=$(id -g root 2>/dev/null)
if [[ "$root_gid" == "0" ]]; then
    ok "root GID ist 0"
else
    warn "root GID ist $root_gid (sollte 0 sein)"
fi

# Prüfe PATH in /etc/profile
if echo "$PATH" | grep -q "::"; then
    warn "PATH enthält leere Einträge (::)"
elif echo "$PATH" | grep -q ":$"; then
    warn "PATH endet mit Doppelpunkt"
elif echo "$PATH" | grep -q "\."; then
    warn "PATH enthält relativen Pfad (.)"
else
    ok "Root-PATH enthält keine unsicheren Einträge"
fi

}

══════════════════════════════════════════════════════════════════════════════

ZUSAMMENFASSUNG

══════════════════════════════════════════════════════════════════════════════

print_summary() { echo "" | tee -a "$LOGFILE" echo -e "${BOLD}${CYAN}╔══════════════════════════════════════════════════════════════╗${NC}" | tee -a "$LOGFILE" echo -e "${BOLD}${CYAN}║ ZUSAMMENFASSUNG ║${NC}" | tee -a "$LOGFILE" echo -e "${BOLD}${CYAN}╚══════════════════════════════════════════════════════════════╝${NC}" | tee -a "$LOGFILE" echo "" | tee -a "$LOGFILE" echo -e " ${GREEN}Bereits OK:${NC} $ALREADY_OK" | tee -a "$LOGFILE" echo -e " ${GREEN}Geändert/Fixed:${NC} $CHANGES_MADE" | tee -a "$LOGFILE" echo -e " ${YELLOW}Übersprungen:${NC} $SKIPPED" | tee -a "$LOGFILE" echo -e " ${RED}Fehler:${NC} $ERRORS" | tee -a "$LOGFILE" echo "" | tee -a "$LOGFILE" echo -e " Logfile: ${BOLD}$LOGFILE${NC}" | tee -a "$LOGFILE" echo -e " Backup-Dir: ${BOLD}$BACKUP_DIR${NC}" | tee -a "$LOGFILE" echo "" | tee -a "$LOGFILE"

if [[ $CHANGES_MADE -gt 0 ]]; then
    echo -e "  ${YELLOW}WICHTIG:${NC}" | tee -a "$LOGFILE"
    echo -e "  - SSH-Verbindung in separater Session testen bevor Logout" | tee -a "$LOGFILE"
    echo -e "  - Reboot empfohlen für Kernel-Modul- und sysctl-Änderungen" | tee -a "$LOGFILE"
    echo -e "  - Rollback: Backup-Verzeichnis $BACKUP_DIR" | tee -a "$LOGFILE"
fi

if $DRY_RUN; then
    echo "" | tee -a "$LOGFILE"
    echo -e "  ${YELLOW}=== DRY-RUN: Es wurden KEINE Änderungen vorgenommen ===${NC}" | tee -a "$LOGFILE"
fi

}

══════════════════════════════════════════════════════════════════════════════

MAIN

══════════════════════════════════════════════════════════════════════════════

main() { preflight "${1:-}"

# Sektion 1: Initial Setup
harden_filesystem_modules
harden_filesystem_partitions
harden_package_management
harden_selinux
harden_bootloader
harden_process_hardening
harden_crypto_policy
harden_banners

# Sektion 2: Services
harden_services
harden_client_services
harden_time_sync
harden_cron

# Sektion 3: Network
harden_network_devices
harden_network_modules
harden_network_params

# Sektion 4: Firewall
harden_firewall

# Sektion 5: Access Control
harden_ssh
harden_sudo
harden_pam
harden_password_policy

# Sektion 6: Logging & Auditing
harden_integrity
harden_journald
harden_auditd
harden_logfile_permissions

# Sektion 7: System Maintenance
harden_system_files
harden_user_groups

print_summary

}

main "$@"