Hardening Linux Servers Based on CIS Benchmarks

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 sudo for 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_keys and 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 ssh or systemctl status sshd shows 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 --load errors or auditctl -l shows 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 ~/.ssh permissions are 0700 and authorized_keys is 0600. 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/tcp and 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, run sudo 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 -tulpen to identify the owning process, then disable the correct unit or remove the package. Confirm again with ss and 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

Leave A Comment

All fields marked with an asterisk (*) are required