Skip to content
Back to Blog
Security11 min read

Advanced Server Security Best Practices: 8 Hardening Steps

Move past firewall rules and SSH keys. These eight server hardening techniques tackle kernel parameters, privilege isolation, and attack-surface reduction for production infrastructure.

Written by Abdul AbrorTechnical Hosting Support Engineer
Advanced Server Security Best Practices: 8 Hardening Steps
On this page

You've closed the unnecessary ports, disabled password auth, and locked down your firewall. Good start. But production servers face threats that basic hardening won't catch: privilege escalation through SUID binaries, kernel exploits via unpatched syscalls, lateral movement after a container breakout, and data exfiltration that never touches your web logs.

The techniques below assume you already handle the fundamentals. We're focusing on the layers most hosting tutorials skip—kernel hardening, mandatory access control, capability dropping, and audit frameworks that actually tell you what happened during an incident.

1. Kernel parameter tuning for attack mitigation

The Linux kernel exposes hundreds of runtime parameters through sysctl. Most distributions ship with defaults optimized for compatibility, not security. A few targeted changes reduce your attack surface significantly.

Disable IPv6 if you're not using it. Attack tools scan both stacks, and every enabled protocol is another code path that can contain bugs:

net.ipv6.conf.all.disable_ipv6 = 1
net.ipv6.conf.default.disable_ipv6 = 1

Enable TCP SYN cookies to survive SYN flood attacks without dropping legitimate connections. The three-way handshake gets validated cryptographically instead of holding state:

net.ipv4.tcp_syncookies = 1

Restrict kernel pointer exposure to prevent attackers from discovering kernel memory layout, which defeats ASLR:

kernel.kptr_restrict = 2

Disable source packet routing. This ancient feature lets packets specify their own route through your network and is only used in attacks now:

net.ipv4.conf.all.accept_source_route = 0
net.ipv4.conf.default.accept_source_route = 0

Harden shared memory to prevent race condition exploits in temporary files:

kernel.shmmax = 68719476736
kernel.shmall = 4294967296

Put these in /etc/sysctl.d/99-security.conf and run sysctl -p /etc/sysctl.d/99-security.conf. Check your settings survive reboot.

2. Mandatory access control (SELinux or AppArmor)

Discretionary access control—standard Unix permissions—lets any process do anything its user can do. An attacker who compromises Apache gets full read access to every file the www-data user owns.

Mandatory access control (MAC) adds a second layer. Even if a process runs as root, MAC policies can deny it network access, restrict which files it touches, or block execution of binaries.

SELinux is the most powerful option but comes with a learning curve. Start by checking the current mode:

getenforce

If it returns "Permissive" or "Disabled", your MAC layer isn't protecting you. Permissive mode logs violations but doesn't block them—useful for building policies. Production servers should run "Enforcing".

Common denial scenarios I've debugged: - Apache can't write to a directory even though permissions look right: wrong SELinux context on the directory - Postfix can't connect to a database socket: policy doesn't allow the database client port - Custom application binary won't execute: missing execmem permission in the policy

Fix context issues with restorecon:

restorecon -Rv /var/www/uploads

For new applications, audit2allow generates policy modules from denial logs. Don't blindly apply these—review the access being requested first:

grep denied /var/log/audit/audit.log | audit2allow -M mypolicy
semodule -i mypolicy.pp

AppArmor is easier to learn and works well for confining specific daemons. Profiles live in /etc/apparmor.d/. Most distributions ship profiles for common services; enable them:

aa-enforce /etc/apparmor.d/usr.sbin.nginx

Either system prevents entire classes of post-exploitation activity. An attacker inside your web app can't modify system binaries or read SSH keys in /root/.ssh if the MAC policy says no.

3. Drop Linux capabilities for service isolation

Traditional Unix has two privilege levels: root can do everything, regular users can't. Linux capabilities split root's powers into distinct units. A process might need to bind port 80 (CAP_NET_BIND_SERVICE) but doesn't need to load kernel modules (CAP_SYS_MODULE).

Most daemons start as root, bind privileged ports, then drop to an unprivileged user. But they often retain capabilities they don't need. Check what a process actually has:

getpcaps $(pgrep nginx)

Systemd lets you strip capabilities in the service unit. For a web server that shouldn't modify the system clock or reboot the machine:

[Service]
CapabilityBoundingSet=CAP_NET_BIND_SERVICE CAP_SETUID CAP_SETGID
AmbientCapabilities=CAP_NET_BIND_SERVICE

The bounding set limits what the process can ever gain. Ambient capabilities are actually granted. After editing the unit, reload and restart:

systemctl daemon-reload
systemctl restart nginx

For containers, drop capabilities in the runtime config. Docker defaults are too permissive:

docker run --cap-drop=ALL --cap-add=NET_BIND_SERVICE myapp

I've seen privilege escalation attempts fail because the compromised process couldn't execute the syscalls required to inject code into another process—CAP_SYS_PTRACE was missing.

4. Restrict access to sensitive proc and sys files

The /proc filesystem exposes kernel and process state. Some files leak information useful for exploitation. Others let unprivileged users observe everything happening on the system.

Mount /proc with hidepid=2 to prevent users from seeing other users' processes:

proc    /proc    proc    defaults,hidepid=2    0 0

Add that line to /etc/fstab and remount. Now a compromised web application can't enumerate what else is running or read command-line arguments from other processes.

Restrict access to kernel logs, which often contain sensitive data:

kernel.dmesg_restrict = 1

Limit who can use perf to profile the system:

kernel.perf_event_paranoid = 3

Block unprivileged user namespaces if you're not running rootless containers. User namespaces enable several kernel privilege escalation techniques:

kernel.unprivileged_userns_clone = 0

These changes break some debugging workflows. That's the point. An attacker shouldn't be able to inspect your system from a low-privilege foothold.

5. File integrity monitoring that scales

You need to know when critical files change. AIDE and Tripwire are classic tools but generate noise on systems where package managers frequently update files.

Practical approach: monitor specific high-value paths instead of the entire filesystem. Watch for changes to: - Systemd units in /etc/systemd/system - Cron jobs in /etc/cron.* and /var/spool/cron - SSH authorized keys in /root/.ssh and /home/*/.ssh - System binaries in /usr/bin, /usr/sbin, /bin, /sbin - Web application entry points and configuration files

Use auditd rules to log modifications:

-w /etc/systemd/system -p wa -k systemd_changes
-w /etc/cron.d -p wa -k cron_changes
-w /root/.ssh -p wa -k root_ssh_changes
-w /usr/bin -p wa -k binary_changes

Add rules to /etc/audit/rules.d/audit.rules and reload:

augenrules --load

Search audit logs for specific keys:

ausearch -k systemd_changes -ts recent

For larger deployments, ship audit logs to a central SIEM. Local logs get wiped by attackers; remote logs survive.

I've investigated incidents where the only useful forensic data came from audit logs showing exactly which binary was replaced and when.

6. Lock down SUID and SGID binaries

SUID binaries run with their owner's privileges regardless of who executes them. They're designed for controlled privilege escalation—ping needs to open raw sockets, passwd needs to modify /etc/shadow. But every SUID root binary is a potential privilege escalation path.

Find all SUID binaries on your system:

find / -perm -4000 -type f -ls 2>/dev/null

For SGID:

find / -perm -2000 -type f -ls 2>/dev/null

Review the list. You'll find legitimate tools and some surprises. Do you really need pkexec, chsh, or newgrp? Remove the SUID bit from anything you don't actively use:

chmod u-s /usr/bin/chsh

Some distributions ship debugging tools with SUID set. gdb with SUID is a gift to attackers. Strip it:

chmod u-s /usr/bin/gdb

Partition your filesystem with the nosuid mount option where possible. Prevents any SUID binary on that partition from gaining privileges:

/dev/sda2    /home    ext4    defaults,nosuid,nodev    0 2
/dev/sda3    /tmp     ext4    defaults,nosuid,nodev,noexec    0 2

User home directories and temp storage should never need SUID execution.

7. Network namespace isolation for multi-tenant systems

If you run customer workloads or untrusted code, network namespace isolation adds a strong boundary. Each namespace gets its own network stack, routing table, and firewall rules. A process in one namespace can't sniff traffic or make connections visible in another.

Create a restricted namespace:

ip netns add isolated

Launch a process inside it:

ip netns exec isolated /usr/bin/myapp

The process sees only a loopback interface. No external network access unless you explicitly configure it. For controlled outbound access, set up a veth pair and NAT:

ip link add veth0 type veth peer name veth1
ip link set veth1 netns isolated
ip addr add 10.200.1.1/24 dev veth0
ip link set veth0 up
ip netns exec isolated ip addr add 10.200.1.2/24 dev veth1
ip netns exec isolated ip link set veth1 up
ip netns exec isolated ip route add default via 10.200.1.1
iptables -t nat -A POSTROUTING -s 10.200.1.0/24 -j MASQUERADE

Now the isolated namespace can make outbound connections, but inbound traffic must explicitly be forwarded. You control exactly which ports are reachable.

Combine this with cgroups for resource limits and you've built lightweight sandboxing without full containers.

8. Audit logging for forensics and compliance

Most servers log application activity but miss syscall-level actions. The auditd framework records kernel events: file access, network connections, process execution, user actions. When something breaks or an incident happens, audit logs tell you what actually occurred.

Enable auditing for authentication events:

-w /etc/pam.d -p wa -k pam_changes
-w /var/log/lastlog -p wa -k last_login

Log execution of administrative commands:

-a always,exit -F arch=b64 -S execve -F path=/usr/bin/passwd -k passwd_changes
-a always,exit -F arch=b64 -S execve -F path=/usr/sbin/useradd -k user_creation

Track network socket creation:

-a always,exit -F arch=b64 -S socket -k network_socket

Audit rules accumulate overhead. Don't monitor everything; focus on security-relevant actions. Test performance impact under load.

Query logs for failed login attempts:

ausearch -m USER_LOGIN -sv no

Or successful privilege escalations:

ausearch -m USER_ROLE_CHANGE -sv yes

Rotate and archive audit logs aggressively. They grow fast. Set retention in /etc/audit/auditd.conf:

max_log_file = 50
num_logs = 10
max_log_file_action = ROTATE

Ship logs off-system if you have the infrastructure. An attacker with root can tamper with local logs but can't reach a remote syslog server if you've locked down outbound traffic.

What happens when you skip these steps

Basic hardening stops opportunistic attacks—port scanners, brute force attempts, unpatched CVE exploits. Advanced hardening stops what comes next: the attacker who already has a foothold and is trying to move laterally, escalate privileges, or persist after reboot.

Without kernel hardening, they use source routing to bypass firewall rules or crash your TCP stack with a flood. Without MAC, they read every file your application user can access. Without capability limits, they inject code into other processes. Without audit logs, you never know what they did or how long they stayed.

I've worked incidents where the initial compromise was a minor web app bug. The real damage came from privilege escalation through an overlooked SUID binary and persistence via a modified systemd unit. The attacker spent weeks inside the environment because nothing flagged unusual syscall patterns or file modifications.

These techniques won't stop every attack. But they raise the bar. An attacker needs more skill, more time, and more noise to succeed. Often they move to easier targets instead.

Start with one layer at a time

Pick the technique that addresses your biggest current risk. Running public-facing apps? Deploy MAC first. Multi-tenant environment? Network namespaces buy isolation fast. Compliance requirement? Audit logging covers you.

Test each change in staging before production. Hardening that breaks your application or monitoring tools is worse than no hardening—it gives false confidence while leaving gaps.

Document what you've implemented and why. Six months from now, when a confused developer asks why their service can't bind port 443, you'll want a reference explaining your capability restrictions and how to grant exceptions properly.

FAQ

Does SELinux really slow down my web server?

Negligible impact for most workloads. Early versions had measurable overhead, but modern implementations cache policy decisions efficiently. Profile your application with and without enforcement if you're concerned.

Can I use these techniques on existing production servers?

Yes, but test in permissive or audit mode first. SELinux denials, capability restrictions, and namespace isolation can all break applications that rely on specific kernel features. Roll out changes incrementally.

How do I know which capabilities my application needs?

Run it with all capabilities dropped and watch for errors. Gradually add back only the ones that fix real failures. Tools like pscap and capsh help inspect running processes.

What if audit logs fill my disk?

Set size limits in auditd.conf, enable rotation, and use ausearch filters to reduce noise. For high-volume systems, send audit logs to a dedicated logging server with more storage.

Should I harden every server the same way?

No. Database servers need different capabilities than web servers. Developer workstations need user namespaces that production hosts don't. Tailor the rules to each system's actual role and threat model.