Linux Server Hardening: SSH, Firewall, SELinux, Updates, User Management, and Logging
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
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
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:
- SSH login from a new source IP address
- Failed sudo attempts (authentication failure when a user attempts sudo)
- Modification of system configuration files (/etc/passwd, /etc/shadow, /etc/sudoers)
- New user account creation
- Service stopped or started outside maintenance windows
- Firewall rule changes
- SELinux/AppArmor policy violations
- Disk space exceeding 90 percent on any partition
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