Host Controls — Hardening Runbook
Scope: Linux host running Wazuh (manager + indexer + dashboard, all-in-one).
Principle: every change below is additive or reload-only. Nothing here restarts wazuh-manager, wazuh-indexer, wazuh-dashboard, or filebeat. Where a service reload is needed, it's the config-owning service (sshd, auditd, chronyd, fail2ban) — never the Wazuh stack itself.
Run everything as root or via sudo. Test each block before moving to the next; each section is independent and can be applied/rolled back on its own.
1. SSH Policy
Edit a drop-in file instead of the main config, so changes are isolated and easy to revert:
sudo mkdir -p /etc/ssh/sshd_config.d
sudo tee /etc/ssh/sshd_config.d/99-hardening.conf > /dev/null <<'EOF'
# Protocol & auth
Protocol 2
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
KbdInteractiveAuthentication no
AuthenticationMethods publickey
PermitEmptyPasswords no
MaxAuthTries 3
LoginGraceTime 20
# Session hygiene
X11Forwarding no
AllowAgentForwarding no
AllowTcpForwarding no
ClientAliveInterval 300
ClientAliveCountMax 2
MaxSessions 4
MaxStartups 10:30:60
# Restrict who can even attempt login
AllowGroups ssh-users
# Crypto (modern, drop weak legacy algorithms)
KexAlgorithms curve25519-sha256,curve25519-sha256@libssh.org,diffie-hellman-group16-sha512
Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com
MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com
# Logging
LogLevel VERBOSE
EOF
Create the access-control group and add your admin account(s) before you reload — otherwise you can lock yourself out:
sudo groupadd -f ssh-users
sudo usermod -aG ssh-users <your_admin_user>
Validate syntax, then reload (does not drop existing sessions):
sudo sshd -t && sudo systemctl reload sshd
Wazuh benefit: sshd's LogLevel VERBOSE gives the Wazuh agent's sshd decoder richer data (key fingerprints, disconnect reasons) for rules 5700–5719.
2. Brute-Force Protection
fail2ban watching sshd (and, since this host also runs the Wazuh dashboard behind nginx/its own server, optionally the dashboard's login endpoint if you front it with a reverse proxy that logs failures).
sudo apt install -y fail2ban # or: sudo dnf install -y fail2ban
sudo tee /etc/fail2ban/jail.local > /dev/null <<'EOF'
[DEFAULT]
bantime = 1h
findtime = 10m
maxretry = 4
backend = systemd
banaction = iptables-multiport
ignoreip = 127.0.0.1/8 ::1
[sshd]
enabled = true
port = ssh
EOF
sudo systemctl enable --now fail2ban
sudo systemctl reload fail2ban # if already running, this re-reads jail.local
Verify:
sudo fail2ban-client status sshd
Wazuh benefit: fail2ban's own log (/var/log/fail2ban.log) can be added as a <localfile> in ossec.conf so bans/unbans show up as indexed alerts — gives you brute-force visibility even though fail2ban is doing the blocking, not Wazuh's active-response.
3. Privilege Model
Goal: no shared root logins, every privileged action attributable to a person, minimum standing privilege.
3.1 — Remove direct root SSH/login access (already done above via PermitRootLogin no). Confirm root password login is also disabled at the PAM level:
sudo passwd -l root
3.2 — Sudo, not su. Put admins in a dedicated group, not directly in wheel/sudo with blanket rights:
sudo groupadd -f wazuh-admins
sudo usermod -aG wazuh-admins <your_admin_user>
sudo visudo -f /etc/sudoers.d/10-wazuh-admins
Contents:
# Full sudo, but with password required every time and full logging
%wazuh-admins ALL=(ALL) ALL
Defaults:%wazuh-admins timestamp_timeout=5
Defaults:%wazuh-admins log_input, log_output
Defaults:%wazuh-admins logfile="/var/log/sudo.log"
3.3 — Restrict su to members of a wheel-equivalent group only (defense-in-depth if a password is ever compromised):
sudo sed -i 's/^#auth\s*required\s*pam_wheel.so/auth required pam_wheel.so use_uid group=wazuh-admins/' /etc/pam.d/su
3.4 — Service accounts. Confirm wazuh-indexer, wazuh (manager), and dashboard service users have no login shell:
for u in wazuh wazuh-indexer wazuh-dashboard; do
sudo usermod -s /usr/sbin/nologin "$u" 2>/dev/null
done
This is safe — it changes the account shell, not the running daemon process.
No service restart required for any of the above; visudo and PAM file edits take effect on the next sudo/su invocation.
4. Kernel Parameters
Drop-in sysctl file, applied live with sysctl --system (no reboot):
sudo tee /etc/sysctl.d/99-hardening.conf > /dev/null <<'EOF'
# Network — anti-spoofing / anti-redirect
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.default.rp_filter = 1
net.ipv4.conf.all.accept_source_route = 0
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.all.secure_redirects = 0
net.ipv4.conf.all.send_redirects = 0
net.ipv6.conf.all.accept_redirects = 0
net.ipv4.icmp_echo_ignore_broadcasts = 1
net.ipv4.tcp_syncookies = 1
net.ipv4.conf.all.log_martians = 1
# IP forwarding off unless this host is a router
net.ipv4.ip_forward = 0
net.ipv6.conf.all.forwarding = 0
# Kernel hardening
kernel.randomize_va_space = 2
kernel.kptr_restrict = 2
kernel.dmesg_restrict = 1
kernel.yama.ptrace_scope = 1
kernel.unprivileged_bpf_disabled = 1
kernel.perf_event_paranoid = 3
# Prevent core dumps of setuid programs leaking memory
fs.suid_dumpable = 0
# Restrict symlink/hardlink attacks
fs.protected_symlinks = 1
fs.protected_hardlinks = 1
EOF
sudo sysctl --system
net.ipv4.ip_forward — leave at 1 instead if this box does any routing/NAT (uncommon for a Wazuh all-in-one). Everything else is safe to apply live; none of these require a reboot or touch the Wazuh JVM (indexer) or manager processes.
5. Authentication Modules (PAM)
5.1 — Account lockout on repeated failures (pam_faillock):
sudo tee /etc/security/faillock.conf > /dev/null <<'EOF'
deny = 5
unlock_time = 900
even_deny_root
audit
EOF
On Debian/Ubuntu, ensure pam_faillock.so is enabled in /etc/pam.d/common-auth; on RHEL/CentOS it's usually already wired into /etc/pam.d/system-auth and password-auth via authselect:
sudo authselect enable-feature with-faillock 2>/dev/null || true
5.2 — Password quality (pam_pwquality):
sudo tee /etc/security/pwquality.conf > /dev/null <<'EOF'
minlen = 14
dcredit = -1
ucredit = -1
lcredit = -1
ocredit = -1
retry = 3
enforce_for_root
EOF
5.3 — Session resource limits (pam_limits) — cap fork bombs / fd exhaustion for interactive logins without touching daemon LimitNOFILE= settings already set by the Wazuh/indexer systemd units:
sudo tee /etc/security/limits.d/99-hardening.conf > /dev/null <<'EOF'
* hard nproc 4096
* hard core 0
EOF
No daemon restart needed — PAM re-reads these on each new authentication/session, so it applies to the next login, not existing sessions.
6. Audit Rules (auditd)
Purpose: tamper-evidence and an independent trail Wazuh can ingest via its auditd decoder/rule set (already built in), rather than replacing Wazuh's own log collection.
sudo apt install -y auditd audispd-plugins # or: sudo dnf install -y audit
sudo tee /etc/audit/rules.d/99-hardening.rules > /dev/null <<'EOF'
## Identity & privilege
-w /etc/passwd -p wa -k identity
-w /etc/shadow -p wa -k identity
-w /etc/group -p wa -k identity
-w /etc/sudoers -p wa -k identity
-w /etc/sudoers.d/ -p wa -k identity
## Auth config
-w /etc/pam.d/ -p wa -k auth_config
-w /etc/ssh/sshd_config -p wa -k sshd_config
-w /etc/ssh/sshd_config.d/ -p wa -k sshd_config
## Wazuh config integrity (belt-and-suspenders alongside its own FIM)
-w /var/ossec/etc/ossec.conf -p wa -k wazuh_config
-w /etc/wazuh-indexer/ -p wa -k wazuh_config
-w /etc/wazuh-dashboard/ -p wa -k wazuh_config
## Privileged command execution
-a always,exit -F arch=b64 -S execve -F euid=0 -k root_cmds
## Scheduled tasks
-w /etc/cron.d/ -p wa -k cron
-w /etc/crontab -p wa -k cron
## Make the audit trail itself tamper-evident (append-only for this boot)
-e 2
EOF
sudo augenrules --load
sudo systemctl enable --now auditd
Note: -e 2 locks the rule set until reboot (prevents runtime tampering) — it does not stop or restart any other service, and doesn't affect Wazuh at all. Load new rules going forward with another file + augenrules --load rather than editing this one.
Verify:
sudo auditctl -l | head
7. Time Synchronization
Accurate, monotonic time is a prerequisite for audit log integrity and for Wazuh's own alert correlation/timestamps — don't skip this on a SIEM host.
# Debian/Ubuntu
sudo apt install -y chrony
# RHEL/CentOS
sudo dnf install -y chrony
sudo tee /etc/chrony/conf.d/99-hardening.conf > /dev/null <<'EOF'
pool time.cloudflare.com iburst
pool time.google.com iburst
makestep 1.0 3
rtcsync
EOF
sudo systemctl enable --now chronyd
sudo systemctl reload chronyd 2>/dev/null || sudo systemctl restart chronyd
(chronyd doesn't support a live SIGHUP reload on all distros — restarting it is safe since it's a standalone time daemon, not part of the Wazuh stack.)
Verify sync:
chronyc tracking
chronyc sources -v
If this host is air-gapped, point at internal NTP servers instead of the public pools above.
8. Patch Automation
Scope this to security updates only, and explicitly exclude the Wazuh repo (which you already disabled per the install runbook) so automated patching can never silently upgrade Wazuh components out from under you.
Debian/Ubuntu — unattended-upgrades:
sudo apt install -y unattended-upgrades apt-listchanges
sudo tee /etc/apt/apt.conf.d/51-hardening-auto-upgrades > /dev/null <<'EOF'
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";
APT::Periodic::AutocleanInterval "7";
EOF
sudo tee /etc/apt/apt.conf.d/50unattended-upgrades.d-hardening > /dev/null <<'EOF'
Unattended-Upgrade::Origins-Pattern {
"origin=Ubuntu,archive=${distro_codename}-security";
"origin=Debian,codename=${distro_codename},label=Debian-Security";
};
Unattended-Upgrade::Package-Blacklist {
"wazuh-*";
"filebeat";
};
Unattended-Upgrade::Automatic-Reboot "false";
Unattended-Upgrade::SyslogEnable "true";
EOF
sudo systemctl enable --now unattended-upgrades
RHEL/CentOS — dnf-automatic:
sudo dnf install -y dnf-automatic
sudo sed -i \
-e 's/^upgrade_type =.*/upgrade_type = security/' \
-e 's/^apply_updates =.*/apply_updates = yes/' \
/etc/dnf/automatic.conf
# Exclude Wazuh packages explicitly (repo is already disabled, this is defense-in-depth)
echo -e "\n[base]\nexclude=wazuh-* filebeat" | sudo tee -a /etc/dnf/automatic.conf > /dev/null
sudo systemctl enable --now dnf-automatic.timer
Both approaches only ever touch the OS package set — the Wazuh repo stays disabled from your earlier install hardening, so wazuh-manager, wazuh-indexer, wazuh-dashboard, and filebeat are never candidates for automatic upgrade, and none of their running processes are restarted by this.
Rollback Reference
| Control | Rollback |
|---|---|
| SSH | delete /etc/ssh/sshd_config.d/99-hardening.conf, sshd -t && systemctl reload sshd |
| fail2ban | systemctl stop fail2ban or remove jail.local and reload |
| sudo/PAM privilege | remove /etc/sudoers.d/10-wazuh-admins, revert /etc/pam.d/su |
| sysctl | delete /etc/sysctl.d/99-hardening.conf, sysctl --system |
| PAM (faillock/pwquality/limits) | delete respective .conf/.d files added above |
| auditd | remove /etc/audit/rules.d/99-hardening.rules, augenrules --load (note: -e 2 requires reboot to fully unlock) |
| chrony | revert /etc/chrony/conf.d/99-hardening.conf |
| patch automation | disable the systemd timer/service (unattended-upgrades or dnf-automatic.timer) |
All changes above are config-file additions read by the managing daemon (sshd, PAM, the kernel, auditd, chronyd, the package manager) — at no point does this runbook stop, restart, or reconfigure wazuh-manager, wazuh-indexer, wazuh-dashboard, or filebeat.