qasim@wiki:~$ ▍

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.

← Back to the Security & Hardening runbook