A freshly installed Linux server is not a secure Linux server. The default configuration optimises for compatibility and ease of use, not for defence in depth. Hardening is the process of reducing the attack surface: removing unnecessary services, enforcing least privilege, enabling auditing, and configuring mandatory access controls.
This guide walks through hardening a Linux server systematically — following CIS Benchmark principles, configuring auditd for forensic-quality logging, setting up AppArmor (Ubuntu/Debian) or SELinux (RHEL/Rocky), tuning kernel parameters, and measuring your progress with Lynis.
Baseline: Know Your Starting Score
Before changing anything, run Lynis to get a baseline score and a prioritised list of findings:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
# Install Lynis
sudo apt install lynis # Debian/Ubuntu
sudo dnf install lynis # RHEL/Rocky/Fedora
# Run a full system audit
sudo lynis audit system
# Key output:
# [+] Hardening index : 58 [############ ]
# [+] Tests performed : 242
# [+] Plugins enabled : 0
#
# -[ Lynis 3.x Results ]-
# Warnings (3):
# - Found one or more vulnerable packages [PKGS-7392]
# - No firewall detected [FIRE-4512]
# - SSH root login is allowed [SSH-7412]
#
# Suggestions (47):
# - Install a PAM module for password strength [AUTH-9262]
# - Consider hardening SSH configuration [SSH-7408]
# ...
|
Save the full report:
1
|
sudo lynis audit system --report-file /var/log/lynis-report-$(date +%Y%m%d).dat
|
Run Lynis again after each hardening pass. Watch the index climb.
1. Keep the System Updated
The single highest-value action. Unpatched vulnerabilities are the most common initial access vector.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
# Debian/Ubuntu
sudo apt update && sudo apt upgrade -y
sudo apt install unattended-upgrades
sudo dpkg-reconfigure --priority=low unattended-upgrades
# Configure automatic security updates
cat > /etc/apt/apt.conf.d/50unattended-upgrades <<'EOF'
Unattended-Upgrade::Allowed-Origins {
"${distro_id}:${distro_codename}-security";
};
Unattended-Upgrade::AutoFixInterruptedDpkg "true";
Unattended-Upgrade::Remove-Unused-Dependencies "true";
Unattended-Upgrade::Automatic-Reboot "false"; // set true if you want auto-reboots
Unattended-Upgrade::Mail "admin@example.com";
EOF
# RHEL/Rocky
sudo dnf install dnf-automatic
sudo systemctl enable --now dnf-automatic-install.timer
# Check for known CVEs in installed packages (any distro)
sudo apt install -y debsecan # Debian/Ubuntu
debsecan --suite $(lsb_release -cs) --format detail --only-fixed
|
2. User and Authentication Hardening
Remove Unnecessary Users and Groups
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
# List all users with login shells
grep -v '/nologin\|/false' /etc/passwd
# Lock accounts that should not log in interactively
sudo usermod -L -s /usr/sbin/nologin sync
sudo usermod -L -s /usr/sbin/nologin games
sudo usermod -L -s /usr/sbin/nologin news
sudo usermod -L -s /usr/sbin/nologin uucp
# Find accounts with UID 0 (should only be root)
awk -F: '($3 == 0) { print $1 }' /etc/passwd
# Find accounts with empty passwords (should be none)
sudo awk -F: '($2 == "" ) { print $1 }' /etc/shadow
|
Password Policy with PAM
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
sudo apt install libpam-pwquality # Debian/Ubuntu
sudo dnf install libpwquality # RHEL
# /etc/security/pwquality.conf
cat > /etc/security/pwquality.conf <<'EOF'
minlen = 14 # minimum 14 characters
dcredit = -1 # at least 1 digit
ucredit = -1 # at least 1 uppercase
lcredit = -1 # at least 1 lowercase
ocredit = -1 # at least 1 special character
maxrepeat = 3 # no more than 3 consecutive identical chars
gecoscheck = 1 # disallow username in password
EOF
# Account lockout after failed attempts — /etc/pam.d/common-auth (Debian/Ubuntu)
# Add before other auth lines:
# auth required pam_faillock.so preauth silent audit deny=5 unlock_time=900
# auth [default=die] pam_faillock.so authfail audit deny=5 unlock_time=900
# Check failed login attempts
sudo faillock --user username
sudo faillock --reset --user username # unlock
|
Password Aging
1
2
3
4
5
6
7
8
9
10
|
# /etc/login.defs
sudo sed -i 's/^PASS_MAX_DAYS.*/PASS_MAX_DAYS 90/' /etc/login.defs
sudo sed -i 's/^PASS_MIN_DAYS.*/PASS_MIN_DAYS 1/' /etc/login.defs
sudo sed -i 's/^PASS_WARN_AGE.*/PASS_WARN_AGE 14/' /etc/login.defs
# Apply to existing accounts
sudo chage --maxdays 90 --mindays 1 --warndays 14 username
# Check current password aging settings
sudo chage -l username
|
Sudo Hardening
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
# Never edit /etc/sudoers directly — use visudo or a drop-in
sudo visudo -f /etc/sudoers.d/hardening
# Add these settings:
Defaults use_pty # prevent sudo from being used in scripts
Defaults logfile=/var/log/sudo.log
Defaults log_input, log_output
Defaults requiretty # require real TTY (prevents cron abuse)
Defaults passwd_timeout=1 # 1 minute to enter password
Defaults timestamp_timeout=5 # re-require password after 5 minutes idle
Defaults badpass_message="Authentication failed."
Defaults secure_path="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
# Restrict sudo to a specific group (wheel or sudo)
# Ensure ONLY trusted admins are in this group
# %sudo ALL=(ALL:ALL) ALL
|
3. SSH Hardening
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
|
# /etc/ssh/sshd_config — replace the relevant lines or use a drop-in:
cat > /etc/ssh/sshd_config.d/hardening.conf <<'EOF'
# Disable root login
PermitRootLogin no
# Key auth only — no passwords
PasswordAuthentication no
PermitEmptyPasswords no
ChallengeResponseAuthentication no
KbdInteractiveAuthentication no
# Disable legacy protocols and features
Protocol 2
X11Forwarding no
AllowTcpForwarding no
AllowAgentForwarding no
PermitUserEnvironment no
# Use strong ciphers, MACs, and KEX algorithms
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
KexAlgorithms curve25519-sha256,curve25519-sha256@libssh.org,diffie-hellman-group16-sha512
# Restrict access to specific users or groups
AllowGroups sshusers admins
# Idle timeout — disconnect inactive sessions after 15 minutes
ClientAliveInterval 300
ClientAliveCountMax 3
# Limit authentication attempts
MaxAuthTries 3
MaxSessions 4
MaxStartups 10:30:60
# Log more verbosely
LogLevel VERBOSE
# Disable host-based authentication
HostbasedAuthentication no
IgnoreUserKnownHosts yes
IgnoreRhosts yes
EOF
sudo sshd -t # validate config before restarting
sudo systemctl restart sshd
|
4. Firewall Configuration
UFW (Debian/Ubuntu)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
sudo apt install ufw
# Default deny all inbound, allow all outbound
sudo ufw default deny incoming
sudo ufw default allow outgoing
# Allow only what you need
sudo ufw allow 22/tcp # SSH (or your custom port)
sudo ufw allow 80/tcp # HTTP
sudo ufw allow 443/tcp # HTTPS
# Rate limit SSH to prevent brute-force
sudo ufw limit ssh
# Enable
sudo ufw enable
sudo ufw status verbose
|
firewalld (RHEL/Rocky)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
sudo systemctl enable --now firewalld
# List current zones and rules
sudo firewall-cmd --list-all
# Allow services
sudo firewall-cmd --permanent --add-service=ssh
sudo firewall-cmd --permanent --add-service=http
sudo firewall-cmd --permanent --add-service=https
# Remove a service
sudo firewall-cmd --permanent --remove-service=telnet
sudo firewall-cmd --reload
|
Kernel-Level Hardening with nftables
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
|
# /etc/nftables.conf — minimal ruleset
cat > /etc/nftables.conf <<'EOF'
#!/usr/sbin/nft -f
flush ruleset
table inet filter {
chain input {
type filter hook input priority 0; policy drop;
# Allow established and related
ct state established,related accept
# Allow loopback
iif lo accept
# Allow ICMP (ping) — rate limited
ip protocol icmp limit rate 4/second accept
ip6 nexthdr ipv6-icmp limit rate 4/second accept
# Allow SSH
tcp dport 22 ct state new limit rate 15/minute accept
# Allow HTTP/HTTPS
tcp dport { 80, 443 } accept
# Log and drop everything else
log prefix "nftables-drop: " drop
}
chain forward {
type filter hook forward priority 0; policy drop;
}
chain output {
type filter hook output priority 0; policy accept;
}
}
EOF
sudo systemctl enable --now nftables
|
5. Kernel Parameter Hardening (sysctl)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
|
cat > /etc/sysctl.d/99-hardening.conf <<'EOF'
# ============================================================
# Network hardening
# ============================================================
# Disable IP forwarding (enable only if this is a router)
net.ipv4.ip_forward = 0
net.ipv6.conf.all.forwarding = 0
# Disable source routing (prevents routing spoofing)
net.ipv4.conf.all.accept_source_route = 0
net.ipv4.conf.default.accept_source_route = 0
net.ipv6.conf.all.accept_source_route = 0
# Enable reverse path filtering (block spoofed packets)
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.default.rp_filter = 1
# Ignore ICMP redirects (prevent MITM via routing manipulation)
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.default.accept_redirects = 0
net.ipv6.conf.all.accept_redirects = 0
net.ipv4.conf.all.send_redirects = 0
# Ignore broadcast ICMP (smurf attack mitigation)
net.ipv4.icmp_echo_ignore_broadcasts = 1
# Ignore bogus ICMP error responses
net.ipv4.icmp_ignore_bogus_error_responses = 1
# Enable SYN cookies (SYN flood protection)
net.ipv4.tcp_syncookies = 1
# Log martian packets (invalid source/dest addresses)
net.ipv4.conf.all.log_martians = 1
net.ipv4.conf.default.log_martians = 1
# Increase the TCP FIN timeout to reduce socket reuse risk
net.ipv4.tcp_fin_timeout = 15
# Disable IPv6 if not needed
# net.ipv6.conf.all.disable_ipv6 = 1
# net.ipv6.conf.default.disable_ipv6 = 1
# ============================================================
# Kernel hardening
# ============================================================
# Restrict dmesg to root only
kernel.dmesg_restrict = 1
# Restrict access to kernel pointers in /proc
kernel.kptr_restrict = 2
# Disable magic SysRq key (prevents reboot/sync via keyboard combo)
kernel.sysrq = 0
# Restrict ptrace to parent process only (prevents debugging of other processes)
kernel.yama.ptrace_scope = 1
# Randomise virtual address space (ASLR) — 2 is full randomisation
kernel.randomize_va_space = 2
# Prevent core dumps containing sensitive memory (can leak secrets)
fs.suid_dumpable = 0
# Restrict loading kernel modules after boot
# kernel.modules_disabled = 1 # WARNING: uncomment only after loading all needed modules
# ============================================================
# File system hardening
# ============================================================
# Protect hard links and symlinks
fs.protected_hardlinks = 1
fs.protected_symlinks = 1
fs.protected_fifos = 2
fs.protected_regular = 2
EOF
# Apply immediately
sudo sysctl --system
|
6. Filesystem and Mount Hardening
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
# /etc/fstab — add security mount options
# For /tmp: nodev (no device files), nosuid (ignore setuid), noexec (no executables)
# tmpfs /tmp tmpfs defaults,rw,nosuid,nodev,noexec,relatime,size=2G 0 0
# For /dev/shm: shared memory should not be executable
# tmpfs /dev/shm tmpfs defaults,nodev,nosuid,noexec 0 0
# Verify current mount options
findmnt -o TARGET,OPTIONS /tmp
findmnt -o TARGET,OPTIONS /dev/shm
# Find world-writable files (potential privilege escalation paths)
sudo find / -xdev -type f -perm -0002 -not -path "/proc/*" 2>/dev/null
# Find SUID/SGID binaries (inventory these carefully)
sudo find / -xdev \( -perm -4000 -o -perm -2000 \) -type f 2>/dev/null
# Remove unnecessary SUID bits
sudo chmod u-s /usr/bin/at # example — only remove if not needed
sudo chmod u-s /usr/bin/newgrp
# Secure /etc/crontab and cron directories
sudo chmod og-rwx /etc/crontab
sudo chmod og-rwx /etc/cron.*
sudo chmod og-rwx /var/spool/cron
|
7. Auditd: Forensic-Quality Syscall Logging
auditd records security-relevant events to a tamper-evident log. It’s the difference between “we think something happened” and “here is exactly what happened, when, and by whom.”
1
2
3
4
|
sudo apt install auditd audispd-plugins # Debian/Ubuntu
sudo dnf install audit # RHEL/Rocky
sudo systemctl enable --now auditd
|
Audit Rules
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
|
# /etc/audit/rules.d/hardening.rules
cat > /etc/audit/rules.d/hardening.rules <<'EOF'
# Delete all existing rules
-D
# Buffer size — increase for busy systems
-b 8192
# Failure mode: 1=printk, 2=panic
-f 1
# ============================================================
# Identity and authentication changes
# ============================================================
-w /etc/passwd -p wa -k identity
-w /etc/shadow -p wa -k identity
-w /etc/group -p wa -k identity
-w /etc/gshadow -p wa -k identity
-w /etc/sudoers -p wa -k sudoers
-w /etc/sudoers.d/ -p wa -k sudoers
# SSH configuration changes
-w /etc/ssh/sshd_config -p wa -k sshd
-w /etc/ssh/sshd_config.d/ -p wa -k sshd
# ============================================================
# Login and session tracking
# ============================================================
-w /var/log/faillog -p wa -k logins
-w /var/log/lastlog -p wa -k logins
-w /var/run/faillock -p wa -k logins
# ============================================================
# System calls — privilege escalation and suspicious activity
# ============================================================
# Monitor setuid/setgid calls
-a always,exit -F arch=b64 -S setuid -S setgid -S seteuid -S setegid -k setuid
-a always,exit -F arch=b32 -S setuid -S setgid -S seteuid -S setegid -k setuid
# Monitor privilege escalation syscalls
-a always,exit -F arch=b64 -S execve -F euid=0 -F auid>=1000 -F auid!=4294967295 -k sudo_commands
-a always,exit -F arch=b32 -S execve -F euid=0 -F auid>=1000 -F auid!=4294967295 -k sudo_commands
# Monitor kernel module loading/unloading
-w /sbin/insmod -p x -k modules
-w /sbin/rmmod -p x -k modules
-w /sbin/modprobe -p x -k modules
-a always,exit -F arch=b64 -S init_module -S delete_module -k modules
# Monitor network configuration changes
-a always,exit -F arch=b64 -S sethostname -S setdomainname -k network_changes
-w /etc/hosts -p wa -k hosts
-w /etc/network/ -p wa -k network_changes
-w /etc/sysconfig/network -p wa -k network_changes
# Monitor mount operations
-a always,exit -F arch=b64 -S mount -k mounts
# Monitor file deletion by privileged users
-a always,exit -F arch=b64 -S unlink -S unlinkat -S rename -S renameat -F auid>=1000 -F auid!=4294967295 -k delete
# Monitor /sbin and /usr/sbin execution
-w /sbin/ -p x -k sbin_execution
-w /usr/sbin/ -p x -k sbin_execution
# ============================================================
# Sensitive file access
# ============================================================
-w /etc/security/ -p wa -k security
-w /etc/pam.d/ -p wa -k pam
-w /etc/ssl/private/ -p r -k ssl_keys
# ============================================================
# Cron and scheduled task changes
# ============================================================
-w /etc/cron.allow -p wa -k cron
-w /etc/cron.deny -p wa -k cron
-w /etc/cron.d/ -p wa -k cron
-w /etc/cron.daily/ -p wa -k cron
-w /etc/cron.hourly/ -p wa -k cron
-w /var/spool/cron/ -p wa -k cron
# Make the audit configuration immutable — requires reboot to change rules
# Uncomment when rules are finalised:
# -e 2
EOF
# Load the new rules
sudo augenrules --load
sudo systemctl restart auditd
# Verify rules loaded
sudo auditctl -l | head -30
|
Querying Audit Logs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
# Search by key
sudo ausearch -k identity | aureport -f -i
sudo ausearch -k sudo_commands --start today | head -50
# Report on failed logins
sudo aureport --auth --failed
# Report on all account modifications
sudo aureport --mods
# Search for a specific user's activity
sudo ausearch -ua 1001 --start today
# Search for execve calls by root
sudo ausearch -sc execve -ui 0 --start this-month
# Generate a summary report
sudo aureport --summary
# Watch audit log in real time
sudo tail -f /var/log/audit/audit.log | ausearch --raw -i
|
8. AppArmor (Ubuntu/Debian)
AppArmor is a Mandatory Access Control (MAC) system. It confines programs to a defined set of resources — even if an attacker gains code execution, they’re limited to what the AppArmor profile permits.
1
2
3
4
5
6
|
# Check AppArmor status
sudo aa-status
# AppArmor has two modes:
# enforce — violations are blocked and logged
# complain — violations are logged only (use for profiling)
|
Working with Existing Profiles
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
# List all loaded profiles
sudo aa-status | grep -E "enforce|complain"
# Enable enforce mode for a profile that's in complain mode
sudo aa-enforce /etc/apparmor.d/usr.sbin.nginx
# Put a profile in complain mode temporarily (for debugging)
sudo aa-complain /etc/apparmor.d/usr.sbin.nginx
# Disable a profile entirely
sudo aa-disable /etc/apparmor.d/usr.sbin.nginx
# Reload a profile after editing
sudo apparmor_parser -r /etc/apparmor.d/usr.sbin.nginx
# Check AppArmor denials
sudo dmesg | grep -i apparmor | grep -i denied
sudo journalctl -k | grep 'apparmor="DENIED"'
|
Writing a Custom Profile
Use aa-genprof to generate a profile by watching what a program does:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
# Generate a profile interactively
sudo aa-genprof /usr/local/bin/myapp
# In another terminal, exercise the application
# curl http://localhost:8080/
# systemctl restart myapp
# Back in aa-genprof, press 'S' to scan, approve or deny each access
# Press 'F' to finish — saves the profile to /etc/apparmor.d/
# Use complain mode first, run the app under load, then refine
sudo aa-complain /usr/local/bin/myapp
# ... run workload ...
sudo aa-logprof # review and approve logged accesses
sudo aa-enforce /usr/local/bin/myapp
|
A hand-written profile example:
# /etc/apparmor.d/usr.local.bin.myapp
#include <tunables/global>
/usr/local/bin/myapp {
#include <abstractions/base>
#include <abstractions/nameservice> # DNS resolution
#include <abstractions/ssl_certs> # read CA certs
# Binary itself
/usr/local/bin/myapp mr,
# Config files — read only
/etc/myapp/ r,
/etc/myapp/** r,
# Data directory — read/write
/var/lib/myapp/ rw,
/var/lib/myapp/** rw,
# Logs
/var/log/myapp/ rw,
/var/log/myapp/** rw,
# PID file
/run/myapp.pid rw,
# Outbound network (TCP only)
network tcp,
# Deny everything else — implicit in AppArmor, but explicit is clearer
deny /etc/shadow r,
deny /proc/*/mem rw,
deny @{HOME}/** rwx,
}
9. SELinux (RHEL/Rocky/CentOS)
SELinux labels every process, file, and socket with a security context. Policies define which contexts can interact. A compromised nginx process cannot read /etc/shadow regardless of Unix permissions — the SELinux label for nginx simply has no policy rule allowing it.
1
2
3
4
5
6
7
|
# Check SELinux status
getenforce # Enforcing / Permissive / Disabled
sestatus # detailed status
# Never disable SELinux in /etc/selinux/config — use Permissive for troubleshooting
sudo setenforce 0 # Permissive (temporary, survives until reboot)
sudo setenforce 1 # Enforcing
|
Working with Contexts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
# View SELinux context of files
ls -Z /etc/nginx/nginx.conf
# -rw-r--r--. root root system_u:object_r:httpd_config_t:s0 nginx.conf
# View context of processes
ps auxZ | grep nginx
# system_u:system_r:httpd_t:s0 nginx ...
# View context of a port
sudo semanage port -l | grep http
# http_port_t tcp 80, 443, 488, 8008, 8009, 8443
# Restore default context (fixes most "SELinux broke my app" issues)
sudo restorecon -Rv /var/www/html
sudo restorecon -Rv /etc/nginx
|
Fixing SELinux Denials
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
# View recent AVC (Access Vector Cache) denials
sudo ausearch -m avc --start today
sudo sealert -a /var/log/audit/audit.log # human-readable with fix suggestions
# Example denial message:
# type=AVC msg=audit(1711468800.123:456): avc: denied { read } for pid=1234
# comm="nginx" name="myapp.conf" dev="sda1" ino=5678
# scontext=system_u:system_r:httpd_t:s0
# tcontext=unconfined_u:object_r:admin_home_t:s0
# tclass=file permissive=0
# Fix 1: change the file's context to one nginx can access
sudo semanage fcontext -a -t httpd_config_t "/etc/myapp(/.*)?"
sudo restorecon -Rv /etc/myapp
# Fix 2: allow nginx to connect to a non-standard port
sudo semanage port -a -t http_port_t -p tcp 8080
# Fix 3: toggle a boolean (predefined policy relaxations)
sudo getsebool -a | grep httpd # list relevant booleans
sudo setsebool -P httpd_can_network_connect on
sudo setsebool -P httpd_read_user_content on
# Generate a custom policy module from denial logs (last resort)
sudo ausearch -m avc --start today | audit2allow -M myapp-policy
sudo semodule -i myapp-policy.pp
|
10. Remove Unnecessary Services and Packages
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
# List all running services
systemctl list-units --type=service --state=running
# Common services to disable if not needed
sudo systemctl disable --now avahi-daemon # mDNS/Zeroconf
sudo systemctl disable --now cups # printing
sudo systemctl disable --now bluetooth # Bluetooth
sudo systemctl disable --now rpcbind # NFS v2/v3 portmapper (if not using NFS)
sudo systemctl disable --now nfs-server
sudo systemctl disable --now telnet.socket
sudo systemctl disable --now rsh.socket
sudo systemctl disable --now rlogin.socket
# Find and remove packages with known vulnerabilities
sudo apt list --installed 2>/dev/null | grep -i telnet
sudo apt purge telnet nis rsh-client rsh-server
# Find all listening ports and the processes behind them
sudo ss -tlnp
sudo ss -ulnp
|
11. File Integrity Monitoring with AIDE
AIDE (Advanced Intrusion Detection Environment) creates a database of file hashes and attributes. Run it periodically to detect unauthorised changes.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
sudo apt install aide # Debian/Ubuntu
sudo dnf install aide # RHEL
# Initialise the database (run after finishing hardening)
sudo aideinit
sudo mv /var/lib/aide/aide.db.new /var/lib/aide/aide.db
# Run a check (compare current state to database)
sudo aide --check
# Example output when something changed:
# changed: /etc/passwd
# Mtime : 2026-03-25 10:00:00 | 2026-03-26 11:30:00
# SHA256 : abc123... | def456...
# Update the database after intentional changes
sudo aide --update
sudo mv /var/lib/aide/aide.db.new /var/lib/aide/aide.db
# Run daily via cron or systemd timer
echo "0 3 * * * root /usr/bin/aide --check | mail -s 'AIDE Report' admin@example.com" \
> /etc/cron.d/aide
|
12. Logging and Log Forwarding
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
# Ensure rsyslog or syslog-ng is running and configured
sudo systemctl enable --now rsyslog
# /etc/rsyslog.conf — forward critical logs to a remote SIEM
# *.crit @siem.internal:514 # UDP (unreliable)
# *.crit @@siem.internal:514 # TCP (reliable)
# Protect local logs from tampering
sudo chown root:adm /var/log/auth.log /var/log/syslog
sudo chmod 640 /var/log/auth.log /var/log/syslog
# Enable logrotate for audit logs
cat > /etc/logrotate.d/auditd <<'EOF'
/var/log/audit/audit.log {
weekly
rotate 13
compress
delaycompress
notifempty
missingok
postrotate
/sbin/service auditd restart > /dev/null 2>&1 || true
endscript
}
EOF
|
Hardening Checklist Summary
Copy this as a working checklist for each new server:
[ ] System fully patched; unattended-upgrades or dnf-automatic enabled
[ ] Unnecessary user accounts locked or removed
[ ] Password complexity policy configured (pam_pwquality)
[ ] Account lockout configured (pam_faillock)
[ ] SSH: root login disabled, password auth disabled, strong algorithms
[ ] Firewall enabled with default-deny inbound policy
[ ] sysctl hardening applied (rp_filter, SYN cookies, ptrace, ASLR, kptr_restrict)
[ ] /tmp and /dev/shm mounted with noexec,nosuid,nodev
[ ] SUID/SGID binaries inventoried; unnecessary bits removed
[ ] auditd running with identity, sudo, module, network, and delete rules
[ ] AppArmor (enforce) or SELinux (enforcing) active
[ ] AppArmor profiles in enforce mode for all internet-facing services
[ ] Unnecessary services disabled (avahi, cups, telnet, rsh)
[ ] Telnet, rsh, nis packages removed
[ ] AIDE database initialised; daily integrity check scheduled
[ ] Logs forwarded to remote syslog / SIEM
[ ] Lynis score ≥ 75 (rerun after each change)
Run Lynis one final time after completing the checklist:
1
|
sudo lynis audit system 2>&1 | grep -E "Hardening index|Warning|Suggestion" | head -30
|
A well-hardened general-purpose server should score 75–85. The remaining suggestions are usually minor or require trade-offs with functionality. Each control you implement reduces the blast radius of an incident — the goal isn’t a perfect score, it’s making your systems meaningfully harder to compromise and meaningfully easier to investigate when something does go wrong.
Comments