Linux Server Hardening: SSH, Firewall, SELinux, Updates, User Management, and Logging

Published March 22, 2026 - 24 min read

Every Linux server connected to the internet is under attack within minutes of receiving a public IP address. Automated scanners probe SSH ports, web servers, and known vulnerable services continuously. The default configuration of most Linux distributions prioritizes usability and compatibility, not security. A freshly installed server accepts password-based SSH authentication, runs unnecessary services, and logs minimally - a configuration that assumes a trusted network that no longer exists.

Server hardening is the process of reducing the attack surface by removing unnecessary software, restricting access to required services, enforcing mandatory access controls, and ensuring that every security-relevant event is logged and monitored. This guide covers the six domains of Linux server hardening that transform a default installation into a defensible system: SSH hardening, firewall configuration, mandatory access control with SELinux, automated patching, user management, and centralized logging.

SSH Hardening

SSH is the primary remote access vector for Linux servers and consequently the most attacked service. Brute-force password attacks against SSH generate millions of attempts per day against internet-facing servers. Properly configured SSH authentication eliminates this entire category of attack.

Key-Based Authentication

The single most impactful SSH hardening step is switching from password authentication to key-based authentication. An SSH key pair consists of a private key that stays on your workstation and a public key that is placed on the server. Authentication proves you possess the private key without transmitting it, making brute-force attacks impossible.

Generate an Ed25519 key pair, which provides the best combination of security and performance:

ssh-keygen -t ed25519 -C "admin@example.com"

Copy the public key to the server:

ssh-copy-id -i ~/.ssh/id_ed25519.pub user@server

Verify that key-based login works by opening a new terminal and connecting. Do not close your existing session until you have confirmed the new connection method works. Then edit the SSH daemon configuration:

# /etc/ssh/sshd_config
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
ChallengeResponseAuthentication no
UsePAM yes
AuthenticationMethods publickey
MaxAuthTries 3
LoginGraceTime 30
ClientAliveInterval 300
ClientAliveCountMax 2

Restart the SSH service:

systemctl restart sshd
Critical: always test SSH access in a new session before closing your existing session after modifying sshd_config. A misconfiguration can lock you out of the server permanently.

SSH Port and Access Restrictions

Changing the SSH port from 22 to a non-standard port reduces log noise from automated scanners but provides no real security against targeted attacks. It is a useful noise reduction measure, not a security control. If you change the port, document it in your infrastructure runbook and update all automation scripts.

More effective restrictions include limiting SSH access to specific source IP addresses using the firewall, restricting SSH access to specific users with the AllowUsers or AllowGroups directive, and disabling X11 forwarding, TCP forwarding, and agent forwarding unless specifically required:

# Additional SSH hardening
AllowGroups sshusers
X11Forwarding no
AllowTcpForwarding no
AllowAgentForwarding no
PermitTunnel no
Banner /etc/ssh/banner

Fail2ban Configuration

Fail2ban monitors log files and temporarily bans IP addresses that show malicious behavior. For SSH, it detects repeated failed authentication attempts and adds firewall rules to block the source IP. Install and configure fail2ban for SSH protection:

# Install fail2ban
# Debian/Ubuntu
apt install fail2ban

# RHEL/Rocky
dnf install fail2ban

Create a local jail configuration that overrides the defaults:

# /etc/fail2ban/jail.local
[DEFAULT]
bantime = 3600
findtime = 600
maxretry = 3
banaction = nftables

[sshd]
enabled = true
port = ssh
logpath = %(sshd_log)s
maxretry = 3
bantime = 86400

This configuration bans an IP address for 24 hours after 3 failed SSH attempts within 10 minutes. The nftables ban action integrates with modern Linux firewall frameworks. Start and enable fail2ban:

systemctl enable --now fail2ban

Firewall Configuration

A host-based firewall is mandatory for every Linux server regardless of whether a network firewall exists. Network firewalls protect the perimeter. Host-based firewalls protect the server itself, including from lateral movement by attackers who have already compromised another system on the same network.

Default Deny Policy

The fundamental principle of firewall configuration is default deny: block all traffic by default and explicitly allow only what is required. Every open port is an attack surface. A web server needs ports 80 and 443. An SSH server needs port 22. Everything else should be blocked inbound.

Using nftables, the modern Linux firewall framework:

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

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

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

        # Allow loopback
        iif lo accept

        # Allow ICMP (ping)
        ip protocol icmp accept
        ip6 nexthdr icmpv6 accept

        # Allow SSH (rate limited)
        tcp dport 22 ct state new limit rate 4/minute accept

        # Allow HTTP/HTTPS
        tcp dport { 80, 443 } accept

        # Log dropped packets
        log prefix "nftables-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;
    }
}

The SSH rate limiting rule allows 4 new connections per minute per source, which is sufficient for legitimate administrators but throttles automated scanners. Load the ruleset and make it persistent:

nft -f /etc/nftables.conf
systemctl enable nftables

Firewalld for Red Hat Distributions

On RHEL, Rocky Linux, and AlmaLinux, firewalld provides a zone-based management layer on top of nftables. The default zone determines policy for interfaces not assigned to a specific zone. Configure the public zone for internet-facing servers:

# Set default zone
firewall-cmd --set-default-zone=public

# Allow only required services
firewall-cmd --zone=public --add-service=ssh --permanent
firewall-cmd --zone=public --add-service=http --permanent
firewall-cmd --zone=public --add-service=https --permanent

# Remove unnecessary services
firewall-cmd --zone=public --remove-service=dhcpv6-client --permanent
firewall-cmd --zone=public --remove-service=cockpit --permanent

# Apply changes
firewall-cmd --reload

# Verify
firewall-cmd --zone=public --list-all

UFW for Debian and Ubuntu

UFW (Uncomplicated Firewall) provides a simplified interface on Debian and Ubuntu:

# Set default policies
ufw default deny incoming
ufw default allow outgoing

# Allow SSH (rate limited)
ufw limit ssh

# Allow web traffic
ufw allow 80/tcp
ufw allow 443/tcp

# Enable firewall
ufw enable

# Verify status
ufw status verbose

SELinux and Mandatory Access Control

Traditional Linux permissions (discretionary access control) rely on file owners and groups to control access. If a process is compromised, it inherits the permissions of the user running it. If that user has broad access, the attacker gains broad access. Mandatory access control (MAC) systems like SELinux and AppArmor add a second layer that restricts processes to only the resources explicitly defined in their security policy, regardless of the user context.

SELinux Configuration

SELinux ships with Red Hat, Rocky Linux, AlmaLinux, and Fedora. The three modes are Enforcing (blocks policy violations), Permissive (logs violations but allows them), and Disabled. Production servers must run in Enforcing mode.

Check the current SELinux status:

getenforce
sestatus

If SELinux is disabled, enable it by editing /etc/selinux/config:

SELINUX=enforcing
SELINUXTYPE=targeted

The targeted policy confines specific services while leaving the rest of the system in an unconfined domain. This is the correct policy for most servers. The mls (Multi-Level Security) policy is for military and government systems with formal security classification requirements.

When an application is blocked by SELinux, the audit log records an AVC denial. Use the audit2why tool to understand why a denial occurred:

# Find recent SELinux denials
ausearch -m AVC -ts recent

# Explain the denial
audit2why < /var/log/audit/audit.log

Common SELinux operations for web servers:

# Allow HTTPD to connect to network (for reverse proxy)
setsebool -P httpd_can_network_connect 1

# Allow HTTPD to send mail
setsebool -P httpd_can_sendmail 1

# Label custom web content directory
semanage fcontext -a -t httpd_sys_content_t "/srv/www(/.*)?"
restorecon -Rv /srv/www
Never disable SELinux to fix an application issue. Instead, investigate the AVC denial, understand what access the application needs, and create a targeted policy module using audit2allow. Disabling SELinux removes protection from every service on the system to fix a problem with one service.

AppArmor on Debian and Ubuntu

AppArmor ships enabled by default on Ubuntu and Debian. It uses path-based profiles rather than SELinux's label-based approach. Check the status:

aa-status

Profiles in enforce mode block violations. Profiles in complain mode log violations without blocking. Use complain mode to develop new profiles, then switch to enforce mode for production:

# Set a profile to enforce mode
aa-enforce /etc/apparmor.d/usr.sbin.nginx

# Set a profile to complain mode for testing
aa-complain /etc/apparmor.d/usr.sbin.nginx

Automated Security Updates

Unpatched software is the leading cause of Linux server compromises. The median time between vulnerability disclosure and active exploitation has dropped to under 72 hours for critical vulnerabilities. Manual patching processes that operate on monthly cycles leave servers exposed for weeks. Automated security updates close this gap.

Debian and Ubuntu: unattended-upgrades

Configure unattended-upgrades to apply security patches automatically:

# Install
apt install unattended-upgrades

# Enable
dpkg-reconfigure -plow unattended-upgrades

Configure the update behavior in /etc/apt/apt.conf.d/50unattended-upgrades:

Unattended-Upgrade::Allowed-Origins {
    "${distro_id}:${distro_codename}-security";
};
Unattended-Upgrade::AutoFixInterruptedDpkg "true";
Unattended-Upgrade::Remove-Unused-Dependencies "true";
Unattended-Upgrade::Automatic-Reboot "false";
Unattended-Upgrade::Mail "admin@example.com";

Set Automatic-Reboot to false for production servers and schedule kernel update reboots during maintenance windows. Enable email notifications so that administrators know when patches are applied and can schedule reboots for kernel updates.

RHEL, Rocky, and AlmaLinux: dnf-automatic

Configure dnf-automatic for automatic security updates:

# Install
dnf install dnf-automatic

# Configure for security-only updates
# /etc/dnf/automatic.conf
[commands]
upgrade_type = security
apply_updates = yes

[emitters]
emit_via = email

[email]
email_from = root@server.example.com
email_to = admin@example.com

Enable the timer:

systemctl enable --now dnf-automatic.timer

Kernel Live Patching

Kernel vulnerabilities require reboots to apply traditional patches. Kernel live patching applies security fixes to the running kernel without a reboot. On Ubuntu, Canonical Livepatch is available for free on up to 5 machines. On RHEL, kpatch is included with the subscription. Enable live patching to eliminate the window between patch availability and the next maintenance window reboot:

# Ubuntu Livepatch
canonical-livepatch enable YOUR-TOKEN

# RHEL kpatch
dnf install kpatch
kpatch list

User Management and Access Control

User account management directly affects the security posture of every Linux server. Excessive privileges, shared accounts, and stale credentials are the most common findings in security audits. Disciplined user management eliminates these problems.

Principle of Least Privilege

Every user account should have the minimum permissions required for its function. Service accounts should not have login shells. Administrative access should be granted through sudo with specific command allowlists, not through direct root access or unrestricted sudo.

# Create a service account with no login shell
useradd -r -s /usr/sbin/nologin -d /nonexistent appservice

# Create an admin user
useradd -m -s /bin/bash -G sshusers,wheel admin01

# Configure sudo for specific commands
# /etc/sudoers.d/admin01
admin01 ALL=(ALL) /usr/bin/systemctl restart nginx, /usr/bin/systemctl restart postgresql, /usr/bin/journalctl

This configuration allows admin01 to restart nginx and PostgreSQL and read journal logs without granting unrestricted root access. If admin01's account is compromised, the attacker can restart two services and read logs but cannot modify system configuration, install software, or access other users' data.

Password Policy Enforcement

For accounts that require passwords (local console access, emergency accounts), enforce strong password policies through PAM. Configure /etc/security/pwquality.conf:

# /etc/security/pwquality.conf
minlen = 14
dcredit = -1
ucredit = -1
ocredit = -1
lcredit = -1
minclass = 3
maxrepeat = 3
reject_username
enforce_for_root

Configure password aging in /etc/login.defs:

PASS_MAX_DAYS   90
PASS_MIN_DAYS   1
PASS_WARN_AGE   14

For existing users, apply password aging with the chage command:

chage -M 90 -m 1 -W 14 username

Account Auditing and Cleanup

Stale accounts are a persistent security risk. Accounts of departed employees, unused service accounts, and test accounts accumulate over time and provide potential entry points. Implement a regular account review process:

# Find accounts that have never logged in
lastlog | grep "Never logged in"

# Find accounts with no password expiration
awk -F: '($5 == "" || $5 == 99999) {print $1}' /etc/shadow

# Find accounts with UID 0 (root equivalents)
awk -F: '$3 == 0 {print $1}' /etc/passwd

# Lock inactive accounts
usermod -L username
# Or set expiration date
usermod -e 2026-04-01 username

Run this audit monthly. Automate it with a cron job that generates a report and emails it to the security team. Accounts that have been inactive for 90 days should be locked. Accounts inactive for 180 days should be disabled and their data archived.

Logging and Monitoring

Logging is the foundation of incident detection and forensic investigation. A compromised server with poor logging gives you no visibility into what happened, when it happened, or what data was accessed. Comprehensive logging with centralized collection and tamper detection transforms security incidents from mysteries into investigations with evidence.

Auditd Configuration

The Linux audit framework (auditd) provides kernel-level event logging that captures security-relevant system calls, file access, and authentication events. Install and configure auditd:

# Install
# Debian/Ubuntu
apt install auditd audispd-plugins

# RHEL/Rocky
dnf install audit

Configure audit rules for critical security events:

# /etc/audit/rules.d/hardening.rules

# Monitor authentication files
-w /etc/passwd -p wa -k identity
-w /etc/group -p wa -k identity
-w /etc/shadow -p wa -k identity
-w /etc/sudoers -p wa -k sudoers
-w /etc/sudoers.d/ -p wa -k sudoers

# Monitor SSH configuration
-w /etc/ssh/sshd_config -p wa -k sshd_config

# Monitor cron
-w /etc/crontab -p wa -k cron
-w /etc/cron.d/ -p wa -k cron

# Monitor firewall rules
-w /etc/nftables.conf -p wa -k firewall

# Log all commands run by root
-a always,exit -F arch=b64 -F euid=0 -S execve -k root_commands

# Log failed file access attempts
-a always,exit -F arch=b64 -S open,openat -F exit=-EACCES -k access_denied
-a always,exit -F arch=b64 -S open,openat -F exit=-EPERM -k access_denied

# Log privilege escalation
-a always,exit -F arch=b64 -S setuid -S setgid -k privilege_escalation

Enable and start auditd:

systemctl enable --now auditd
augenrules --load

Centralized Log Collection

Local logs are valuable but vulnerable. An attacker with root access can modify or delete local log files to cover their tracks. Centralized log collection sends copies of log events to a separate system in real time, preserving evidence even if the source server is fully compromised.

Configure rsyslog to forward logs to a central syslog server:

# /etc/rsyslog.d/50-remote.conf
# Forward all logs to central server via TCP with TLS
*.* @@(o)logserver.example.com:6514

For modern deployments, consider using Promtail with Loki, Filebeat with Elasticsearch, or the OpenTelemetry Collector for log aggregation. These tools provide structured logging, efficient storage, and search capabilities that traditional syslog forwarding does not offer.

Log Monitoring and Alerting

Logs that nobody reads provide no security value. Configure alerting for critical events that require immediate investigation:

Use logwatch for daily email summaries of log activity on individual servers. For fleet-wide monitoring, deploy a SIEM solution or use Grafana with Loki for log visualization and alerting based on patterns rather than individual events.

Additional Hardening Measures

Remove Unnecessary Packages and Services

Every installed package is potential attack surface. Audit installed packages and remove anything not required for the server's function:

# List installed packages
# Debian/Ubuntu
dpkg --list | grep ^ii

# RHEL/Rocky
rpm -qa

# List enabled services
systemctl list-unit-files --state=enabled

Common services to disable on servers that do not need them: bluetooth, cups (printing), avahi-daemon (mDNS), rpcbind (NFS portmapper), and postfix (if not used for outbound email).

File System Hardening

Configure mount options to restrict what can happen on specific file systems. The /tmp directory should be mounted with noexec, nosuid, and nodev options to prevent execution of uploaded malicious binaries:

# /etc/fstab entry for /tmp
tmpfs /tmp tmpfs defaults,noexec,nosuid,nodev,size=2G 0 0

Set the sticky bit and restrictive permissions on /tmp:

chmod 1777 /tmp

Configure AIDE (Advanced Intrusion Detection Environment) or OSSEC for file integrity monitoring. These tools create a baseline hash of critical system files and alert when files are modified:

# Install AIDE
apt install aide    # Debian/Ubuntu
dnf install aide    # RHEL/Rocky

# Initialize the database
aide --init

# Move the new database into place
mv /var/lib/aide/aide.db.new /var/lib/aide/aide.db

# Run a check
aide --check

Schedule AIDE checks via cron and review the output for unauthorized modifications to system binaries, configuration files, and libraries.

Automate Your Server Hardening Compliance Checks

HelpBot monitors your Linux servers continuously and creates tickets automatically when configurations drift from your security baseline. No more manual audits.

Start Free Trial

Related Articles

Back to Home

Still managing IT tickets manually?

See how HelpBot can cut your ticket resolution time by 70%. Free ROI calculator included.

Calculate Your ROIStart Free Trial