1454 lines
52 KiB
Markdown
1454 lines
52 KiB
Markdown
|
||
# 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 "$@" |