LUNAROPS · OPERATIONAL UPLINK 100% UPTIME 1,247d POSTS 893 JEFF.MOON@LUNAROPS.DEV UTC --:--:--

Linux Hardening Checklist

securitylinuxhardeningcisauditdapparmorselinuxlynissysadmin

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