Automating Linux endpoint hardening with Wazuh

| by | Wazuh 4.14.4
Post icon

Endpoint hardening is a continuous process for securing modern IT environments against vulnerabilities and misconfigurations. It reduces the attack surface of endpoints and strengthens defenses against cyber threats by enforcing standardized security configurations. Organizations typically rely on established guidelines and frameworks, such as the Center for Internet Security (CIS) Benchmarks and NIST, for hardening. These resources provide best practices and predefined baseline configurations to help secure IT systems effectively.

Establishing a secure environment is essential, but manually addressing security gaps across a large number of endpoints is time-consuming, inconsistent, and difficult to scale. As infrastructure grows, organizations need a reliable way to enforce and maintain a minimally acceptable security baseline. Automating this process enables consistent enforcement of security controls and helps sustain a strong security posture without constant manual intervention.

In this blog post, we explore how to automate Linux endpoint hardening using the Wazuh Command module while maintaining continuous visibility into configuration drift through the Wazuh Security Configuration Assessment (SCA) capability. The Wazuh Command module allows you to move beyond simple monitoring and into automated remediation actions. This approach ensures consistent security controls and higher compliance scores without the burden of manual intervention. 

Note

This blog post is based on the CIS Ubuntu 24.04 Benchmark v1.0.0. The configurations described here are intended for standalone endpoints. For managing multiple endpoints, you can use the Wazuh Centralized configuration on the Wazuh server.

Infrastructure

We use the following infrastructure to demonstrate the configuration required for automatic hardening of the monitored endpoints:

  • Wazuh 4.14.4 central components (Wazuh server, Wazuh indexer, Wazuh dashboard) installed on an Ubuntu 24.04 endpoint using the Quickstart guide.
  • An Ubuntu 24.04 endpoint with Wazuh agent 4.14.4 installed and enrolled in the Wazuh server.

Initial CIS benchmark score

Security Configuration Assessment (SCA) scans are enabled by default on monitored endpoints. Before applying the remediation script configuration, we review the Wazuh dashboard to confirm the initial CIS Ubuntu Linux 24.04 v1.0.0 SCA scan results.

Navigate to Endpoint security > Configuration Assessment on the Wazuh dashboard, then select the monitored Ubuntu endpoint to view the current SCA scan results. As seen in the image below, the initial scan has 100 of 226 tests passed and 53 Not applicable tests, which is a 44% score. 

Figure 1: Initial CIS Benchmark score of the monitored endpoint.
Figure 1: Initial CIS Benchmark score of the monitored endpoint.

The images below show details of some individual benchmark checks with Failed results.

  • 35517 - Ensure noexec option on /dev/shm partition
Figure 2: Initial SCA policy check 35517 result.
Figure 2: Initial SCA policy check 35517 result.
  • 35609 - Ensure packet redirect sending is disabled
Figure 3: Initial SCA policy check 35609 result.
Figure 3: Initial SCA policy check 35609 result.

Hardening the endpoint

This section outlines the steps required to configure automated hardening for Ubuntu endpoints. We create a script that executes commands to fix some of the failed policy checks from the initial SCA scan on the monitored endpoint. The Wazuh Command module is then configured to run this script periodically, ensuring the target configuration is maintained consistently across the monitored endpoints.

Ubuntu endpoint

Perform the steps below on the Ubuntu endpoint.

1. Create a Bash script linux_hardening.sh, in the /var/ossec/active-response/bin/ folder and add the content below. The script contains configuration commands to modify thirty-six (36) required settings in line with the CIS Benchmark requirements:

Warning: This script is a proof of concept (PoC). Review and validate it to ensure it meets the operational and security requirements of your environment.
#!/usr/bin/env bash
set -e

############################################
# 35517 Ensure noexec on /dev/shm

fstab_file="/etc/fstab"
shm_entry="tmpfs /dev/shm tmpfs defaults,nodev,nosuid,noexec 0 0"

if grep -q "^tmpfs /dev/shm" "$fstab_file"; then
    sed -i "s|^tmpfs /dev/shm.*|$shm_entry|" "$fstab_file"
else
    echo "$shm_entry" >> "$fstab_file"
fi

mount -o remount,noexec /dev/shm || true
systemctl daemon-reload


############################################
# 35545 Disable Automatic Error Reporting

systemctl disable apport.service --now || true
sed -i 's/enabled=1/enabled=0/' /etc/default/apport || true

############################################
# 35547 Local login banner

banner_msg="Authorized users only. All activities are monitored."
grep -qxF "$banner_msg" /etc/issue || echo "$banner_msg" > /etc/issue

############################################
# 35548 Remote login banner

grep -qxF "$banner_msg" /etc/issue.net || echo "$banner_msg" > /etc/issue.net

############################################
# 35553 GDM login banner

if dpkg -s gdm3 >/dev/null 2>&1; then

gdm_file="/etc/gdm3/greeter.dconf-defaults"

# Ensure section exists
grep -Eq '^\s*\[org/gnome/login-screen\]' "$gdm_file" 2>/dev/null || \
    echo "[org/gnome/login-screen]" >> "$gdm_file"

set_gdm_param() {
    local key="$1"
    local value="$2"

    if grep -Eq "^\s*#?\s*${key}\s*=" "$gdm_file"; then
        # Replace commented OR uncommented line
        sed -i "s|^\s*#\?\s*${key}\s*=.*|${key}=${value}|" "$gdm_file"
    else
        # Add under section
        sed -i "/^\[org\/gnome\/login-screen\]/a ${key}=${value}" "$gdm_file"
    fi
}

# Apply settings
set_gdm_param banner-message-enable true
set_gdm_param banner-message-text "'$banner_msg'"

dconf update

fi


############################################
# 35562 Disable avahi

for svc in avahi-daemon.socket avahi-daemon.service; do
    systemctl stop $svc || true
	systemctl kill $svc || true
	systemctl disable $svc || true
    systemctl mask $svc || true
done

############################################
# 35571 Disable print services

pkg="cups cups-browsed cups-filters"

for svc in cups.socket cups.service; do
    systemctl stop $svc || true
    systemctl mask $svc || true
	apt purge $pkg -y || true
done

############################################
# 35588 Configure systemd-timesyncd

timesync_file="/etc/systemd/timesyncd.conf"
if grep -q "^#NTP=" "$timesync_file"; then
    sed -i 's/^#NTP=.*/NTP=pool.ntp.org/' "$timesync_file"
elif ! grep -q "^NTP=" "$timesync_file"; then
    echo "NTP=pool.ntp.org" >> "$timesync_file"
fi
systemctl restart systemd-timesyncd

############################################
# 35594-35599 Cron permissions

for file in /etc/crontab /etc/cron.hourly /etc/cron.daily /etc/cron.weekly /etc/cron.monthly /etc/cron.d; do
    chown root:root "$file"
    chmod og-rwx "$file"
done

############################################
# 35609-35616 Network sysctl hardening

CONFIG_FILE="/etc/sysctl.d/99-hardening.conf"

sysctl_settings=(
"net.ipv4.conf.all.send_redirects=0"
"net.ipv4.conf.all.accept_redirects=0"
"net.ipv4.conf.all.secure_redirects=0"
"net.ipv4.conf.all.accept_source_route=0"
"net.ipv4.conf.all.rp_filter=1"
"net.ipv4.conf.all.log_martians=1"
"net.ipv4.conf.default.send_redirects=0"
"net.ipv4.conf.default.accept_redirects=0"
"net.ipv4.conf.default.secure_redirects=0"
"net.ipv4.conf.default.accept_source_route=0"
"net.ipv4.conf.default.rp_filter=1"
"net.ipv4.conf.default.log_martians=1"
"net.ipv6.conf.all.accept_redirects=0"
"net.ipv6.conf.default.accept_redirects=0"
)

for setting in "${sysctl_settings[@]}"; do
    key="${setting%%=*}"
     "$CONFIG_FILE" 2>/dev/null || echo "$setting" >> "$CONFIG_FILE"grep -q "^$key"
done

sysctl --system >/dev/null

############################################
# 35664 Ensure sudo log file

if ! grep -Eq '^\s*Defaults\s+logfile=' /etc/sudoers; then
    echo 'Defaults logfile="/var/log/sudo.log"' | EDITOR='tee -a' visudo
else
    sed -i 's|^\s*Defaults\s\+logfile=.*|Defaults logfile="/var/log/sudo.log"|' /etc/sudoers
    visudo -c
fi

############################################
# 35668 Restrict su command
grep -Eq '^\s*auth\s+required\s+pam_wheel\.so.*group=sudo' /etc/pam.d/su || \
    sed -i '/^auth/a auth required pam_wheel.so use_uid group=sudo' /etc/pam.d/su

############################################
# 35676-35683 Password policies (PAM)
apt-get install -y libpam-pwquality

# Set key=value
set_config() {
    local key="$1"
    local value="$2"
    local file="$3"

    if grep -Eq "^\s*${key}\s*=" "$file"; then
        # Replace existing (commented or not)
        sed -i "s|^\s*#\?\s*${key}\s*=.*|${key} = ${value}|" "$file"
    else
        # Add if missing
        echo "${key} = ${value}" >> "$file"
    fi
}

# pwquality.conf settings
pwquality_conf="/etc/security/pwquality.conf"

set_config difok 2 "$pwquality_conf"
set_config minlen 14 "$pwquality_conf"
set_config minclass 4 "$pwquality_conf"
set_config maxrepeat 3 "$pwquality_conf"
set_config maxsequence 3 "$pwquality_conf"


# faillock.conf settings
faillock_conf="/etc/security/faillock.conf"

set_config deny 5 "$faillock_conf"
set_config unlock_time 900 "$faillock_conf"
set_config root_unlock_time 60 "$faillock_conf"

############################################
# 35694-35698 Password aging

# Set login.defs values
set_login_def() {
    local key="$1"
    local value="$2"
    local file="/etc/login.defs"

    if grep -Eq "^\s*#?\s*${key}\b" "$file"; then
        sed -i "s|^\s*#\?\s*${key}.*|${key} ${value}|" "$file"
    else
        echo "${key} ${value}" >> "$file"
    fi
}

# Apply for future users
set_login_def PASS_MAX_DAYS 90
set_login_def PASS_MIN_DAYS 1
set_login_def PASS_WARN_AGE 7

# Apply to existing users (/etc/shadow via chage)
# Note - Passwords for users with password age over the setting should be changed first or else the user will be locked out.

for user in $(awk -F: '$3 >= 1000 && $1 != "nobody" {print $1}' /etc/passwd); do
    chage --mindays 1 --maxdays 90 --warndays 7 --inactive 45 "$user"
done

# Apply to root
chage --mindays 1 --maxdays 90 --warndays 7 --inactive 45 root


############################################
# 35723 auditd installed
apt-get install -y auditd audispd-plugins
systemctl enable auditd --now

echo "$(date '+%Y-%m-%d %H:%M:%S') Hardening complete." >> /var/ossec/logs/ossec.log

Note

The script is designed to be modular. You can choose to add or remove any individual configuration check based on your specific needs.

This script remediates the following thirty-six (36) CIS requirements for Ubuntu endpoints:

  • 35517: Ensure noexec option set on /dev/shm partition.
  • 35545: Ensure Automatic Error Reporting is not enabled.
  • 35547: Ensure local login warning banner is configured properly.
  • 35548: Ensure remote login warning banner is configured properly.
  • 35553: Ensure GDM login banner is configured.
  • 35562: Ensure Avahi daemon services are not in use.
  • 35571: Ensure print server services are not in use.
  • 35588: Ensure systemd-timesyncd configured with an authorized timeserver.
  • 35594: Ensure permissions on /etc/crontab are configured.
  • 35595: Ensure permissions on /etc/cron.hourly are configured.
  • 35596: Ensure permissions on /etc/cron.daily are configured.
  • 35597: Ensure permissions on /etc/cron.weekly are configured.
  • 35598: Ensure permissions on /etc/cron.monthly are configured.
  • 35599: Ensure permissions on /etc/cron.d are configured.
  • 35609: Ensure packet redirect sending is disabled.
  • 35612: Ensure icmp redirects are not accepted.
  • 35613: Ensure secure icmp redirects are not accepted.
  • 35614: Ensure reverse path filtering is enabled.
  • 35615: Ensure source routed packets are not accepted.
  • 35616: Ensure suspicious packets are logged.
  • 35664: Ensure sudo log file exists.
  • 35668: Ensure access to the su command is restricted.
  • 35676: Ensure password failed attempts lockout is configured.
  • 35677: Ensure password unlock time is configured.
  • 35678: Ensure password failed attempts lockout includes the root account.
  • 35679: Ensure the password number of changed characters is configured.
  • 35680: Ensure minimum password length is configured.
  • 35681: Ensure password complexity is configured.
  • 35682: Ensure password with the same consecutive characters is configured.
  • 35683: Ensure the password maximum sequential characters is configured.
  • 35694: Ensure password expiration is configured.
  • 35695: Ensure the minimum password days is configured.
  • 35696: Ensure password expiration warning days are configured.
  • 35697: Ensure a strong password hashing algorithm is configured.
  • 35698: Ensure inactive password lock is configured.
  • 35723: Ensure auditd packages are installed.

2. Append the following configuration to the local configuration file, /var/ossec/etc/ossec.conf, to automate the execution of the script:

<ossec_config>
  <wodle name="command">
    <disabled>no</disabled>
    <tag>hardening-script</tag>
    <command>/var/ossec/active-response/bin/linux_hardening.sh</command>
    <interval>7d</interval>
    <ignore_output>yes</ignore_output>
    <run_on_start>yes</run_on_start>
    <verify_sha256><SHA_256_HASH></verify_sha256>
    <timeout>300</timeout>
  </wodle>
</ossec_config>

This configuration runs the remediation script every 7 days (7d) and also executes it immediately when the Wazuh agent starts. This ensures persistent policy compliance and remediation of unauthorized endpoint changes.

Note

Replace <SHA_256_HASH> with the SHA256 hash of the Bash script. The SHA256 hash can be obtained using the following command:

# sha256sum /var/ossec/active-response/bin/linux_hardening.sh

Optionally, you can use the Wazuh centralized configuration on the Wazuh server to distribute the script and configuration across your monitored Ubuntu endpoints.

3. Set the ownership and permissions of the /var/ossec/active-response/bin/linux_hardening.sh file. This ensures the script is only available to the root user and the wazuh group:

# chown root:wazuh /var/ossec/active-response/bin/linux_hardening.sh
# chmod 750 /var/ossec/active-response/bin/linux_hardening.sh

4. Restart the Wazuh agent to apply the configuration changes. This action triggers the remediation script configuration and initiates a fresh SCA scan.

# systemctl restart wazuh-agent

When the Wazuh agent is restarted, the remediation script is executed, and the SCA scan result is updated on the next SCA scan.

Reviewing the CIS Benchmark score

This section reviews the SCA scan results after applying the remediation script.

Navigate to Endpoint security > Configuration Assessment on the Wazuh dashboard and select the monitored Ubuntu endpoint to see the updated SCA scan result. In this instance, the result moves from 100 to 135 of 251 tests passed and 28 Not applicable tests, which is a 53% score.

Figure 4: Updated CIS Benchmark score of the monitored endpoint.
Figure 4: Updated CIS Benchmark score of the monitored endpoint.

Taking a closer look at some of the expanded remediated checks, we can see that the status has changed from Failed to Passed.

  • 35517 - Ensure noexec option on /dev/shm partition
Figure 5: Remediated SCA policy check 35517.
Figure 5: Remediated SCA policy check 35517.
  • 35609 - Ensure packet redirect sending is disabled
Figure 6: Remediated SCA policy check 35609.
Figure 6: Remediated SCA policy check 35609.

Conclusion

The Wazuh SCA module provides continuous visibility into inconsistent configuration across your monitored endpoints. You can use the Wazuh Command module and custom scripts to automate repeatable remediation workflows that keep your Ubuntu endpoints in line with CIS Benchmarks. This approach helps security teams reduce response times and maintain a consistently hardened Ubuntu environment without the constant need for manual intervention.

If you have any questions about this blog post or Wazuh, we invite you to join our community, where our team will be happy to assist you.

References