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

1454 lines
52 KiB
Markdown
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.
# 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 [[ "$current" == "$value" ]]; 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 "$@"