Why CIS hardening becomes urgent after the “easy” phase
In the beginning, a Linux server is simple. One application, a few users, a firewall rule or two, and a sense that “we’ll tighten it later.” Then the server survives its first quarter. It gets a monitoring agent. A backup job. A second admin. A vendor asks for SSH access. A new integration needs a port opened “temporarily.” Logs start rotating. Keys start multiplying. And slowly, without anyone making a single reckless decision, the system becomes a living thing with history.
That is exactly when CIS Benchmarks stop being a checkbox and start being a stabilizer. They give us a repeatable way to reduce attack surface, enforce consistent controls, and produce evidence that stands up to audits. The goal is not to make the server “perfect.” The goal is to make it controlled: predictable configuration, measurable drift, and changes that survive reboots and personnel turnover.
Scope and approach
We are going to harden a Linux server using CIS Benchmark principles with a manual + scripted approach. Manual matters because auditors and incident responders need to understand intent and evidence. Scripted matters because enterprises need repeatability and drift control. We will do both: we will apply changes step-by-step, and we will also capture those changes in a simple, auditable script that can be reused safely.
- Platform: Linux (examples use Ubuntu Server 22.04 LTS and RHEL/Rocky/Alma 9 patterns where they differ)
- Audience: Enterprises and auditors
- Focus: CIS-aligned baseline hardening (access control, SSH, sysctl, firewall, logging, auditing, file permissions, services)
- Not included: Automation-only approaches; we will not rely solely on a single tool to “do everything.”
Prerequisites and system assumptions
Before we touch configuration, we need to be explicit about assumptions. CIS hardening is safe when we understand what the server does, how we access it, and what “breaking access” would mean.
- OS and package manager:
- Ubuntu Server 22.04 LTS (APT) or RHEL-compatible 9 (DNF/YUM).
- We assume the system is reasonably current on security updates.
- Access method:
- We assume we have console access (cloud serial console, hypervisor console, or physical) in case SSH is misconfigured.
- We assume we can use
sudofor administrative actions.
- Server role awareness:
- We must know whether this server is a bastion, application node, database node, or utility server.
- We must know which inbound ports are truly required. Hardening without this knowledge often causes outages.
- Change control:
- We should schedule a maintenance window for SSH and firewall changes.
- We should capture a baseline snapshot/AMI/VM checkpoint if available.
- Evidence and auditability:
- We will log what we change and verify after each major step.
- We will keep backups of configuration files before editing.
Step 1: Establish a baseline and capture evidence
Before we harden anything, we need a baseline. This gives us two benefits: we can prove what changed (audit evidence), and we can quickly spot unexpected drift later. We will collect OS details, network exposure, running services, and current security posture.
set -euo pipefail
echo "=== OS ==="
cat /etc/os-release || true
uname -a
echo "=== Identity ==="
id
sudo -n true 2>/dev/null && echo "sudo: OK (non-interactive)" || echo "sudo: requires password or not permitted"
echo "=== Network listeners (TCP/UDP) ==="
sudo ss -tulpen
echo "=== Active services ==="
sudo systemctl list-units --type=service --state=running
echo "=== SSH effective config (if sshd present) ==="
if command -v sshd >/dev/null 2>&1; then
sudo sshd -T | sort
else
echo "sshd not installed"
fi
echo "=== Kernel sysctl snapshot ==="
sudo sysctl -a 2>/dev/null | egrep '^(net.ipv4.conf.all.|net.ipv4.conf.default.|net.ipv4.tcp_|net.ipv4.icmp_|net.ipv6.conf.all.|net.ipv6.conf.default.|kernel.randomize_va_space|fs.suid_dumpable|kernel.core_pattern)' || true
We just captured a practical baseline: what is running, what is listening, and what the system believes its security-relevant kernel settings are. This output is the “before” picture we will compare against as we apply CIS-aligned controls.
Step 2: Patch level and secure package management
CIS hardening assumes we are not running known-vulnerable packages. We are going to apply security updates and ensure package sources are sane. This is also one of the easiest audit wins: “system is patched” is measurable.
Ubuntu Server 22.04 LTS
We will refresh package metadata and apply updates. We will also ensure unattended security updates are enabled so the server stays compliant over time.
set -euo pipefail
sudo apt-get update
sudo apt-get -y upgrade
sudo apt-get -y install unattended-upgrades apt-listchanges
sudo dpkg-reconfigure -f noninteractive unattended-upgrades
We updated installed packages and enabled unattended upgrades. This reduces exposure to known CVEs and helps maintain compliance between audit cycles.
Now we verify that unattended upgrades are configured and the service is active.
sudo systemctl status unattended-upgrades --no-pager
sudo grep -R "Unattended-Upgrade" -n /etc/apt/apt.conf.d/ 2>/dev/null | head -n 50
RHEL/Rocky/Alma 9
We will apply updates and enable automatic security updates via dnf-automatic. This keeps patch posture consistent without relying on manual intervention.
set -euo pipefail
sudo dnf -y update
sudo dnf -y install dnf-automatic
sudo sed -i 's/^apply_updatess*=.*/apply_updates = yes/' /etc/dnf/automatic.conf
sudo systemctl enable --now dnf-automatic.timer
We updated packages and enabled a timer that applies updates automatically. This supports ongoing compliance and reduces the “we forgot to patch” failure mode.
Now we verify the timer is active.
sudo systemctl status dnf-automatic.timer --no-pager
sudo systemctl list-timers --all | grep -E 'dnf-automatic|NEXT|LEFT' || true
Step 3: Create an auditable hardening workspace and backups
Hardening becomes fragile when edits are scattered and undocumented. We are going to create a dedicated directory for backups and change artifacts. This is simple, but it is the difference between “we think we hardened it” and “we can prove what we did.”
set -euo pipefail
sudo install -d -m 0750 /root/cis-hardening
sudo install -d -m 0750 /root/cis-hardening/backups
sudo install -d -m 0750 /root/cis-hardening/artifacts
sudo bash -c 'date -Is > /root/cis-hardening/artifacts/started_at.txt'
sudo bash -c 'hostnamectl > /root/cis-hardening/artifacts/hostnamectl.txt'
sudo bash -c 'ss -tulpen > /root/cis-hardening/artifacts/ss_before.txt'
We created a controlled workspace under /root with restricted permissions and captured initial artifacts. This supports audit evidence and safe rollback.
Step 4: Lock down SSH without locking ourselves out
SSH is usually the front door. CIS guidance pushes us toward key-based auth, no root login, and tight access control. The risk is obvious: a single mistake can cut off access. We will reduce that risk by validating configuration before restarting the service and by keeping a backup.
First, we confirm which SSH service is present and where its configuration lives.
set -euo pipefail
if systemctl list-unit-files | grep -q '^ssh.service'; then
echo "Detected: ssh.service (Debian/Ubuntu naming)"
elif systemctl list-unit-files | grep -q '^sshd.service'; then
echo "Detected: sshd.service (RHEL naming)"
else
echo "No SSH service detected"
fi
sudo test -f /etc/ssh/sshd_config && echo "Found /etc/ssh/sshd_config"
Now we back up the SSH configuration and create a hardened drop-in file. Using a drop-in is cleaner for audits and upgrades because it isolates our intent.
set -euo pipefail
sudo cp -a /etc/ssh/sshd_config /root/cis-hardening/backups/sshd_config.$(date +%F_%H%M%S)
sudo install -d -m 0755 /etc/ssh/sshd_config.d
sudo tee /etc/ssh/sshd_config.d/10-cis-hardening.conf >/dev/null <<'EOF'
# CIS-aligned SSH hardening (baseline)
Protocol 2
# Authentication
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
ChallengeResponseAuthentication no
UsePAM yes
# Reduce exposure
X11Forwarding no
AllowTcpForwarding no
AllowAgentForwarding no
PermitTunnel no
GatewayPorts no
PermitUserEnvironment no
# Session controls
ClientAliveInterval 300
ClientAliveCountMax 2
LoginGraceTime 30
MaxAuthTries 4
MaxSessions 10
# Crypto policy (keep conservative; align with org standards)
Ciphers [email protected],[email protected],[email protected]
MACs [email protected],[email protected]
KexAlgorithms curve25519-sha256,[email protected],diffie-hellman-group-exchange-sha256
# Logging
LogLevel VERBOSE
EOF
We backed up the original SSH configuration and added a hardened configuration drop-in. This enforces key-based authentication, blocks root login, reduces forwarding features often abused for lateral movement, and tightens session behavior.
Before we restart SSH, we validate the configuration syntax. This is the safest habit we can build.
set -euo pipefail
sudo sshd -t
If the syntax check returns silently, the configuration is valid. Now we restart the SSH service using the correct unit name and verify it is running.
set -euo pipefail
if systemctl list-unit-files | grep -q '^ssh.service'; then
sudo systemctl restart ssh
sudo systemctl status ssh --no-pager
elif systemctl list-unit-files | grep -q '^sshd.service'; then
sudo systemctl restart sshd
sudo systemctl status sshd --no-pager
fi
SSH is now running with the hardened settings. Next, we verify the effective configuration to confirm the server is actually enforcing what we intended.
sudo sshd -T | egrep -i 'permitrootlogin|passwordauthentication|kbdinteractiveauthentication|x11forwarding|allowtcpforwarding|loglevel|clientaliveinterval|maxauthtries' | sort
Access control note for enterprises
In many environments, we also need explicit allow-lists. If we know the administrative group, we can restrict SSH to that group. We will first detect a likely admin group and then apply an allow rule only if it exists.
set -euo pipefail
getent group sudo >/dev/null 2>&1 && echo "Group exists: sudo"
getent group wheel >/dev/null 2>&1 && echo "Group exists: wheel"
If one of these groups exists and is our approved admin group, we can enforce it. We will append a controlled rule in a separate drop-in so it is easy to audit and revert.
set -euo pipefail
ADMIN_GROUP=""
if getent group sudo >/dev/null 2>&1; then ADMIN_GROUP="sudo"; fi
if getent group wheel >/dev/null 2>&1; then ADMIN_GROUP="wheel"; fi
if [ -n "$ADMIN_GROUP" ]; then
sudo tee /etc/ssh/sshd_config.d/20-cis-allowlist.conf >/dev/null <<EOF
AllowGroups ${ADMIN_GROUP}
EOF
sudo sshd -t
if systemctl list-unit-files | grep -q '^ssh.service'; then sudo systemctl restart ssh; fi
if systemctl list-unit-files | grep -q '^sshd.service'; then sudo systemctl restart sshd; fi
fi
We restricted SSH access to an approved administrative group when available. This is a strong control for audit scope reduction and for limiting credential abuse.
Step 5: Enforce firewall policy with persistence
CIS expects us to control inbound traffic. The enterprise mistake is to “just enable a firewall” without confirming required ports, which causes outages. We will first identify the primary interface and current listeners, then apply a default-deny inbound policy while allowing SSH.
First, we detect the default route interface and capture it for evidence.
set -euo pipefail
EXT_IFACE=$(ip route show default 0.0.0.0/0 | awk '{print $5}' | head -n 1)
echo "External interface: ${EXT_IFACE}"
sudo ss -tulpen | sudo tee /root/cis-hardening/artifacts/ss_listeners_before_firewall.txt >/dev/null
We identified the primary interface and recorded current listeners. Now we apply firewall rules in an OS-appropriate way.
Ubuntu Server 22.04 LTS with UFW
We will install UFW if needed, set default policies, allow SSH, and enable the firewall. This persists across reboots and is easy to audit.
set -euo pipefail
sudo apt-get -y install ufw
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp comment 'SSH'
sudo ufw --force enable
We enforced a default-deny inbound policy and explicitly allowed SSH. This reduces exposed surface area immediately while keeping administrative access.
Now we verify firewall status and effective rules.
sudo ufw status verbose
RHEL/Rocky/Alma 9 with firewalld
We will ensure firewalld is enabled, set a conservative baseline, and allow SSH. This persists across reboots and aligns with common enterprise standards.
set -euo pipefail
sudo systemctl enable --now firewalld
sudo firewall-cmd --set-default-zone=public
sudo firewall-cmd --permanent --zone=public --add-service=ssh
sudo firewall-cmd --reload
We enabled a persistent firewall configuration and allowed SSH in the default zone. This establishes a controlled inbound policy.
Now we verify the active zone and allowed services.
sudo firewall-cmd --get-active-zones
sudo firewall-cmd --zone=public --list-all
Step 6: Kernel hardening with sysctl (persistent and auditable)
Network-level kernel settings are a classic CIS area: they reduce spoofing, improve TCP resilience, and limit risky behaviors like redirects. We will apply a controlled sysctl file under /etc/sysctl.d so it persists across reboots and is easy to review.
First, we back up current sysctl configuration references for evidence.
set -euo pipefail
sudo sysctl -a 2>/dev/null | sudo tee /root/cis-hardening/artifacts/sysctl_before.txt >/dev/null
sudo ls -la /etc/sysctl.d | sudo tee /root/cis-hardening/artifacts/sysctl_d_listing.txt >/dev/null
Now we create a CIS-aligned sysctl file. We will keep it conservative and broadly compatible. If the server is IPv6-disabled by design, we will handle that separately rather than forcing it here.
set -euo pipefail
sudo tee /etc/sysctl.d/60-cis-hardening.conf >/dev/null <<'EOF'
# CIS-aligned kernel network hardening baseline
# IP spoofing protection
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.default.rp_filter = 1
# Ignore ICMP redirects (prevents malicious route changes)
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.default.accept_redirects = 0
net.ipv4.conf.all.secure_redirects = 0
net.ipv4.conf.default.secure_redirects = 0
# Do not send redirects
net.ipv4.conf.all.send_redirects = 0
net.ipv4.conf.default.send_redirects = 0
# Log suspicious packets
net.ipv4.conf.all.log_martians = 1
net.ipv4.conf.default.log_martians = 1
# TCP hardening
net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_rfc1337 = 1
# Kernel address space randomization
kernel.randomize_va_space = 2
# Restrict core dumps for setuid programs
fs.suid_dumpable = 0
EOF
sudo sysctl --system
We wrote a persistent sysctl configuration and applied it immediately. The kernel now enforces stricter network behavior and safer defaults, and these settings will survive reboots.
Now we verify key values to confirm the system is enforcing them.
sudo sysctl net.ipv4.conf.all.rp_filter
sudo sysctl net.ipv4.conf.all.accept_redirects
sudo sysctl net.ipv4.conf.all.send_redirects
sudo sysctl net.ipv4.tcp_syncookies
sudo sysctl kernel.randomize_va_space
sudo sysctl fs.suid_dumpable
Step 7: Filesystem and permission hygiene
CIS benchmarks repeatedly come back to one theme: permissions are policy. If sensitive files are readable or writable by the wrong principals, the rest of the controls become negotiable. We will enforce safe permissions on common sensitive files and verify them.
First, we back up current permissions for evidence.
set -euo pipefail
sudo stat -c '%a %U:%G %n' /etc/passwd /etc/group /etc/shadow /etc/gshadow 2>/dev/null | sudo tee /root/cis-hardening/artifacts/core_file_perms_before.txt >/dev/null || true
Now we apply conservative permissions. These are standard expectations in enterprise Linux environments.
set -euo pipefail
sudo chown root:root /etc/passwd /etc/group /etc/shadow /etc/gshadow
sudo chmod 0644 /etc/passwd /etc/group
sudo chmod 0640 /etc/shadow /etc/gshadow
We ensured ownership is root and tightened permissions on credential-related files. This reduces the risk of credential disclosure and unauthorized modification.
Now we verify the resulting permissions.
sudo stat -c '%a %U:%G %n' /etc/passwd /etc/group /etc/shadow /etc/gshadow
Step 8: Time synchronization for audit integrity
Auditors do not just care that logs exist; they care that timestamps are trustworthy. Incident response depends on time alignment across systems. We will ensure time sync is enabled and verify it.
First, we check whether systemd-timesyncd or chrony is in use. Then we enable one consistent method.
Ubuntu Server 22.04 LTS
We will use systemd-timesyncd (default on Ubuntu Server) unless the organization standard is chrony. We will enable NTP and verify synchronization.
set -euo pipefail
sudo timedatectl set-ntp true
sudo systemctl enable --now systemd-timesyncd
timedatectl status
We enabled NTP and ensured the time sync service is active. Now we verify that the clock is synchronized.
timedatectl show -p NTPSynchronized -p NTP -p TimeUSec --value
RHEL/Rocky/Alma 9
We will use chrony, which is the common enterprise default on RHEL-like systems.
set -euo pipefail
sudo dnf -y install chrony
sudo systemctl enable --now chronyd
timedatectl status
We installed and enabled chrony. Now we verify sources and synchronization state.
sudo chronyc tracking
sudo chronyc sources -v | head -n 30
Step 9: Central logging posture and local log protections
CIS expects logging to be enabled and protected. Even if we forward logs to a SIEM, local logs still matter for short-term triage and for “what happened right before the network went away.” We will ensure rsyslog is present, running, and that log files are not world-writable.
First, we ensure rsyslog is installed and enabled.
Ubuntu Server 22.04 LTS
set -euo pipefail
sudo apt-get -y install rsyslog
sudo systemctl enable --now rsyslog
sudo systemctl status rsyslog --no-pager
RHEL/Rocky/Alma 9
set -euo pipefail
sudo dnf -y install rsyslog
sudo systemctl enable --now rsyslog
sudo systemctl status rsyslog --no-pager
We ensured the logging daemon is installed and active. Now we tighten permissions on common log directories and verify.
set -euo pipefail
sudo find /var/log -type f -perm -0002 -print -exec chmod o-w {} ; | sudo tee /root/cis-hardening/artifacts/world_writable_logs_fixed.txt >/dev/null || true
sudo find /var/log -type f -perm -0002 -print | head -n 50
We removed world-writable permissions from log files where present. This reduces the risk of log tampering by non-privileged users.
Step 10: Enable auditd for accountability
For enterprises and auditors, auditd is often non-negotiable. It provides a tamper-resistant trail of security-relevant events. We will install it, enable it at boot, and apply a minimal but meaningful ruleset aligned with common CIS expectations.
First, we install and enable auditd.
Ubuntu Server 22.04 LTS
set -euo pipefail
sudo apt-get -y install auditd audispd-plugins
sudo systemctl enable --now auditd
sudo systemctl status auditd --no-pager
RHEL/Rocky/Alma 9
set -euo pipefail
sudo dnf -y install audit audit-libs
sudo systemctl enable --now auditd
sudo systemctl status auditd --no-pager
We enabled auditd and ensured it starts at boot. Now we will apply a baseline rules file. We will keep it focused: identity files, sudo usage, time changes, and authentication events.
set -euo pipefail
sudo install -d -m 0750 /etc/audit/rules.d
sudo cp -a /etc/audit/rules.d /root/cis-hardening/backups/audit_rules_d.$(date +%F_%H%M%S) 2>/dev/null || true
sudo tee /etc/audit/rules.d/10-cis-baseline.rules >/dev/null <<'EOF'
# CIS-aligned audit baseline
# Increase buffer to reduce event loss under load
-b 8192
# Failure mode: 1=printk, 2=panic (enterprises often choose 1; choose 2 only with explicit approval)
-f 1
# Identity files
-w /etc/passwd -p wa -k identity
-w /etc/group -p wa -k identity
-w /etc/shadow -p wa -k identity
-w /etc/gshadow -p wa -k identity
# Sudoers and sudo usage
-w /etc/sudoers -p wa -k scope
-w /etc/sudoers.d -p wa -k scope
-w /var/log/sudo.log -p wa -k scope
# Time changes
-a always,exit -F arch=b64 -S adjtimex,settimeofday,clock_settime -k time-change
-w /etc/localtime -p wa -k time-change
# Authentication logs (paths vary; include common)
-w /var/log/auth.log -p wa -k auth
-w /var/log/secure -p wa -k auth
EOF
sudo augenrules --load
We created a persistent audit ruleset and loaded it. The system will now record changes to identity and privilege boundaries, and it will retain evidence of time manipulation attempts.
Now we verify that audit rules are loaded and auditd is functioning.
sudo auditctl -s
sudo auditctl -l | head -n 80
Step 11: Remove or disable unnecessary services safely
CIS hardening is not only about adding controls; it is also about removing what we do not need. Every enabled service is a potential entry point. The safe way is to list what is running, decide what is unnecessary for the server’s role, and disable it with verification.
First, we capture a list of enabled services for review and evidence.
set -euo pipefail
sudo systemctl list-unit-files --type=service --state=enabled | sudo tee /root/cis-hardening/artifacts/enabled_services.txt >/dev/null
sudo systemctl list-units --type=service --state=running | sudo tee /root/cis-hardening/artifacts/running_services.txt >/dev/null
We now have an auditable list of what is enabled and running. Next, we apply a conservative example: disabling services that are commonly unnecessary on servers. We will only disable a service if it exists, and we will verify after.
set -euo pipefail
for svc in avahi-daemon cups rpcbind nfs-server smbd vsftpd telnet.socket tftp.socket; do
if systemctl list-unit-files | awk '{print $1}' | grep -qx "${svc}"; then
sudo systemctl disable --now "${svc}" || true
fi
done
sudo systemctl --failed --no-pager
sudo ss -tulpen | sudo tee /root/cis-hardening/artifacts/ss_after_service_disable.txt >/dev/null
We disabled a set of commonly unneeded services when present and captured the new listener state. This reduces attack surface while keeping the process controlled and verifiable.
Step 12: Capture the hardening as a reusable script
Manual changes are valuable because they force us to think. But enterprises need repeatability. We are going to capture a baseline script that applies the same controls in a controlled way. This is not “one button hardening.” It is a documented, reviewable artifact that can be version-controlled and used consistently.
We will write a script into our hardening workspace. It will detect OS family, apply the core controls we implemented, and include verification outputs. We will keep it readable so auditors and security teams can review it.
set -euo pipefail
sudo tee /root/cis-hardening/cis_baseline_apply.sh >/dev/null <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
if [ -r /etc/os-release ]; then
. /etc/os-release
else
echo "Cannot read /etc/os-release"
exit 1
fi
timestamp() { date -Is; }
echo "[$(timestamp)] Starting CIS baseline apply on: ${PRETTY_NAME:-unknown}"
mkdir -p /root/cis-hardening/backups /root/cis-hardening/artifacts
chmod 0750 /root/cis-hardening /root/cis-hardening/backups /root/cis-hardening/artifacts
echo "[$(timestamp)] Capturing pre-change listeners"
ss -tulpen > /root/cis-hardening/artifacts/ss_pre.txt
# Packages and update posture
if command -v apt-get >/dev/null 2>&1; then
echo "[$(timestamp)] Detected APT system"
apt-get update
DEBIAN_FRONTEND=noninteractive apt-get -y upgrade
DEBIAN_FRONTEND=noninteractive apt-get -y install rsyslog auditd audispd-plugins ufw unattended-upgrades apt-listchanges
dpkg-reconfigure -f noninteractive unattended-upgrades || true
systemctl enable --now rsyslog
systemctl enable --now auditd
elif command -v dnf >/dev/null 2>&1; then
echo "[$(timestamp)] Detected DNF system"
dnf -y update
dnf -y install rsyslog audit dnf-automatic firewalld chrony
sed -i 's/^apply_updatess*=.*/apply_updates = yes/' /etc/dnf/automatic.conf || true
systemctl enable --now dnf-automatic.timer || true
systemctl enable --now rsyslog
systemctl enable --now auditd
systemctl enable --now firewalld
systemctl enable --now chronyd || true
else
echo "No supported package manager found"
exit 1
fi
# SSH hardening drop-in
if [ -f /etc/ssh/sshd_config ]; then
cp -a /etc/ssh/sshd_config "/root/cis-hardening/backups/sshd_config.$(date +%F_%H%M%S)"
install -d -m 0755 /etc/ssh/sshd_config.d
cat > /etc/ssh/sshd_config.d/10-cis-hardening.conf <<'CONF'
Protocol 2
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
ChallengeResponseAuthentication no
UsePAM yes
X11Forwarding no
AllowTcpForwarding no
AllowAgentForwarding no
PermitTunnel no
GatewayPorts no
PermitUserEnvironment no
ClientAliveInterval 300
ClientAliveCountMax 2
LoginGraceTime 30
MaxAuthTries 4
MaxSessions 10
LogLevel VERBOSE
CONF
sshd -t
if systemctl list-unit-files | grep -q '^ssh.service'; then systemctl restart ssh; fi
if systemctl list-unit-files | grep -q '^sshd.service'; then systemctl restart sshd; fi
fi
# Firewall baseline: allow SSH, deny inbound by default
if command -v ufw >/dev/null 2>&1; then
ufw default deny incoming
ufw default allow outgoing
ufw allow 22/tcp comment 'SSH'
ufw --force enable
elif command -v firewall-cmd >/dev/null 2>&1; then
firewall-cmd --set-default-zone=public
firewall-cmd --permanent --zone=public --add-service=ssh
firewall-cmd --reload
fi
# sysctl baseline
cat > /etc/sysctl.d/60-cis-hardening.conf <<'CONF'
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.default.rp_filter = 1
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.default.accept_redirects = 0
net.ipv4.conf.all.secure_redirects = 0
net.ipv4.conf.default.secure_redirects = 0
net.ipv4.conf.all.send_redirects = 0
net.ipv4.conf.default.send_redirects = 0
net.ipv4.conf.all.log_martians = 1
net.ipv4.conf.default.log_martians = 1
net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_rfc1337 = 1
kernel.randomize_va_space = 2
fs.suid_dumpable = 0
CONF
sysctl --system
# Core file permissions
chown root:root /etc/passwd /etc/group /etc/shadow /etc/gshadow
chmod 0644 /etc/passwd /etc/group
chmod 0640 /etc/shadow /etc/gshadow
# audit rules baseline
install -d -m 0750 /etc/audit/rules.d
cat > /etc/audit/rules.d/10-cis-baseline.rules <<'CONF'
-b 8192
-f 1
-w /etc/passwd -p wa -k identity
-w /etc/group -p wa -k identity
-w /etc/shadow -p wa -k identity
-w /etc/gshadow -p wa -k identity
-w /etc/sudoers -p wa -k scope
-w /etc/sudoers.d -p wa -k scope
-w /var/log/sudo.log -p wa -k scope
-a always,exit -F arch=b64 -S adjtimex,settimeofday,clock_settime -k time-change
-w /etc/localtime -p wa -k time-change
-w /var/log/auth.log -p wa -k auth
-w /var/log/secure -p wa -k auth
CONF
augenrules --load || true
echo "[$(timestamp)] Capturing post-change listeners"
ss -tulpen > /root/cis-hardening/artifacts/ss_post.txt
echo "[$(timestamp)] Verification summary"
echo "--- SSH effective settings (if available) ---"
if command -v sshd >/dev/null 2>&1; then
sshd -T | egrep -i 'permitrootlogin|passwordauthentication|kbdinteractiveauthentication|x11forwarding|allowtcpforwarding|loglevel' | sort || true
fi
echo "--- sysctl checks ---"
sysctl net.ipv4.conf.all.rp_filter net.ipv4.conf.all.accept_redirects net.ipv4.conf.all.send_redirects net.ipv4.tcp_syncookies kernel.randomize_va_space fs.suid_dumpable || true
echo "--- auditd status ---"
systemctl status auditd --no-pager || true
echo "[$(timestamp)] Done"
EOF
sudo chmod 0750 /root/cis-hardening/cis_baseline_apply.sh
We created a reusable, reviewable script that applies the same baseline controls and captures evidence artifacts. This supports consistent deployments and makes audits easier because the intent is written down in one place.
Now we verify the script is present and readable only by privileged users.
sudo ls -la /root/cis-hardening/cis_baseline_apply.sh
sudo stat -c '%a %U:%G %n' /root/cis-hardening/cis_baseline_apply.sh
Verification checklist for auditors
At this point, we should be able to demonstrate compliance-oriented outcomes. We will run a compact set of checks that map cleanly to common CIS expectations: patch posture, SSH restrictions, firewall enforcement, sysctl persistence, auditd operation, and logging.
set -euo pipefail
echo "=== Patch posture ==="
if command -v apt-get >/dev/null 2>&1; then
apt-cache policy | head -n 20
systemctl is-enabled unattended-upgrades && true || true
fi
if command -v dnf >/dev/null 2>&1; then
dnf check-update || true
systemctl is-enabled dnf-automatic.timer && true || true
fi
echo "=== SSH posture ==="
if command -v sshd >/dev/null 2>&1; then
sshd -T | egrep -i 'permitrootlogin|passwordauthentication|kbdinteractiveauthentication|x11forwarding|allowtcpforwarding|clientaliveinterval|maxauthtries|loglevel' | sort
fi
echo "=== Firewall posture ==="
if command -v ufw >/dev/null 2>&1; then
ufw status verbose
fi
if command -v firewall-cmd >/dev/null 2>&1; then
firewall-cmd --get-active-zones
firewall-cmd --zone=public --list-all
fi
echo "=== sysctl persistence ==="
test -f /etc/sysctl.d/60-cis-hardening.conf && echo "Found /etc/sysctl.d/60-cis-hardening.conf"
sysctl net.ipv4.conf.all.rp_filter net.ipv4.conf.all.accept_redirects net.ipv4.conf.all.send_redirects net.ipv4.tcp_syncookies kernel.randomize_va_space fs.suid_dumpable
echo "=== auditd ==="
systemctl is-active auditd
auditctl -s
auditctl -l | head -n 60
echo "=== logging ==="
systemctl is-active rsyslog
ls -la /var/log | head -n 50
We now have a repeatable verification set that produces evidence: service states, effective SSH configuration, firewall rules, sysctl values, and audit rules. This is the kind of output that survives scrutiny because it is direct and measurable.
Troubleshooting
SSH access fails after hardening
- Symptom: SSH login fails with
Permission denied (publickey). - Likely cause: Password authentication was disabled, but authorized keys were not installed for the user.
- Fix: Use console access to add the correct public key to
~/.ssh/authorized_keysand ensure permissions are correct.
set -euo pipefail
USER_NAME=$(id -un)
sudo install -d -m 0700 "/home/${USER_NAME}/.ssh"
sudo touch "/home/${USER_NAME}/.ssh/authorized_keys"
sudo chmod 0600 "/home/${USER_NAME}/.ssh/authorized_keys"
sudo chown -R "${USER_NAME}:${USER_NAME}" "/home/${USER_NAME}/.ssh"
echo "Paste the public key into /home/${USER_NAME}/.ssh/authorized_keys using a safe editor."
We ensured the SSH directory and authorized_keys file exist with correct permissions. Once the correct key is present, key-based SSH authentication will succeed again.
SSH service fails to restart
- Symptom:
systemctl status sshorsystemctl status sshdshows a failed state after changes. - Likely cause: A syntax error or unsupported cipher/KEX/MAC on the installed OpenSSH version.
- Fix: Validate config, then temporarily remove the crypto lines if needed.
set -euo pipefail
sudo sshd -t
sudo sed -i '/^Ciphers /d;/^MACs /d;/^KexAlgorithms /d' /etc/ssh/sshd_config.d/10-cis-hardening.conf
sudo sshd -t
if systemctl list-unit-files | grep -q '^ssh.service'; then sudo systemctl restart ssh; sudo systemctl status ssh --no-pager; fi
if systemctl list-unit-files | grep -q '^sshd.service'; then sudo systemctl restart sshd; sudo systemctl status sshd --no-pager; fi
We validated SSH configuration, removed potentially incompatible crypto directives, revalidated, and restarted the service. This restores availability while keeping the core hardening controls in place.
Firewall blocks required application traffic
- Symptom: Application health checks fail after enabling UFW/firewalld.
- Likely cause: Default-deny inbound policy is correct, but required ports were not explicitly allowed.
- Fix: Identify the listening port and allow it explicitly, then verify.
set -euo pipefail
sudo ss -tulpen
# Example: allow TCP 443 if the service is HTTPS
if command -v ufw >/dev/null 2>&1; then
sudo ufw allow 443/tcp comment 'HTTPS'
sudo ufw status verbose
fi
if command -v firewall-cmd >/dev/null 2>&1; then
sudo firewall-cmd --permanent --zone=public --add-service=https
sudo firewall-cmd --reload
sudo firewall-cmd --zone=public --list-all
fi
We identified active listeners, allowed the required service, and verified the firewall configuration. This keeps the default-deny posture intact while restoring required functionality.
auditd rules do not load
- Symptom:
augenrules --loaderrors orauditctl -lshows missing rules. - Likely cause: Conflicting rules files, syntax issues, or auditd not running.
- Fix: Check auditd status, validate rules directory, and reload.
set -euo pipefail
sudo systemctl status auditd --no-pager || true
sudo ls -la /etc/audit/rules.d
sudo augenrules --check || true
sudo augenrules --load
sudo auditctl -l | head -n 80
We confirmed auditd health, inspected the rules directory, checked rules, reloaded them, and verified the active rules list.
Common mistakes
Disabling password SSH before keys are confirmed
- Symptom: SSH login fails with
Permission denied (publickey)immediately after changes. - Fix: Use console access to install the correct public key and ensure
~/.sshpermissions are0700andauthorized_keysis0600. Then retry SSH.
Enabling a firewall without allowing SSH first
- Symptom: Existing SSH session stays open, but new SSH connections time out.
- Fix: From the existing session, add an allow rule for
22/tcpand re-check firewall status. If the session is lost, use console access to disable the firewall temporarily and re-apply rules safely.
Applying sysctl values that conflict with the environment
- Symptom: Network behavior changes unexpectedly, or certain routing/VPN behaviors break.
- Fix: Review
/etc/sysctl.d/60-cis-hardening.conf, revert the specific setting, runsudo sysctl --system, and document the exception with justification for audit purposes.
Assuming “service disabled” means “port closed”
- Symptom: A port remains open even after disabling a service.
- Fix: Use
sudo ss -tulpento identify the owning process, then disable the correct unit or remove the package. Confirm again withssand firewall rules.
How do we at NIILAA look at this
This setup is not impressive because it is complex. It is impressive because it is controlled. Every component is intentional. Every configuration has a reason. This is how infrastructure should scale — quietly, predictably, and without drama.
At NIILAA, we help organizations design, deploy, secure, and maintain CIS-aligned Linux baselines that hold up in production: clear standards, role-based exceptions, audit-ready evidence, and operational workflows that keep systems compliant without breaking delivery. We treat hardening as an engineering discipline—measurable, reviewable, and sustainable across fleets and teams.
Website: https://www.niilaa.com
Email: [email protected]
LinkedIn: https://www.linkedin.com/company/niilaa
Facebook: https://www.facebook.com/niilaa.llc