CSIPE

Published

- 39 min read

How to Harden a Linux Server for Your Application


Secure Software Development Book

How to Write, Ship, and Maintain Code Without Shipping Vulnerabilities

A hands-on security guide for developers and IT professionals who ship real software. Build, deploy, and maintain secure systems without slowing down or drowning in theory.

Buy the book now
The Anonymity Playbook Book

Practical Digital Survival for Whistleblowers, Journalists, and Activists

A practical guide to digital anonymity for people who can’t afford to be identified. Designed for whistleblowers, journalists, and activists operating under real-world risk.

Buy the book now
The Digital Fortress Book

The Digital Fortress: How to Stay Safe Online

A simple, no-jargon guide to protecting your digital life from everyday threats. Learn how to secure your accounts, devices, and privacy with practical steps anyone can follow.

Buy the book now

Introduction

Linux servers power a significant portion of the modern web and enterprise applications due to their reliability, flexibility, and open-source nature. However, like any other system, they are vulnerable to cyberattacks if not properly secured. Server hardening—the process of enhancing the security of a server—ensures your Linux environment is robust against potential threats.

This comprehensive guide outlines essential steps to harden your Linux server, protecting your applications and data from vulnerabilities.

Linux Server Security: The Layered Defence Model

Effective server hardening is not a single action; it is a collection of overlapping controls. Each layer compensates for weaknesses in the layer above it — even if an attacker bypasses the outermost defence, deeper controls slow or stop further progress.

   graph TD
    A[Internet / Untrusted Traffic] --> B[Firewall — ufw / nftables]
    B --> C[SSH Hardening & Fail2ban]
    C --> D[User Privilege Controls & sudo]
    D --> E[Mandatory Access Control\nSELinux / AppArmor]
    E --> F[Kernel Hardening — sysctl]
    F --> G[Auditing, IDS & Log Monitoring]
    G --> H[Encrypted Storage & Secure Backups]
    H --> I[Your Application]
    style I fill:#4caf50,color:#fff
    style A fill:#f44336,color:#fff

This guide follows this layered model from the outermost network controls inward to the kernel. Work through each layer in order: there is little value in hardening the kernel if SSH is left wide open.

Understanding the Threat Landscape for Linux Servers

Before configuring a single setting, it is worth understanding exactly what you are defending against. Linux servers are targeted by a diverse range of adversaries with different motivations, toolsets, and levels of sophistication. Grouping these threats helps you prioritise controls correctly.

Automated scanners and opportunistic bots are by far the most common threat. Within minutes of a server being given a public IP address, automated tools discover it by scanning large IP ranges, then probe every port for known vulnerabilities and default credentials. The majority of successful compromises are not the result of sophisticated nation-state attacks; they happen because a server was left with a default password or an unpatched service. Keeping software updated, disabling password authentication on SSH, and enabling a firewall eliminates the vast majority of these threats.

Brute-force credential attacks specifically target authentication endpoints — most commonly SSH on port 22. Attackers run distributed lists of common username and password combinations at high speed. Rate-limiting, key-based authentication, and tools like Fail2ban make brute-force attacks impractical even when a server is publicly accessible.

Exploitation of known vulnerabilities is the next step up in sophistication. Attackers weaponise published CVEs — particularly those with public proof-of-concept exploit code — against servers that are running outdated versions of software. A web application framework, a database connector, or a system library can all be attack surfaces. This threat is countered by prompt patching and by reducing the number of installed packages to only those genuinely needed (minimising attack surface).

Privilege escalation after initial access is the stage that follows a successful initial compromise. An attacker who gains a foothold as an unprivileged process or user attempts to escalate to root. Kernel hardening parameters (ASLR, dmesg restrictions, ptrace scope), mandatory access control frameworks (SELinux, AppArmor), and disciplined SUID/SGID management all significantly raise the cost and complexity of this stage.

Supply chain and insider threats are less common in typical web application deployments but worth understanding. Installing software from untrusted repositories, using Docker base images of unclear provenance, or granting overly broad sudo access to a third-party contractor are all examples. The principle of least privilege and only using packages from verified sources directly mitigates these vectors.

Lateral movement and data exfiltration are the goals once an attacker has elevated privileges. Encrypting data at rest, centralising logs to a remote server (so they cannot be tampered with by a compromised host), and network segmentation all make this phase harder. Even if a single server is fully compromised, proper containment limits what the attacker can access and reduces the blast radius.

Understanding these threats in order of probability and impact helps you invest hardening effort where it matters most. The controls in this guide are ordered to address the highest-probability attacks first, then progressively address more sophisticated scenarios.

Why Server Hardening Is Critical

Security hardening is not an optional extra for mature teams with spare capacity. It is a foundational practice that determines whether your infrastructure is resilient or brittle from day one of deployment. Understanding the specific reasons hardening matters helps you build the internal case for investing time in it — and helps you prioritise which controls to apply when you cannot do everything at once.

1. Minimizing Attack Surfaces

Reducing unnecessary services and ports ensures fewer entry points for attackers.

Every running service is a potential entry point. A default Linux installation often includes dozens of services that are entirely irrelevant to your specific application — print spoolers, Bluetooth daemons, legacy remote access tools like Telnet and rsh, and various system administration services that made sense on multi-user mainframes but serve no purpose on a single-application cloud server. Each of these represents a potential vulnerability, because every service has bugs, and bugs in network-exposed services can be critical. Minimising attack surface by disabling and removing unnecessary services is one of the rare security controls that has zero downside: you are not trading off usability for security, you are simply removing functionality you never needed.

2. Protecting Sensitive Data

Encryption, secure storage, and restricted access protect critical application and user data.

Modern applications almost always process sensitive information: user credentials, payment card details, health records, proprietary business logic, or personally identifiable information. A server compromise that exposes this data creates legal liability under frameworks like GDPR, HIPAA, and PCI DSS, reputational damage that can permanently harm a business, and direct financial loss from fraud and remediation costs. Encryption at rest and in transit, combined with strict access control on who and what can read sensitive files, limits the damage even if a partial compromise occurs.

3. Complying with Security Standards

Server hardening is often a requirement for regulatory compliance frameworks like PCI DSS, GDPR, and HIPAA.

Regulatory frameworks and security certifications generally include explicit requirements for server hardening. PCI DSS Requirement 2 mandates that default passwords be changed, unnecessary services be disabled, and security parameters be configured. The CIS Benchmarks — referenced by ISO 27001, SOC 2, and FedRAMP — provide a detailed, verifiable hardening standard for every major Linux distribution. Even if your organisation is not formally required to comply with a specific regulation today, adopting these standards positions you well for future certification and gives your customers evidence that their data is well-protected.

4. Maintaining Server Integrity

Prevents unauthorized access, malware injections, and other malicious activities that could compromise system operations.

A compromised server is often not immediately obvious. Attackers who gain access frequently install persistent backdoors, join the server to a botnet for spam sending or DDoS amplification, use computational resources for cryptocurrency mining, or quietly exfiltrate data over weeks or months. The cost is not just a one-time incident; it is ongoing damage to your infrastructure, your customers, and your reputation. Intrusion detection, file integrity monitoring, and centralised logging create visibility into these activities so you can detect and respond before the damage compounds.

Steps to Harden a Linux Server

The following nine steps represent the essential baseline for any Linux server that is exposed to a network. Each step is independent and can be applied in sequence without waiting for the others. That said, the order is deliberate — later steps assume earlier ones are in place, and some steps (like enabling the firewall before hardening SSH) are safer when done in order.

Step 1: Keep the System Updated

What to Do:

  • Regularly update the operating system and installed software to patch known vulnerabilities.

Commands:

   sudo apt update && sudo apt upgrade -y   # For Debian/Ubuntu-based systems
sudo yum update -y                       # For RHEL/CentOS-based systems

Automate Updates:

Enable unattended upgrades for critical updates.

   sudo apt install unattended-upgrades
sudo dpkg-reconfigure --priority=low unattended-upgrades

Step 2: Configure a Firewall

What to Do:

  • Use tools like iptables or ufw to control incoming and outgoing traffic.

Example (UFW):

   sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow ssh
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable

Best Practices:

  • Allow only the necessary ports (e.g., 22 for SSH, 80 for HTTP, 443 for HTTPS).
  • Log and monitor dropped packets for analysis.

Step 3: Disable Unused Services

What to Do:

  • Identify and disable unnecessary services to reduce attack surfaces.

Commands:

   sudo systemctl list-unit-files --type=service  # List all services
sudo systemctl disable <service_name>         # Disable an unnecessary service

Example:

Disable unused services like telnet and ftp:

   sudo systemctl stop telnet
sudo systemctl disable telnet

Step 4: Secure SSH Access

4.1 Use Strong Authentication

  • Disable password authentication and use SSH keys.

Edit /etc/ssh/sshd_config:

   PasswordAuthentication no
PermitRootLogin no

Generate SSH Keys:

   ssh-keygen -t rsa -b 4096

4.2 Limit SSH Access by IP

Restrict SSH access to specific IPs.

   sudo ufw allow from <your-ip> to any port 22

4.3 Change the Default Port

Edit /etc/ssh/sshd_config:

   Port 2222

Step 5: Implement Intrusion Detection

Tools to Use:

  1. Fail2ban: Protects against brute-force attacks.
  2. Tripwire: Monitors system integrity.

Install and Configure Fail2ban:

   sudo apt install fail2ban
sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local

Edit /etc/fail2ban/jail.local to enable rules for SSH:

   [sshd]
enabled = true
port = 22
maxretry = 5

Restart the service:

   sudo systemctl restart fail2ban

Step 6: Enable Data Encryption

6.1 Use SSL/TLS

Encrypt communication between the server and clients.

  • Obtain a free SSL certificate from Let’s Encrypt.
   sudo apt install certbot
sudo certbot --nginx -d yourdomain.com

6.2 Encrypt File Systems

Encrypt sensitive directories during installation using tools like LUKS.

6.3 Use Secure File Transfer

Replace FTP with SFTP or SCP for transferring files.

Step 7: Restrict User Privileges

What to Do:

  • Use the principle of least privilege for all users and processes.

Commands:

   sudo usermod -L <username>  # Lock unnecessary accounts
sudo userdel <username>     # Delete unused accounts

Example:

Allow only specific users to use sudo:

   sudo visudo
# Add the line
username ALL=(ALL) ALL

Step 8: Monitor Logs and Alerts

Tools to Use:

  1. Logwatch: Generates daily system summaries.
  2. Syslog-ng: Advanced logging with filtering capabilities.

Install Logwatch:

   sudo apt install logwatch
logwatch --detail high --mailto [email protected] --range today

Step 9: Backup and Disaster Recovery

What to Do:

  • Schedule regular backups and store them securely.

Commands:

   rsync -avz /important/data /backup/location

Use tools like Duplicity or cloud solutions for encrypted backups.

Tools for Server Hardening

The following tools form a standard toolkit for Linux server security. Many of them are complementary rather than competing — a well-hardened server typically uses several of them together.

  1. Lynis

Lynis is an open-source, agent-less security auditing tool that runs directly on the host it is assessing. It performs over 300 checks covering the operating system, file system, installed software, network configuration, and cryptographic settings. After each run it produces a Hardening Index score and a prioritised list of suggestions. Lynis is particularly useful as a recurring baseline check — scheduling it weekly lets you detect configuration drift caused by software updates, new package installations, or unreviewed changes by other team members.

   sudo apt install lynis
sudo lynis audit system
  1. SELinux

SELinux (Security-Enhanced Linux) is a mandatory access control framework developed by the NSA and included in the Linux kernel. It enforces policies that restrict what each process can do, what files it can access, and which network ports it can connect to — regardless of the process’s user identity. This means that even if an attacker successfully exploits a vulnerability in your web server process, SELinux prevents that process from reading /etc/shadow, writing to /tmp, or binding to a privileged port. SELinux is the default MAC framework on all RHEL-family distributions.

  1. AppArmor

AppArmor accomplishes a similar goal to SELinux but uses a path-based profile model instead of labels. Each profile is a text file that enumerates exactly which files and capabilities a specific program may use. AppArmor ships by default on Ubuntu and Debian. Its profiles are generally considered easier to write and maintain than SELinux policies, making it a practical choice for teams that need MAC protection without the steep learning curve of SELinux. Ubuntu ships with AppArmor profiles for common services (nginx, mysql, cups, and dozens more) enabled out of the box.

  1. Fail2ban

Fail2ban watches log files for patterns that indicate brute-force or denial-of-service activity and dynamically inserts firewall rules to block the offending IP address for a configurable period. It supports not just SSH but also HTTP basic auth, Postfix, Dovecot, and many other services through a plugin system. A well-configured Fail2ban installation effectively neutralises automated credential-stuffing attacks without requiring any interaction from the administrator.

  1. rkhunter and chkrootkit

Once an attacker gains root access, they may install a rootkit — a collection of tools that hides the attacker’s presence by modifying or replacing system binaries, hiding processes and network connections, and establishing persistent backdoors. rkhunter and chkrootkit scan the system for known rootkit signatures, suspicious file modifications, and dangerous kernel module loads. Run them on a schedule and compare output across runs to detect post-compromise activity.

Real-World Use Cases

Server hardening is not theoretical. The steps in this guide map directly to concrete incidents and outcomes. The two scenarios below illustrate how specific controls prevent real damage in real deployments.

Use Case 1: E-Commerce Platform

Problem:

Frequent brute-force login attempts on the server.

Solution:

  • Implemented Fail2ban to block malicious IPs.
  • Enforced SSH key-based authentication.

Use Case 2: Healthcare Application

Problem:

Unencrypted data transmissions led to compliance issues.

Solution:

  • Enabled SSL/TLS for all communications.
  • Encrypted sensitive directories using LUKS.

Server security is not static. As infrastructure patterns evolve, so do the hardening techniques that protect them. Understanding where the field is heading allows you to invest in controls that will remain relevant rather than those that will soon be superseded.

  1. Zero-Trust Architecture
  • Continuous verification of every user and device, regardless of network location.
  1. AI-Driven Security Tools
  • Predict and prevent attacks using machine learning.
  1. Serverless Security
  • Hardening measures for serverless environments will become standard.

File System Hardening and Mount Point Security

The Linux file system is more than just a hierarchy of files and directories. It is a policy surface. Mount options, file permissions, and the rights attached to individual binaries all determine how far an attacker can move after gaining access to a process running on your server.

Securing /tmp and /run/shm

The /tmp directory is world-writable and executable by default. Attackers routinely drop exploit payloads into /tmp, compile them there, and execute them. Mounting /tmp as a separate filesystem with the noexec, nosuid, and nodev options removes this entire class of attack:

   # Add to /etc/fstab
tmpfs   /tmp        tmpfs   defaults,noexec,nosuid,nodev,size=512M   0 0
tmpfs   /run/shm    tmpfs   defaults,noexec,nosuid,nodev             0 0

After editing /etc/fstab, apply without rebooting:

   sudo mount -o remount /tmp
sudo mount -o remount /run/shm

Verify the options are active:

   mount | grep /tmp
# Should include: noexec, nosuid, nodev

The size=512M limit also prevents a denial-of-service attack where a process fills /tmp to exhaust disk space. Adjust the size for your workload — applications that write large temporary files (e.g., video processing) will need more space.

Protecting /boot and /proc

The /boot directory contains the kernel and bootloader configuration. There is no reason for any user process to write there after the system has started:

   # Make /boot read-only in /etc/fstab
# (Change the existing /boot entry, or add one if /boot is part of the root filesystem)
UUID=<your-boot-uuid>   /boot   ext4   defaults,ro,nosuid,nodev   0 2

Similarly, /proc exposes kernel internal state that can leak information about running processes. Remount it with hidepid=2 to prevent users from seeing other users’ processes:

   proc   /proc   proc   defaults,hidepid=2   0 0

Note: hidepid=2 can interfere with some monitoring agents and system administration tools that inspect /proc for all processes. Test this in a non-production environment first.

Setting a Restrictive Default umask

The umask is a bitmask that sets the default permissions for newly created files and directories. The standard default of 022 creates files readable by everyone. For a server handling sensitive application data, a more restrictive 027 is more appropriate:

   # Edit /etc/profile or /etc/login.defs to set system-wide umask
echo "umask 027" | sudo tee -a /etc/profile.d/hardening.sh

# Verify the active umask in a new shell
umask    # Should show 0027

With umask 027, new files are 640 (owner read/write, group read only, no access for others) and new directories are 750. This means application configuration files and log files will not be readable by other unprivileged users on the system.

Finding and Reviewing World-Writable Directories

World-writable directories (beyond the standard /tmp and /var/tmp) can be used to plant malicious files that are later executed by privileged processes:

   # Find world-writable directories (excluding /proc and /sys virtual filesystems)
sudo find / -xdev -type d -perm -0002 2>/dev/null | grep -v '/proc\|/sys'

# Fix any unexpected results
sudo chmod o-w /some/unexpected/directory

Integrate this check into your weekly Lynis run — it will flag world-writable directories as a finding automatically.

Immutable Files for Critical Configuration

The chattr command can mark files as immutable (+i), preventing modification even by root (unless the attribute is explicitly removed first). This is a useful backstop for critical files that should never change in production:

   # Make the sudoers file immutable
sudo chattr +i /etc/sudoers

# Make SSH daemon configuration immutable
sudo chattr +i /etc/ssh/sshd_config

# Remove the immutable attribute when you need to make a legitimate change
sudo chattr -i /etc/ssh/sshd_config
# ... make your change ...
sudo chattr +i /etc/ssh/sshd_config

# List attributes on a file
lsattr /etc/ssh/sshd_config    # Should show: ----i---------

Use this sparingly — forgetting about immutable attributes is itself a source of operational confusion. Document every file you mark immutable in your runbook.


Kernel-Level Audit Logging with auditd

The Linux Audit subsystem (auditd) provides detailed, tamper-evident logging of security-relevant events directly at the kernel level. Unlike application-level logs that a compromised process might be able to manipulate, kernel audit records are written before user space has a chance to interfere.

auditd can log events such as:

  • File reads, writes, and attribute changes on specific paths
  • Use of privileged system calls (execve, open, connect, etc.)
  • Changes to user identities (setuid, su, sudo usage)
  • Authentication events (successful and failed logins)
  • Modification of the audit configuration itself

Installing and Enabling auditd

   sudo apt install auditd audispd-plugins    # Debian/Ubuntu
sudo yum install audit audit-libs          # RHEL/CentOS

sudo systemctl enable --now auditd

Configuring Audit Rules

Audit rules are defined in /etc/audit/rules.d/audit.rules. The following is a practical starting set for a web application server:

   # Delete all existing rules (start clean)
-D

# Set the buffer size for high-traffic systems
-b 8192

# -- Identity and privilege events --
-w /etc/passwd   -p wa -k identity
-w /etc/shadow   -p wa -k identity
-w /etc/sudoers  -p wa -k sudoers

# -- Authentication events --
-w /var/log/faillog  -p wa -k auth
-w /var/log/lastlog  -p wa -k auth

# -- Directory watches --
-w /etc/ssh/sshd_config -p wa -k sshd
-w /etc/cron.d          -p wa -k cron
-w /etc/cron.daily      -p wa -k cron

# -- Privileged command execution --
-a always,exit -F arch=b64 -S execve -F euid=0 -k root_commands

# -- File deletions by non-root users --
-a always,exit -F arch=b64 -S unlink -S rename -F auid>=1000 -F auid!=4294967295 -k file_deletions

# Make the rules immutable until next reboot (requires reboot to change rules)
-e 2

After editing, reload:

   sudo augenrules --load
sudo systemctl restart auditd

Querying Audit Logs

   # Show all events related to the "identity" key
sudo ausearch -k identity

# Show events from the last hour
sudo ausearch -ts recent

# Generate a summary report of events by type
sudo aureport --summary

# Show all authentication events
sudo aureport --auth

Forwarding Audit Logs Securely

On its own, a server’s audit log can be deleted by an attacker who gains root. Forward logs to a remote system immediately after they are written:

   # Edit /etc/audisp/plugins.d/syslog.conf
active = yes
direction = out
path = builtin_syslog
type = builtin
args = LOG_INFO LOG_USER
format = string

Combine this with the remote rsyslog forwarding shown earlier to ensure audit records leave the host before an attacker can tamper with them.


Patch Management and Vulnerability Scanning

Keeping software updated is the single highest return-on-investment security activity. The majority of successful exploits use vulnerabilities for which patches have been available, sometimes for months or years, before the attack occurs. Organisations that apply patches promptly are rarely compromised through known vulnerabilities.

Automated Security Updates in Practice

Unattended upgrades handle the routine case: security patches from the distribution’s official repositories are reviewed by the distribution maintainers and can generally be applied without manual testing. Configure them to apply only security updates, not all updates, to minimise the risk of an automatic upgrade breaking application compatibility:

   sudo nano /etc/apt/apt.conf.d/50unattended-upgrades

The key directives to verify:

   Unattended-Upgrade::Allowed-Origins {
    "${distro_id}:${distro_codename}-security";
};
Unattended-Upgrade::AutoFixInterruptedDpkg "true";
Unattended-Upgrade::Remove-Unused-Kernel-Packages "true";
Unattended-Upgrade::Reboot "false";           // Set true if automatic reboots are acceptable
Unattended-Upgrade::Mail "[email protected]";

Leaving Reboot as false means kernel and glibc updates will not take effect until the next planned maintenance window — a reasonable trade-off for most production servers. Schedule maintenance windows at least monthly to apply these deferred updates.

Checking for Vulnerabilities with debsecan and yum-security

   # Debian/Ubuntu: list packages with known CVEs
sudo apt install debsecan
debsecan --suite $(lsb_release -cs) --format detail

# RHEL/CentOS: show available security updates with CVE identifiers
sudo yum updateinfo list security
sudo yum update --security    # Apply only security updates

Tracking Installed Package History

Knowing exactly what changed and when is invaluable during an incident. Both major packaging systems maintain a log:

   # Debian/Ubuntu: recent apt history
grep "Install\|Upgrade\|Remove" /var/log/apt/history.log | tail -50

# RHEL/CentOS: yum/dnf transaction history
sudo dnf history list
sudo dnf history info <id>    # Inspect a specific transaction

Removing Unnecessary Packages

Every installed package is a potential attack surface. Audit installed packages after initial setup and again quarterly:

   # List all installed packages, sorted by size (largest first)
dpkg-query -W -f='${Installed-Size}\t${Package}\n' | sort -n | tail -30

# Remove a package and its configuration files
sudo apt purge telnet rsh-client ftp

# For RHEL/CentOS
sudo yum remove telnet rsh

Pay particular attention to packages that open network ports (check with ss -tlnp before and after installation) and to compilers and development tools (gcc, make, python3-dev) that have no place on a production application server.


SSH Hardening Walkthrough

The brief SSH changes in Step 4 are the minimum. A production server deserves a fully hardened sshd_config. The changes below work for OpenSSH 8.x and later (shipping on Ubuntu 22.04, Debian 12, RHEL 9, and Rocky Linux 9).

Generating ED25519 Keys

ED25519 is the modern recommended key type. It produces shorter keys than RSA while providing equivalent security, and is less susceptible to implementation bugs:

   # On your CLIENT machine — generate a key pair with a strong passphrase
ssh-keygen -t ed25519 -C "[email protected]" -f ~/.ssh/id_ed25519

# Copy the public key to the server (installs into ~/.ssh/authorized_keys automatically)
ssh-copy-id -i ~/.ssh/id_ed25519.pub adminuser@your-server-ip

# Or append manually if ssh-copy-id is unavailable
cat ~/.ssh/id_ed25519.pub | ssh adminuser@your-server-ip \
  "mkdir -p ~/.ssh && chmod 700 ~/.ssh && \
   cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys"

A Production-Ready sshd_config

Open /etc/ssh/sshd_config and apply (or add) the following settings. Always validate before reloading:

   # --- Protocol and port ---
Port 2222                          # Non-default port reduces automated scan noise
Protocol 2                         # SSHv1 is broken; never use it

# --- Authentication ---
PermitRootLogin no                 # Always deny direct root login
PasswordAuthentication no          # Keys only — no passwords
ChallengeResponseAuthentication no
UsePAM yes
PubkeyAuthentication yes

# --- Access control ---
AllowUsers adminuser devuser       # Whitelist individual users
# AllowGroups sshusers             # Or use a group instead

# --- Session hardening ---
ClientAliveInterval 300            # Send keepalive every 5 minutes
ClientAliveCountMax 2              # Disconnect after 2 missed keepalives
LoginGraceTime 30                  # 30 seconds to complete authentication
MaxAuthTries 3                     # Lock channel after 3 failed attempts
MaxSessions 4                      # Limit concurrent multiplexed sessions

# --- Cryptographic hardening ---
HostKeyAlgorithms ecdsa-sha2-nistp256,ssh-ed25519
KexAlgorithms curve25519-sha256,diffie-hellman-group14-sha256
Ciphers [email protected],[email protected]
MACs [email protected],[email protected]

# --- Disable unused features ---
X11Forwarding no
AllowTcpForwarding no              # Disable unless you explicitly need tunnels
PrintMotd no
AcceptEnv LANG LC_*
Banner /etc/issue.net              # Show a legal warning before login

Validate the configuration, then reload — never restart, in case of errors:

   sudo sshd -t && echo "Config OK" || echo "CONFIG HAS ERRORS — do not reload"
sudo systemctl reload sshd

A login banner satisfies compliance requirements (PCI DSS, SOC 2) and can deter casual intruders:

   sudo tee /etc/issue.net << 'EOF'
***************************************************************************
NOTICE: This system is for authorised use only. All activities may be
monitored and recorded. Unauthorised access or use is prohibited and
may be subject to legal action.
***************************************************************************
EOF

Testing SSH Security

From a second machine, confirm that password login and root login are truly blocked:

   # Should fail with "Permission denied (publickey)"
ssh -o PasswordAuthentication=yes -o PubkeyAuthentication=no adminuser@server-ip

# Root login should also be denied
ssh root@server-ip

Always test from a separate, open session before closing your current connection. Losing SSH access to a remote server is frustrating at best and catastrophic at worst.


Firewall Configuration In Depth

The basics in Step 2 cover the most common scenario. This section explores more complete setups and the tool landscape.

UFW: Full Application Server Setup

   # Start from a clean state
sudo ufw reset

# Conservative defaults
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw default deny forward

# Allow SSH FIRST — before enabling the firewall
sudo ufw allow 2222/tcp comment "SSH non-default port"

# Web traffic
sudo ufw allow 80/tcp  comment "HTTP"
sudo ufw allow 443/tcp comment "HTTPS"

# Rate-limit SSH to blunt brute-force attempts (6 connections per 30 seconds)
sudo ufw limit 2222/tcp

# Allow a database only from an internal CIDR — never expose to the internet
sudo ufw allow from 10.0.0.0/8 to any port 5432 comment "PostgreSQL internal"

# Enable and verify
sudo ufw enable
sudo ufw status verbose

nftables: Modern iptables Replacement

nftables is the successor to iptables, the default firewall backend on Debian 10+, RHEL 8+, and Ubuntu 20.10+. Its syntax is simpler and handles both IPv4 and IPv6 in a single ruleset:

   sudo apt install nftables    # Debian/Ubuntu
sudo systemctl enable --now nftables

Create /etc/nftables.conf:

   #!/usr/sbin/nft -f
flush ruleset

table inet filter {
    chain input {
        type filter hook input priority 0; policy drop;

        # Always accept loopback
        iif lo accept

        # Accept established and related connections
        ct state established,related accept

        # Drop invalid packets immediately
        ct state invalid drop

        # Accept ICMP so ping and path MTU work
        ip  protocol icmp  accept
        ip6 nexthdr  icmpv6 accept

        # SSH, HTTP, HTTPS
        tcp dport { 2222, 80, 443 } accept

        # Log and drop everything else
        log prefix "nft-drop: " limit rate 5/minute
    }

    chain forward {
        type filter hook forward priority 0; policy drop;
    }

    chain output {
        type filter hook output priority 0; policy accept;
    }
}

Apply it:

   sudo nft -f /etc/nftables.conf
sudo nft list ruleset    # Verify

Firewall Tool Comparison

ToolDistroSyntaxIPv4+IPv6Notes
ufwDebian/UbuntuSimpleYes (with config)Best for straightforward setups
firewalldRHEL/FedoraZone-basedYesDynamic rules, no reload needed
iptablesUniversalComplexVia ip6tablesBattle-tested, wide support
nftablesModern kernelsConsistentYes (single ruleset)Future standard, replaces iptables

Kernel Hardening with sysctl

The Linux kernel exposes hundreds of tunable parameters through the /proc/sys filesystem. The sysctl command reads and writes them at runtime; persisting them across reboots requires a file in /etc/sysctl.d/.

Default kernel settings favour compatibility over security. For a production server that is the wrong trade-off. The parameters described below address six distinct classes of attack that affect network-connected Linux servers.

TCP SYN floods are a denial-of-service technique where an attacker sends a large volume of TCP connection requests with spoofed source addresses. The server allocates memory for each half-open connection and eventually exhausts its connection table, making it unable to accept legitimate connections. Enabling tcp_syncookies makes the kernel send a cryptographic cookie in its response rather than allocating state immediately, so the connection table is only populated when a valid acknowledgement arrives.

IP spoofing attacks involve crafting packets with a false source IP address to make traffic appear to come from a trusted host or to obscure the attacker’s location. The Reverse Path Filter (rp_filter) instructs the kernel to check that packets arriving on a given interface could actually have originated from the claimed source address. Packets that fail this check are dropped silently.

ICMP redirect attacks can be used to manipulate a server’s routing table, redirecting its traffic through an attacker-controlled router for interception or modification. Disabling accept_redirects and send_redirects prevents both receiving and forwarding these messages.

Address Space Layout Randomisation (ASLR) is one of the most important mitigations for memory-corruption exploits. When enabled with randomize_va_space = 2, the kernel places the stack, heap, and dynamically linked libraries at random addresses every time a process starts. This forces attackers to guess memory addresses rather than using hardcoded offsets, substantially increasing the difficulty of remote code execution.

Kernel pointer leaks in /proc files reveal the memory addresses of kernel data structures, which attackers can use to bypass ASLR and craft reliable kernel exploits. Setting kptr_restrict = 2 replaces these addresses with zeroes for all users.

ptrace-based privilege escalation allows one process to inspect and modify another process’s memory. Setting yama.ptrace_scope = 1 limits ptrace to parent-child relationships, preventing a compromised unprivileged process from attaching to and stealing credentials from other processes it did not spawn.

Creating a Hardened sysctl Profile

Create a dedicated drop-in file to keep your changes clearly separated from the distribution defaults:

   sudo nano /etc/sysctl.d/99-hardening.conf

Add the following content:

   # ============================================================
# Network hardening
# ============================================================

# Protect against SYN flood attacks
net.ipv4.tcp_syncookies = 1

# Disable IP source routing (used in spoofing attacks)
net.ipv4.conf.all.accept_source_route = 0
net.ipv4.conf.default.accept_source_route = 0
net.ipv6.conf.all.accept_source_route = 0

# Reverse-path filter: drop packets that arrive on the wrong interface (anti-spoofing)
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.default.rp_filter = 1

# Ignore ICMP redirects (used in man-in-the-middle attacks)
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.secure_redirects = 0

# Do not send ICMP redirects
net.ipv4.conf.all.send_redirects = 0

# Ignore broadcast pings (protects against Smurf amplification attacks)
net.ipv4.icmp_echo_ignore_broadcasts = 1

# Log packets with impossible source addresses (martians)
net.ipv4.conf.all.log_martians = 1

# ============================================================
# Memory and process hardening
# ============================================================

# Restrict kernel log to root only
kernel.dmesg_restrict = 1

# Hide kernel symbol addresses in /proc (defeats some exploit techniques)
kernel.kptr_restrict = 2

# Randomise address-space layout — makes remote exploits much harder
kernel.randomize_va_space = 2

# Disable core dumps for setuid programs (prevents leaking sensitive memory)
fs.suid_dumpable = 0

# Restrict ptrace to parent-child relationships only (limits live debugging attacks)
kernel.yama.ptrace_scope = 1

Apply immediately without rebooting:

   sudo sysctl --system

Verifying the Profile

A quick validation script you can run after applying:

   #!/bin/bash
PARAMS=(
  "net.ipv4.tcp_syncookies=1"
  "net.ipv4.conf.all.rp_filter=1"
  "kernel.randomize_va_space=2"
  "kernel.dmesg_restrict=1"
  "fs.suid_dumpable=0"
  "kernel.kptr_restrict=2"
)

PASS=0; FAIL=0
for param in "${PARAMS[@]}"; do
  key="${param%%=*}"
  expected="${param##*=}"
  actual=$(sysctl -n "$key" 2>/dev/null)
  if [ "$actual" = "$expected" ]; then
    echo "PASS  $key = $actual"
    ((PASS++))
  else
    echo "FAIL  $key: expected $expected, got '${actual:-not found}'"
    ((FAIL++))
  fi
done
echo "---"
echo "Results: $PASS passed, $FAIL failed"

Save this as verify-sysctl.sh, make it executable (chmod +x), and run it after every configuration change.


SELinux and AppArmor — Mandatory Access Control

Standard Unix permissions (discretionary access control, DAC) let the owner of a file share it with anyone. Mandatory Access Control (MAC) enforces centrally-defined policies that override ownership, severely limiting what a compromised process can do — even after a successful exploit.

Linux provides two mature MAC frameworks: SELinux on RHEL-family distributions and AppArmor on Debian/Ubuntu.

The most important thing to understand about MAC frameworks is what they protect against that DAC does not. Suppose an attacker exploits a remote code execution vulnerability in your web application. With only DAC in place, that attacker now has everything the web server process has: access to all files readable by the www-data user, the ability to open outbound network connections, and the ability to create or modify files in directories writable by www-data. In many configurations this is enough to steal database credentials, escalate privileges, or establish a persistent backdoor.

With SELinux or AppArmor enforcing a profile for the web server process, the same exploit produces a far more limited breach. The policy may allow the web server process to read only the specific document root directories, to connect only to the database port on a specific internal IP, and to write only to the configured upload directory. Every other action — reading /etc/shadow, spawning a shell, connecting to external servers — is denied and logged, even if the process is running as root within its container. This containment is often the difference between a contained incident and a complete server compromise.

   graph LR
    subgraph DAC["Standard Unix DAC"]
        P1[Process] -->|owner decides| F1[File]
    end
    subgraph MAC["Mandatory Access Control"]
        P2[Process] -->|policy check| K[Kernel Policy Engine]
        K -->|allow / deny| F2[File]
    end
    style MAC fill:#e8f5e9
    style DAC fill:#fff3e0

SELinux on RHEL / CentOS / Rocky Linux / AlmaLinux

SELinux was developed by the US National Security Agency and uses a label-based model. Every file, process, and socket carries a security context (user:role:type:level). Policies define which contexts can interact.

Check current status:

   sestatus
# SELinux status:                 enabled
# SELinuxfs mount:                /sys/fs/selinux
# Current mode:                   enforcing

SELinux Operating Modes

ModeBehaviour
EnforcingPolicy violations are blocked and logged. Use this in production.
PermissiveViolations are logged only — nothing is blocked. Use for troubleshooting.
DisabledSELinux is inactive. Never use on a production server.
   # Switch temporarily to permissive for debugging (reverts on reboot)
sudo setenforce 0

# Return to enforcing
sudo setenforce 1

Edit /etc/selinux/config to persist the mode across reboots:

   SELINUX=enforcing
SELINUXTYPE=targeted

Diagnosing and Fixing Denials

When a legitimate service stops working after enabling SELinux, diagnose the denial rather than disabling SELinux:

   # Show recent Access Vector Cache (AVC) denials in readable form
sudo ausearch -m avc -ts recent | audit2why

# Generate a targeted policy module to allow only the denied action
sudo ausearch -m avc -ts recent | audit2allow -M myservice
sudo semodule -i myservice.pp

Restoring File Contexts

Moving or copying files can strip SELinux labels. Restore them before starting a service:

   # Restore default context for the web root
sudo restorecon -Rv /var/www/html

# Inspect the context of a specific file
ls -Z /etc/ssh/sshd_config

AppArmor on Debian / Ubuntu

AppArmor uses path-based profiles that list exactly which files and capabilities a program may access. Profiles are human-readable text files, making them easier to audit than SELinux policies.

   # Check status — lists loaded profiles and their modes
sudo apparmor_status

AppArmor Profile Modes

ModeBehaviour
enforceViolations are blocked and logged. Use in production.
complainViolations are logged only. Use when writing new profiles.
   # Install community-contributed profiles
sudo apt install apparmor-profiles apparmor-utils

# Enable enforce mode for nginx
sudo aa-enforce /etc/apparmor.d/usr.sbin.nginx

# Switch a profile to complain mode for debugging
sudo aa-complain /etc/apparmor.d/usr.sbin.nginx

# Reload all profiles after any change
sudo systemctl reload apparmor

Generating a Profile for a Custom Application

   # Start profile generation — run your application while aa-genprof watches
sudo aa-genprof /usr/local/bin/myapp
# Follow the interactive prompts to allow/deny each access

Choosing Between SELinux and AppArmor

FeatureSELinuxAppArmor
Primary distrosRHEL, Fedora, Rocky, AlmaUbuntu, Debian, SUSE
Policy modelLabel-based (context on every object)Path-based (per-binary profiles)
Learning curveSteepModerate
Default coveragetargeted policy covers most daemonsProfiles available for common services
Audit toolaudit2why, audit2allowaa-genprof, aa-logprof

Advanced User Management and Privilege Hardening

The principle of least privilege is one of the highest-impact controls available to a system administrator. Every account and process should have exactly the permissions it needs — nothing more.

Creating a Dedicated Service Account

Never run your application as root. Create a dedicated, unprivileged account:

   # -r creates a system account (no home directory, no aging)
# -s /usr/sbin/nologin prevents interactive shell login
sudo useradd -r -s /usr/sbin/nologin myapp

# Run your application process as this user
# In a systemd unit file:
# [Service]
# User=myapp
# Group=myapp

Locking Down sudo Access

After creating an admin user, restrict which commands each person may run using visudo:

   sudo visudo -f /etc/sudoers.d/webdev
   # Grant webdev only the ability to manage nginx — nothing else
webdev ALL=(ALL) NOPASSWD: /bin/systemctl restart nginx, \
                            /bin/systemctl reload nginx, \
                            /bin/systemctl status nginx

Audit who has sudo access regularly:

   grep -E '^sudo|^wheel' /etc/group
getent group sudo wheel

Enforcing Password Policies via PAM

Install libpam-pwquality to enforce minimum password complexity:

   sudo apt install libpam-pwquality   # Debian/Ubuntu
sudo yum install pam_pwquality      # RHEL/CentOS

Edit /etc/security/pwquality.conf:

   minlen   = 14        # Minimum 14 characters
dcredit  = -1        # At least 1 digit
ucredit  = -1        # At least 1 uppercase letter
lcredit  = -1        # At least 1 lowercase letter
ocredit  = -1        # At least 1 special character
maxrepeat = 3        # No more than 3 consecutive identical characters

Set account aging per user with chage:

   # Maximum 90 days between password changes, 14-day warning, lock 30 days after expiry
sudo chage -M 90 -W 14 -I 30 adminuser

# View an account's aging configuration
sudo chage -l adminuser

Locking and Auditing Accounts

   # Lock an account without deleting it
sudo passwd -l inactiveuser

# Unlock it later
sudo passwd -u inactiveuser

# List accounts with no password-expiry policy set
sudo awk -F: '$5 == "" {print $1}' /etc/shadow

# Verify that system accounts cannot log in interactively
awk -F: '$3 < 1000 && $7 != "/usr/sbin/nologin" && $7 != "/bin/false" {print $1, $7}' /etc/passwd

Auditing SUID and SGID Binaries

SUID/SGID binaries execute with the file owner’s (often root’s) privileges regardless of who runs them. Any attacker who finds a vulnerability in one of these binaries gains elevated access:

   # Find all SUID binaries on all mounted filesystems
sudo find / -perm -4000 -type f 2>/dev/null | sort

# Find all SGID binaries
sudo find / -perm -2000 -type f 2>/dev/null | sort

# Find world-writable files (a significant risk)
sudo find / -perm -0002 -type f 2>/dev/null | grep -v /proc

# Remove SUID from a binary that does not need it
sudo chmod u-s /usr/bin/somebinary

Review the SUID list when new software is installed, and again quarterly. Compare against a known-good baseline stored in version control.


Security Auditing with Lynis and OpenSCAP

Applying controls without verifying them gives a false sense of security. Integrate automated auditing from day one.

Lynis — Open-Source Security Auditing

Lynis scans the local system against a library of best-practice checks and produces a Hardening Index score out of 100. It requires no agent and can run from the system itself:

   sudo apt install lynis    # Debian/Ubuntu
sudo yum install lynis    # RHEL/CentOS

# Run a full audit — output goes to terminal and /var/log/lynis.log
sudo lynis audit system

# Save a dated report for trending
sudo lynis audit system --report-file /var/log/lynis-$(date +%F).txt 2>&1

Each finding is categorised as a Warning (remediate immediately) or a Suggestion (consider for your environment). Aim for a Hardening Index of 80+ on a freshly deployed server.

OpenSCAP — Compliance Scanning Against CIS Benchmarks

For regulated environments, OpenSCAP evaluates a system directly against published benchmarks such as the CIS Ubuntu Linux Benchmark:

   sudo apt install openscap-scanner scap-security-guide

# View available profiles
ls /usr/share/xml/scap/ssg/content/ssg-ubuntu*-ds.xml

# Run a CIS Level 1 scan and generate an HTML report
sudo oscap xccdf eval \
  --profile xccdf_org.ssgproject.content_profile_cis_level1_server \
  --results /tmp/scap-results.xml \
  --report  /tmp/scap-report.html \
  /usr/share/xml/scap/ssg/content/ssg-ubuntu2204-ds.xml

Open /tmp/scap-report.html in a browser to review pass/fail findings tied to specific CIS controls.


Testing and Validating Hardened Configurations

Every hardening step needs a corresponding verification step. Build tests into your deployment runbook so that future changes do not silently undo your work.

Security configurations drift over time: packages update and overwrite modified files, colleagues make changes without full context, and edge cases in provisioning scripts silently skip hardening steps. The only reliable way to confirm that your security controls are in place is to test them systematically, ideally from outside the system being tested and ideally as an automated step in your deployment pipeline.

Think of validation as a test suite for your infrastructure configuration. Just as a software test suite catches regressions when code changes, a security validation suite catches configuration regressions whenever the server changes. Running it before and after every change event — package update, new application, configuration modification — provides immediate feedback if something has inadvertently weakened the security posture, and gives you a dated record of compliance for auditors.

The tests below cover four key dimensions: network accessibility (what can outside computers reach), authentication controls (what login methods actually work), kernel parameter values (are the sysctl settings applied), and overall compliance scoring. Together these provide a complete and trackable view of the server’s security posture.

Firewall Validation

From a separate machine (or a CI runner in a different network segment), scan for unintended open ports:

   # Full TCP port scan — should only see ports you deliberately opened
nmap -sT -p 1-65535 --open your-server-ip

# Quick connectivity tests for specific ports
nc -zvw3 your-server-ip 22  2>&1
nc -zvw3 your-server-ip 80  2>&1
nc -zvw3 your-server-ip 443 2>&1

# Always test IPv6 separately
nmap -6 -sT -p 1-65535 --open your-server-ipv6

SSH Authentication Tests

   # Confirm password authentication is rejected
ssh -o PasswordAuthentication=yes \
    -o PubkeyAuthentication=no \
    adminuser@your-server-ip
# Expected output: "Permission denied (publickey)."

# Confirm root login is rejected
ssh root@your-server-ip
# Expected: "Permission denied" or connection refused

Testing Fail2ban

   # Check jail status
sudo fail2ban-client status sshd

# View currently banned IPs
sudo fail2ban-client status sshd | grep "Banned IP"

# Manually ban a test IP and immediately un-ban it to confirm the mechanism works
sudo fail2ban-client set sshd banip 198.51.100.250
sudo fail2ban-client status sshd | grep "Banned IP"
sudo fail2ban-client set sshd unbanip 198.51.100.250

Verify sysctl Parameters

Run the validation script introduced in the Kernel Hardening section, or query individual parameters:

   # These should all return the expected hardened values
sysctl net.ipv4.tcp_syncookies          # Expect: 1
sysctl net.ipv4.conf.all.rp_filter     # Expect: 1
sysctl kernel.randomize_va_space       # Expect: 2
sysctl kernel.dmesg_restrict           # Expect: 1
sysctl fs.suid_dumpable                # Expect: 0

Infrastructure-as-Code Tests with Inspec

Teams that manage servers through code can write executable compliance specs:

   # test/ssh_spec.rb
describe sshd_config do
  its('PermitRootLogin')              { should eq 'no'  }
  its('PasswordAuthentication')       { should eq 'no'  }
  its('X11Forwarding')                { should eq 'no'  }
  its('MaxAuthTries')                 { should eq '3'   }
  its('AllowTcpForwarding')           { should eq 'no'  }
end

describe port(22) do
  it { should be_listening }
end

describe kernel_parameter('kernel.randomize_va_space') do
  its('value') { should eq 2 }
end

Run with inspec exec test/ssh_spec.rb. Integrate this into your CI pipeline so any configuration drift is caught before it reaches production.


Common Mistakes and Anti-Patterns

Even experienced engineers make predictable errors when hardening servers. These anti-patterns are dangerous precisely because they feel correct in the moment — they appear to solve a problem, but they introduce a larger one or create a false sense of security. Knowing them in advance is the best way to avoid them.

Many of these mistakes share a common root cause: time pressure. Security hardening is rarely prioritised in initial development sprints, so it is often done quickly, under pressure, right before a launch deadline. That context produces shortcuts like broad sudo rules (“I’ll tighten it up later”), disabled SELinux (“it was blocking the service, this is quicker”), and skip-the-second-session firewall changes (“I’ve done this a hundred times”). Building a hardening checklist and process that your team follows consistently removes the need to think under pressure about steps that should be automatic.

The mistakes below are ordered roughly from most to least common based on real-world incident patterns.

Mistake 1: Locking Yourself Out of SSH

The most common hardening disaster is enabling the firewall or restarting sshd before verifying the new configuration from a second session.

Anti-pattern:

   # Enabling UFW without having opened the SSH port first — locks you out
sudo ufw enable

Correct approach: Always open a second SSH connection before making any SSH or firewall changes. If the new rules are correct, the second session stays alive; if they are broken, you use that session to roll back.

   # Step 1: Allow your SSH port
sudo ufw allow 2222/tcp

# Step 2: In a SECOND terminal, verify you can still connect on port 2222
# Only then enable the firewall in the first terminal
sudo ufw enable

Mistake 2: Disabling SELinux Instead of Fixing the Denial

When a service breaks after enabling SELinux, the tempting quick fix is:

   # WRONG — disabling SELinux removes a critical security layer
sudo setenforce 0
echo "SELINUX=disabled" >> /etc/selinux/config

Always diagnose the specific denial and create a targeted policy module instead:

   sudo ausearch -m avc -ts recent | audit2why
sudo ausearch -m avc -ts recent | audit2allow -M myfix
sudo semodule -i myfix.pp

Mistake 3: Overly Broad sudo Rules

   # WRONG — gives unrestricted root equivalent access
webdev ALL=(ALL) NOPASSWD: ALL

Enumerate only the specific commands actually needed:

   # CORRECT
webdev ALL=(ALL) NOPASSWD: /bin/systemctl restart nginx

Mistake 4: Using Weak or Legacy SSH Key Types

RSA keys shorter than 3072 bits and all DSA keys are considered weak by modern cryptographic standards:

   # WRONG
ssh-keygen -t dsa
ssh-keygen -t rsa -b 1024

# CORRECT — ED25519 is the current recommendation
ssh-keygen -t ed25519 -C "user@host"
# Or RSA 4096 when ED25519 is not supported by an old server
ssh-keygen -t rsa -b 4096

Mistake 5: Hardening Only IPv4 and Forgetting IPv6

If your server has a public IPv6 address and you configure firewall rules only for IPv4, an attacker reachable over IPv6 bypasses all your rules entirely.

   # Check whether the server has a public IPv6 address
ip -6 addr show scope global

# Verify UFW's IPv6 setting is enabled
grep ^IPV6 /etc/default/ufw    # Should be: IPV6=yes

# Scan your own IPv6 address from an external host to confirm filtering
nmap -6 -sT -p 1-65535 your-server-ipv6-address

Mistake 6: Not Rotating or Monitoring Logs

Hardening reduces the attack surface but does not make your server invulnerable. Log files are your early-warning system — if you are not reading them, you will not notice a breach until it is too late.

   # Forward logs to a remote syslog server (log tampering becomes much harder)
echo "*.* @192.168.1.100:514" | sudo tee /etc/rsyslog.d/50-remote.conf
sudo systemctl restart rsyslog

# Check for failed authentication attempts in real time
sudo journalctl -u sshd -f

# Review the last 50 failed login attempts
sudo lastb | head -50

Mistake 7: Treating Hardening as a One-Time Task

Security configuration drifts over time as software is installed, updated, and configured by different people. Automate recurring checks:

   # Add to root's crontab: weekly Lynis audit, daily security updates
sudo crontab -e
   # Run Lynis every Sunday at 03:00
0 3 * * 0 /usr/sbin/lynis audit system --quiet

# Daily unattended security updates at 02:00
0 2 * * * /usr/bin/unattended-upgrade -d

Hardening Checklist Summary

Use this table as a quick reference before signing off on a server deployment:

CategoryControlVerify With
UpdatesUnattended upgrades enabledsystemctl status unattended-upgrades
FirewallDefault deny on all inboundufw status verbose or nft list ruleset
SSHRoot login disabled, keys onlygrep PermitRootLogin /etc/ssh/sshd_config
SSHNon-standard port, rate-limitedss -tlnp | grep sshd
Fail2banSSH jail activefail2ban-client status sshd
MACSELinux/AppArmor enforcingsestatus or apparmor_status
Kernelsysctl hardening appliedsysctl -a | grep syncookie
UsersNo blank passwords, root lockedsudo awk -F: '$2=="" {print}' /etc/shadow
sudoLeast privilege rules in placesudo visudo -c
SUIDBaseline documentedfind / -perm -4000 2>/dev/null | sort
LoggingRemote log forwarding activegrep @ /etc/rsyslog.d/*.conf
AuditingLynis score ≥ 80Run lynis audit system
BackupsEncrypted, tested restoreRun a restore drill quarterly

Conclusion

Securing a Linux server is an ongoing discipline, not a one-time checklist. The layered approach outlined in this guide — from firewall rules and SSH hardening through kernel tuning, mandatory access control, and automated auditing — gives you defence-in-depth: multiple independent barriers that an attacker must defeat sequentially.

The most important mindset shift is moving from reactive to proactive security. Enable unattended security updates from day one. Run Lynis weekly and fix high-priority findings promptly. Audit your SUID binaries after every software installation. Test your firewall rules from outside your own network. Keep logs centralised and monitored.

Hardening is also a team discipline. Document every change you make to the default configuration — in a runbook, in your infrastructure-as-code repository, or in your configuration management system. This documentation serves two purposes: it lets your colleagues understand the system state without examining every configuration file directly, and it creates a baseline that you can diff against after an incident to understand what may have changed. Share your Lynis scores and SCAP reports with your team on a regular cadence. Treat hardening findings with the same priority you would give to failing unit tests — as concrete, remediable problems that have a direct business impact.

No server is perfectly secure; the goal is to make the cost of a successful attack so high that attackers move on to softer targets. Every hardening step you implement raises that cost. Start with the foundational controls — update, firewall, SSH keys, Fail2ban — and progressively layer in SELinux or AppArmor, kernel parameter tuning, file system hardening, and automated compliance scanning. With each layer added, your server and the applications running on it become measurably safer.

Related Posts

There are no related posts yet for this article.