security

Best Practices to Secure a Linux Server in 2026

FastSox Team2026-03-2717 min read

A freshly provisioned Linux server is an open invitation. Within minutes of a public IP address going live, automated scanners begin probing SSH ports, testing default credentials, and mapping exposed services. Most breaches are not the result of sophisticated zero-days — they are the result of well-known configuration mistakes that take an afternoon to fix and years to recover from.

This guide walks through every major layer of Linux server security: network exposure, authentication, privilege management, file system integrity, kernel hardening, and monitoring. Work through the sections in order the first time you set up a server, then use the checklist at the end for auditing existing machines. Guide authored in collaboration with OneDotNet Ltd.


1. The Threat Model: Who Attacks Linux Servers and How

Before configuring anything, it helps to understand what you are actually defending against.

Automated bots account for the vast majority of attacks. Tools like Shodan index every public IP address continuously. Within seconds of a server going live, botnets are already attempting SSH logins using wordlists of common credentials. These bots do not care about your organization — they are looking for any foothold they can sell or use for cryptomining, spam relaying, or DDoS participation.

Ransomware operators increasingly target Linux infrastructure. Unlike Windows ransomware that spreads via phishing, Linux ransomware often arrives through unpatched web applications, exposed database ports (Redis, MongoDB, Elasticsearch without authentication), or compromised SSH credentials. Once inside, attackers move laterally across internal networks before deploying encryption payloads.

Supply chain attacks target the software you install. A malicious package uploaded to PyPI, npm, or a compromised apt mirror can silently exfiltrate credentials or install a backdoor. Integrity verification and minimal installed software are the mitigations.

Insider threats and credential theft round out the picture. Leaked SSH keys, reused passwords, and overly broad sudo permissions turn a single compromised developer laptop into full server access.

The mitigations in this guide address all four categories.


2. SSH Hardening

SSH is the primary remote management interface for most Linux servers and therefore the most attacked service. A handful of configuration changes eliminate the majority of risk.

Use Ed25519 Keys

Generate a modern Ed25519 key pair on your workstation before making any SSH configuration changes:

ssh-keygen -t ed25519 -C "[email protected]" -f ~/.ssh/id_ed25519

Copy the public key to your server:

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

Verify you can log in with the key before proceeding. Ed25519 keys are shorter, faster to verify, and not vulnerable to the parameter-selection weaknesses that affect some RSA implementations.

Harden /etc/ssh/sshd_config

Edit /etc/ssh/sshd_config (create a drop-in at /etc/ssh/sshd_config.d/hardening.conf on modern distributions to avoid conflicts with package upgrades):

# /etc/ssh/sshd_config.d/hardening.conf

# Never allow direct root login
PermitRootLogin no

# Disable all password-based authentication — keys only
PasswordAuthentication no
ChallengeResponseAuthentication no
KbdInteractiveAuthentication no

# Disable PAM password fallback
UsePAM yes
# (keep UsePAM yes for account/session modules, but passwords are already disabled above)

# Allow only specific users or groups
AllowUsers deploy alice bob
# AllowGroups sshusers   # alternative: group-based allowlist

# Restrict authentication algorithms to modern choices
PubkeyAcceptedAlgorithms ssh-ed25519,[email protected],rsa-sha2-512,rsa-sha2-256
HostKeyAlgorithms ssh-ed25519,rsa-sha2-512,rsa-sha2-256
KexAlgorithms curve25519-sha256,[email protected],diffie-hellman-group16-sha512
Ciphers [email protected],[email protected],[email protected]
MACs [email protected],[email protected]

# Disconnect idle sessions after 5 minutes
ClientAliveInterval 300
ClientAliveCountMax 0

# Reduce login grace period
LoginGraceTime 20

# Disable X11 and agent forwarding unless required
X11Forwarding no
AllowAgentForwarding no

# Disable TCP tunneling unless required
AllowTcpForwarding no

# Log level — INFO is sufficient; VERBOSE records key fingerprints
LogLevel VERBOSE

# Restrict to IPv4 or IPv6 as appropriate
#AddressFamily inet

Validate the configuration and restart:

sshd -t          # syntax check — must return no output
systemctl restart sshd

Changing the Default SSH Port

Running SSH on a non-standard port (e.g., 2222) does not improve security in any meaningful way against a determined attacker — port scanners find it in seconds. However, it does dramatically reduce log noise from automated bots that only probe port 22, which makes legitimate activity easier to spot.

If you change the port, update AddressFamily and Port in the config, then add the new port to your firewall before restarting sshd. Never close port 22 without confirming the new port works in a separate terminal session.

The tradeoff: non-standard ports break tools and scripts that assume port 22. The noise-reduction benefit is real but marginal. VPN-gated access (covered in Section 11) is a strictly better approach.


3. Firewall: Default Deny with an Allowlist

A firewall that only allows traffic you explicitly intend is your primary network-level defense. Every port you leave open that does not need to be open is unnecessary attack surface.

Using UFW (Ubuntu/Debian)

# Install
apt install ufw

# Default policy: deny all inbound, allow all outbound
ufw default deny incoming
ufw default allow outgoing

# Allow only what you need
ufw allow 22/tcp        # SSH (or your custom port)
ufw allow 80/tcp        # HTTP
ufw allow 443/tcp       # HTTPS

# Enable
ufw enable
ufw status verbose

For rate-limiting SSH (built-in alternative to fail2ban for simple cases):

ufw limit 22/tcp comment 'SSH rate limit'

Using iptables Directly

For production environments where you need fine-grained control, iptables gives you full flexibility. A complete walkthrough of building a stateful iptables ruleset — including anti-spoofing rules, connection tracking, and logging — is available in our companion guide: Bootstrap a Secure Linux Server with iptables and UFW.

Key principles regardless of which tool you use:

  • Default DROP on INPUT and FORWARD chains — allow only what is needed.
  • Allow established/related connections so responses to your outbound requests are not blocked.
  • Log and DROP everything else so you have a record of rejected traffic.
  • Save rules so they persist across reboots (iptables-save, ufw, or nftables with a systemd unit).

4. Fail2ban: Automatic Brute-Force Protection

Even with password authentication disabled, fail2ban is worth running. It blocks IPs that generate authentication errors — protecting against key enumeration, misconfigured clients hammering your server, and future misconfigurations that accidentally re-enable passwords.

Install and Basic SSH Configuration

# Debian/Ubuntu
apt install fail2ban

# RHEL/Fedora
dnf install fail2ban

Create a local override file (never edit /etc/fail2ban/jail.conf directly — it is overwritten on upgrades):

# /etc/fail2ban/jail.local

[DEFAULT]
# Ban for 1 hour
bantime  = 3600
# Watch a 10-minute window
findtime = 600
# Ban after 5 failures
maxretry = 5
# Ignore your own IPs
ignoreip = 127.0.0.1/8 ::1 YOUR_OFFICE_IP/32

[sshd]
enabled  = true
port     = ssh
filter   = sshd
logpath  = %(sshd_log)s
maxretry = 3
bantime  = 86400
systemctl enable --now fail2ban
fail2ban-client status sshd

Example output:

Status for the jail: sshd
|- Filter
|  |- Currently failed: 2
|  |- Total failed:     847
|  `- File list:        /var/log/auth.log
`- Actions
   |- Currently banned: 3
   |- Total banned:     91
   `- Banned IP list:   203.0.113.45 198.51.100.12 192.0.2.8

Custom Jail for Nginx

If you run a web server, add a jail for HTTP authentication failures:

# /etc/fail2ban/jail.d/nginx.local

[nginx-http-auth]
enabled  = true
filter   = nginx-http-auth
logpath  = /var/log/nginx/error.log
maxretry = 5
bantime  = 3600

[nginx-limit-req]
enabled  = true
filter   = nginx-limit-req
logpath  = /var/log/nginx/error.log
maxretry = 10
bantime  = 600

Unban an IP that was caught by mistake:

fail2ban-client set sshd unbanip 203.0.113.45

5. Automatic Security Updates

Known vulnerabilities with public exploits are patched by distributions within hours of disclosure. Servers that do not apply those patches become low-hanging fruit weeks or months later.

Ubuntu and Debian: unattended-upgrades

apt install unattended-upgrades apt-listchanges
dpkg-reconfigure -plow unattended-upgrades   # interactive enable

The configuration in /etc/apt/apt.conf.d/50unattended-upgrades controls what gets updated. The default setting applies security updates only — not all updates — which is the right balance between stability and security:

Unattended-Upgrade::Allowed-Origins {
    "${distro_id}:${distro_codename}-security";
    "${distro_id}ESMApps:${distro_codename}-apps-security";
    "${distro_id}ESM:${distro_codename}-infra-security";
};

// Reboot if required (e.g., kernel update), at 02:00
Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-Time "02:00";

// Email on errors
Unattended-Upgrade::Mail "[email protected]";
Unattended-Upgrade::MailReport "on-change";

Test the configuration without installing anything:

unattended-upgrade --dry-run --debug

Fedora, RHEL, and CentOS Stream: dnf-automatic

dnf install dnf-automatic

# Edit /etc/dnf/automatic.conf
# Set: upgrade_type = security
# Set: apply_updates = yes

systemctl enable --now dnf-automatic-install.timer
systemctl status dnf-automatic-install.timer

For RHEL systems with dnf, this applies security updates daily via a systemd timer.


6. User and Privilege Management

The principle of least privilege: every user and process should have exactly the access it needs — nothing more.

Create a Non-Root Sudo User

Never operate as root interactively. Create a dedicated administrator account:

adduser alice
usermod -aG sudo alice       # Debian/Ubuntu
usermod -aG wheel alice      # RHEL/Fedora

Verify sudo access from the new account before locking down root:

su - alice
sudo whoami    # should print "root"

Minimal sudo Permissions

The default sudo configuration gives sudo group members unrestricted root access. For production systems, define fine-grained rules in /etc/sudoers.d/:

# /etc/sudoers.d/deploy
# The deploy user can restart nginx and reload its config — nothing else
deploy ALL=(root) NOPASSWD: /bin/systemctl restart nginx, /bin/systemctl reload nginx

# /etc/sudoers.d/backup
# The backup user can read /etc and /var/lib but nothing else
backup ALL=(root) NOPASSWD: /usr/bin/rsync

Always edit sudoers files with visudo -f /etc/sudoers.d/deploy — it validates syntax before saving.

Audit Login Activity

Check who has logged in and from where:

last              # successful logins from /var/log/wtmp
lastb             # failed login attempts from /var/log/btmp
who               # currently logged-in users
w                 # logged-in users with what they are doing
lastlog           # last login for every account on the system

Review /var/log/auth.log (Debian/Ubuntu) or /var/log/secure (RHEL) regularly, or pipe it through fail2ban and auditd (Section 9).


7. File System Security

Attackers who gain a shell often rely on writable directories and setuid binaries to escalate privileges. Reduce those opportunities.

Mount /tmp with noexec

The /tmp directory should never need to execute binaries. Mounting it with noexec prevents attackers from dropping and running exploits there:

# /etc/fstab — add or modify the /tmp entry
tmpfs   /tmp   tmpfs   defaults,noexec,nosuid,nodev   0 0

Apply without rebooting:

mount -o remount,noexec,nosuid,nodev /tmp

Consider the same for /var/tmp and /dev/shm.

Audit File Permissions

Find world-writable files and directories:

find / -xdev -type f -perm -0002 -ls 2>/dev/null
find / -xdev -type d -perm -0002 -ls 2>/dev/null

Find setuid (SUID) and setgid (SGID) binaries — these run with elevated privileges and are prime escalation targets:

find / -xdev -perm -4000 -type f -ls 2>/dev/null   # SUID
find / -xdev -perm -2000 -type f -ls 2>/dev/null   # SGID

Maintain a baseline of expected SUID binaries. Any new entry after a package installation should be investigated.

File Integrity Monitoring with AIDE

aide (Advanced Intrusion Detection Environment) creates a cryptographic baseline of your file system and alerts you to any changes:

apt install aide       # or: dnf install aide

# Initialize the database (do this immediately after a clean install)
aideinit
cp /var/lib/aide/aide.db.new /var/lib/aide/aide.db

# Check for changes
aide --check

Run aide --check from cron daily and send the output to your monitoring system. Alternatives include tripwire and samhain.


8. Kernel Hardening via sysctl

The Linux kernel exposes runtime configuration through /proc/sys. Several defaults favor compatibility over security and should be changed on production servers.

Create /etc/sysctl.d/99-hardening.conf:

# --- Network hardening ---

# Disable IP forwarding (enable only if this machine routes traffic)
net.ipv4.ip_forward = 0
net.ipv6.conf.all.forwarding = 0

# Reverse path filtering: drop packets that appear to come from an address
# that the kernel would not route back to (anti-spoofing)
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.default.rp_filter = 1

# Do not accept ICMP redirects (prevents MITM via routing manipulation)
net.ipv4.conf.all.accept_redirects = 0
net.ipv6.conf.all.accept_redirects = 0
net.ipv4.conf.default.accept_redirects = 0

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

# Ignore ICMP echo broadcast (Smurf attack mitigation)
net.ipv4.icmp_echo_ignore_broadcasts = 1

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

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

# --- Kernel hardening ---

# Restrict access to kernel logs — prevents info leak from dmesg
kernel.dmesg_restrict = 1

# Disable core dumps for setuid programs
fs.suid_dumpable = 0

# Restrict kernel pointer exposure in /proc
kernel.kptr_restrict = 2

# Restrict ptrace to child processes only (reduces scope of credential theft)
kernel.yama.ptrace_scope = 1

# Disable the magic SysRq key in production
kernel.sysrq = 0

# Randomize memory layout (ASLR) — should already be 2, verify it
kernel.randomize_va_space = 2

Apply immediately:

sysctl --system
# or apply a specific file:
sysctl -p /etc/sysctl.d/99-hardening.conf

Verify a value:

sysctl net.ipv4.conf.all.rp_filter
# Expected: net.ipv4.conf.all.rp_filter = 1

9. Monitoring and Audit

You cannot defend what you cannot see. A server without audit logging is a server where attackers operate silently.

auditd: Syscall-Level Logging

auditd is the Linux kernel's native audit subsystem. It can record file accesses, command executions, privilege escalations, and network connections:

apt install auditd audispd-plugins    # Debian/Ubuntu
dnf install audit                     # RHEL/Fedora

systemctl enable --now auditd

Add audit rules to /etc/audit/rules.d/hardening.rules:

# Monitor writes to sensitive files
-w /etc/passwd -p wa -k identity
-w /etc/shadow -p wa -k identity
-w /etc/sudoers -p wa -k privilege
-w /etc/ssh/sshd_config -p wa -k sshd

# Monitor privileged commands
-a always,exit -F arch=b64 -S execve -F euid=0 -k root_commands

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

Query the audit log:

ausearch -k identity          # changes to /etc/passwd or /etc/shadow
ausearch -k privilege         # sudo rule changes
aureport --auth               # authentication summary report

Log Analysis with logwatch

logwatch parses system logs and emails a daily digest:

apt install logwatch
logwatch --output mail --mailto [email protected] --detail high

For web traffic analysis, GoAccess provides real-time visualization of nginx or Apache access logs:

apt install goaccess
goaccess /var/log/nginx/access.log -o /var/www/html/report.html --log-format=COMBINED --real-time-html

Rootkit Detection

Periodically scan for known rootkits and suspicious modifications:

# rkhunter
apt install rkhunter
rkhunter --update
rkhunter --check --skip-keypress

# chkrootkit
apt install chkrootkit
chkrootkit

Run these from cron and alert on any positive findings. Note that these tools generate false positives — investigate each finding rather than dismissing alerts as noise.


10. Network Exposure Reduction

Every service listening on a network interface is potential attack surface. Audit and minimize what is exposed.

Audit Listening Ports

ss -tlnp       # TCP listening ports with process names
ss -ulnp       # UDP listening ports

Example output:

State   Recv-Q  Send-Q  Local Address:Port  Peer Address:Port  Process
LISTEN  0       128     0.0.0.0:22          0.0.0.0:*          users:(("sshd",pid=892))
LISTEN  0       511     0.0.0.0:80          0.0.0.0:*          users:(("nginx",pid=1043))
LISTEN  0       511     0.0.0.0:443         0.0.0.0:*          users:(("nginx",pid=1043))

Question every entry. If you do not recognize a service, investigate before continuing.

Disable Unused Services

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

# Disable services you do not need
systemctl disable --now avahi-daemon    # mDNS/Bonjour — unnecessary on servers
systemctl disable --now cups            # printing
systemctl disable --now bluetooth       # Bluetooth
systemctl disable --now rpcbind         # NFS portmapper, unless you use NFS

On minimal server installs many of these will already be absent. On general-purpose installs they often appear.

Bind Services to Specific Interfaces

Applications that only need to serve localhost should not bind to all interfaces (0.0.0.0). For example, a PostgreSQL database used only by a local application:

# /etc/postgresql/16/main/postgresql.conf
listen_addresses = '127.0.0.1'

For Redis:

# /etc/redis/redis.conf
bind 127.0.0.1 ::1

Never expose database ports to the public internet.


11. VPN-Only SSH Access with FastSox

The most effective way to protect SSH is to remove it from the public internet entirely. If only VPN-connected clients can reach port 22, the entire category of internet-facing brute force attacks disappears — regardless of whether fail2ban is configured, regardless of key strength.

FastSox provides a lightweight, WireGuard-based overlay network that lets you connect your servers and workstations into a private network. Once configured:

  1. Your server's SSH port is firewalled to accept connections only from the FastSox VPN subnet.
  2. Administrators connect to FastSox before SSH-ing into any server.
  3. Public-facing ports (80, 443) remain open; management access is invisible to the internet.

The firewall rule to implement this (replacing the broad SSH allow):

# Allow SSH only from FastSox VPN subnet (adjust to your actual VPN range)
ufw allow from 10.0.0.0/8 to any port 22 proto tcp comment 'SSH via FastSox VPN'
ufw delete allow 22/tcp   # remove the previous public rule

Or with iptables:

iptables -A INPUT -s 10.0.0.0/8 -p tcp --dport 22 -j ACCEPT
iptables -A INPUT -p tcp --dport 22 -j DROP

This approach eliminates the tradeoff between convenience and security: administrators get seamless SSH access through a secure, authenticated channel, while public attackers see no SSH port at all.

FastSox supports Linux, macOS, Windows, iOS, and Android clients, making it practical for teams of any size. See fastsox.com for setup guides and pricing.


12. Quick Security Checklist

Use this table to audit an existing server or verify a new deployment:

| Area | Check | Command / File | |------|-------|----------------| | SSH | Root login disabled | grep PermitRootLogin /etc/ssh/sshd_configno | | SSH | Password auth disabled | grep PasswordAuthentication /etc/ssh/sshd_configno | | SSH | Ed25519 keys in use | ls ~/.ssh/authorized_keys | | SSH | AllowUsers / AllowGroups set | grep -E 'AllowUsers\|AllowGroups' /etc/ssh/sshd_config | | Firewall | Default deny on INPUT | ufw status or iptables -L INPUT | | Firewall | Only necessary ports open | ss -tlnp cross-checked with firewall rules | | Fail2ban | Running and watching SSH | fail2ban-client status sshd | | Updates | Automatic security updates enabled | systemctl status unattended-upgrades / dnf-automatic | | Users | No password for root | passwd -S rootL (locked) | | Users | Non-root sudo user exists | getent group sudo / getent group wheel | | Users | sudoers uses minimal permissions | visudo -c + review /etc/sudoers.d/ | | File system | /tmp mounted noexec | mount | grep /tmp → includes noexec | | File system | SUID binary baseline maintained | find / -xdev -perm -4000 -type f reviewed | | File system | AIDE baseline initialized | aide --check returns no unexpected changes | | Kernel | rp_filter enabled | sysctl net.ipv4.conf.all.rp_filter1 | | Kernel | ASLR enabled | sysctl kernel.randomize_va_space2 | | Kernel | dmesg restricted | sysctl kernel.dmesg_restrict1 | | Kernel | suid_dumpable disabled | sysctl fs.suid_dumpable0 | | Monitoring | auditd running | systemctl status auditd | | Monitoring | Rootkit scan scheduled | crontab -l includes rkhunter or chkrootkit | | Network | No unexpected listening services | ss -tlnp reviewed | | Network | Databases not internet-exposed | ss -tlnp | grep -E '5432\|3306\|6379' bound to loopback | | VPN | SSH accessible only via VPN | ufw status / firewall rules restrict port 22 to VPN subnet |


Closing Thoughts

Security is not a one-time configuration exercise — it is an ongoing practice. A server that was secure at deployment can become vulnerable through a new CVE, a dependency update introducing a regression, or a team member making a well-intentioned but insecure configuration change.

Build the habits alongside the configuration: automate updates, review audit logs weekly, run rootkit scans from cron, and revisit this checklist after any significant infrastructure change. The goal is not perfect security — it is making your server a sufficiently unattractive target that automated attacks move on and attackers would need to invest disproportionate effort to succeed.

For teams managing multiple servers, centralizing management access through a VPN like FastSox reduces the attack surface of every server simultaneously. Rather than hardening SSH on each machine individually, you make the entire management plane invisible to the public internet.


This guide is maintained by the FastSox Team. Infrastructure security consulting is available through OneDotNet Ltd.

#linux#security#server#hardening#ssh#fail2ban#best-practices

Related Articles