Linux Security Checklist for Production Servers
DEV Community

Linux Security Checklist for Production Servers

A step-by-step practical guide to locking down SSH, configuring firewalls, enforcing least privilege, hardening the kernel, and setting up audit trails on your production systems. The moment you spin up a fresh virtual machine on AWS, DigitalOcean, Hetzner, or a bare-metal server in a datacenter, the clock starts ticking. Within minutes of your public IP address going live, automated bots and port scanners around the globe will begin probing your server. They will scan port 22, attempt thousands of default password combinations, search for open web ports, and test for known vulnerabilities. If your server runs on default settings, it is only a matter of time before someone finds a crack. A default Linux installation (whether Ubuntu, Debian, Rocky Linux, or AlmaLinux) is built for convenience, not fortress-grade security. Default configurations often leave password authentication enabled, root logins permitted, unused network ports exposed, and kernel settings tuned for general desktop workloads rather than high-security production environments. Security is not a single tool you install. It is a process of defense-in-depth, building multiple overlapping layers of protection around your system. If an attacker bypasses one layer, the next layer stops them in their tracks. Here is a practical, battle-tested Linux security checklist you can use to harden your production servers from day one. 1. SSH Hardening: Locking Down the Front Door Secure Shell (SSH) is your primary administrative interface, which also makes it the number one target for automated brute-force attacks. Securing SSH is the first and most critical step in server hardening. All SSH server configurations live in /etc/ssh/sshd_config or modular files inside /etc/ssh/sshd_config.d/ . Step 1: Disable Root Login and Password Authentication Never allow direct logins to the root account over SSH, and never allow plain text passwords. Always require cryptographic SSH key pairs (preferably Ed25519 keys). Generate a secure Ed25519 key on your local machine if you have not already: $ ssh-keygen -t ed25519 -C "a****@yourcompany.com" Copy your public key to the remote server: $ ssh-copy-id -i ~/.ssh/id_ed25519.pub asep@203.0.113.10 Now, edit the SSH daemon configuration on the server: $ sudo nano /etc/ssh/sshd_config.d/99-hardening.conf Add the following hardening directives: # Disable root login over SSH PermitRootLogin no # Enforce public key authentication only PubkeyAuthentication yes PasswordAuthentication no PermitEmptyPasswords no # Disable legacy authentication methods KbdInteractiveAuthentication no ChallengeResponseAuthentication no GSSAPIAuthentication no # Limit authentication attempts per connection MaxAuthTries 3 MaxSessions 4 # Terminate idle SSH sessions after 10 minutes of inactivity ClientAliveInterval 300 ClientAliveCountMax 2 # Disable risky forwarding features X11Forwarding no AllowAgentForwarding no AllowTcpForwarding no # Restrict SSH access to specific users or groups AllowGroups sudo sysadmin Step 2: Use Modern Ciphers and Key Exchange Algorithms Legacy SSH implementations may still negotiate outdated ciphers like 3DES, blowfish, or SHA-1 hashes. Restrict your SSH daemon to modern, secure cryptographic algorithms: KexAlgorithms curve25519-sha256,cur**************@libssh.org,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512 Ciphers cha**************@openssh.com,ae********@openssh.com,ae********@openssh.com MACs hma**************@openssh.com,hma**************@openssh.com Step 3: Test and Apply Configuration Before restarting the SSH daemon, always test the configuration syntax. A single typo in sshd_config can lock you out of a remote server permanently: $ sudo sshd -t If the command returns no output, your syntax is valid. Now restart the SSH service: $ sudo systemctl restart sshd Safety Tip: Do not close your current active terminal session after restarting SSH. Open a new terminal window and test logging in with your SSH key to confirm you can still connect before disconnecting your existing session. 2. User & Access Control: Applying the Principle of Least Privilege Every user and service on your server should operate with the minimum level of privileges necessary to perform its job. If a service account is compromised, least privilege stops the attacker from taking over the entire host. Lock Default and Unused Accounts Linux distributions ship with dozens of system accounts (like games , news , ftp , lp ). Verify that these system accounts have their login shells set to /usr/sbin/nologin or /bin/false , and lock unused accounts: $ sudo passwd -l root $ sudo usermod -s /usr/sbin/nologin games Configure Modular Sudo Access Never edit /etc/sudoers directly with a normal text editor. Always use visudo , which checks syntax before saving to prevent corrupting your superuser configuration. Create dedicated sudo rules inside /etc/sudoers.d/ : $ sudo visudo -f /etc/sudoers.d/99-sysadmin Add granular permissions instead of handing out blanket ALL access where possible. For instance, allowing a deploy user to only restart a specific web service: deploy ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart nginx, /usr/bin/systemctl reload nginx Ensure the permissions on any file in /etc/sudoers.d/ are strictly set to 0440 : $ sudo chmod 0440 /etc/sudoers.d/99-sysadmin Run Applications Under Dedicated Non-Root Users Never run web applications, Node.js backends, Python scripts, or Docker containers as root . Create isolated system users without home directories or interactive login shells: $ sudo useradd -r -s /usr/sbin/nologin -d /var/www/my-app appuser When building self-hosted network applications, restricting application permissions and enforcing strict path isolation is a fundamental design rule. For example, in projects like AiroShare, strict path isolation guards against directory traversal attacks, ensuring that even if an HTTP request attempts to access ../../etc/shadow , the application layer and underlying non-root user permissions reject the request immediately. 3. Network Security & Firewall Configuration A production server should never expose internal ports to the public internet. If a database, cache, or internal metrics exporter does not need public access, bind it strictly to 127.0.0.1 or a private VPN interface (like WireGuard or Tailscale). Set Up a Default-Deny Firewall with UFW On Ubuntu and Debian systems, Uncomplicated Firewall (UFW) provides a simple, dependable interface for managing iptables and nftables rules. Step 1: Set the default policy to deny all incoming traffic and allow outgoing traffic: $ sudo ufw default deny incoming $ sudo ufw default allow outgoing Step 2: Allow your SSH port (make sure you do this before enabling the firewall): $ sudo ufw allow 22/tcp comment "SSH Management" Step 3: Allow only required public application traffic (e.g., HTTP and HTTPS): $ sudo ufw allow 80/tcp comment "HTTP Web Traffic" $ sudo ufw allow 443/tcp comment "HTTPS Web Traffic" Step 4: Enable the firewall and check status: $ sudo ufw enable $ sudo ufw status verbose Output: Status: active Logging: on (low) Default: deny (incoming), allow (outgoing), disabled (routed) New profiles: skip To Action From -- ------ ---- 22/tcp ALLOW IN Anywhere # SSH Management 80/tcp ALLOW IN Anywhere # HTTP Web Traffic 443/tcp ALLOW IN Anywhere # HTTPS Web Traffic 22/tcp (v6) ALLOW IN Anywhere (v6) # SSH Management 80/tcp (v6) ALLOW IN Anywhere (v6) # HTTP Web Traffic 443/tcp (v6) ALLOW IN Anywhere (v6) # HTTPS Web Traffic Audit Open Sockets and Listening Ports Check what services are currently listening on network sockets: $ sudo ss -tulnp Look closely at the Local Address:Port column: - 0.0.0.0:* or[::]:* means the service is listening on all network interfaces, including public IPs. - 127.0.0.1:* or[::1]:* means the service is bound strictly to localhost and unreachable from outside. If you see Redis (6379 ), PostgreSQL (5432 ), or MySQL (3306 ) bound to 0.0.0.0 , edit their respective configuration files immediately and set their bind address to 127.0.0.1 . 4. Automated Intrusion Prevention with Fail2ban Even with password authentication disabled, automated bots will flood your SSH port with connection requests, filling up your authentication logs and consuming system resources. Fail2ban monitors system log files (like /var/log/auth.log or systemd-journald ) for repeated failed login attempts and dynamically updates firewall rules to ban the offending IP addresses. Install and Configure Fail2ban $ sudo apt update && sudo apt install fail2ban -y Copy the default configuration to a local override file: $ sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local $ sudo nano /etc/fail2ban/jail.local Configure your global ban policies and enable the SSH jail: [DEFAULT] # Ban hosts for 1 hour after failed attempts bantime = 1h # Window of time to track failures findtime = 10m # Number of failures before triggering a ban maxretry = 4 # Ignore trusted IP addresses (like your office VPN or home static IP) ignoreip = 127.0.0.1/8 ::1 198.51.100.45 [sshd] enabled = true port = ssh filter = sshd maxretry = 3 bantime = 24h Start and enable Fail2ban: $ sudo systemctl enable --now fail2ban Check the status of your SSH jail to see active bans: $ sudo fail2ban-client status sshd Output: Status for the jail: sshd |- Filter | |- Currently failed: 2 | |- Total failed: 48 | - File list: /var/log/auth.log - Actions |- Currently banned: 5 |- Total banned: 14 `- Banned IP list: 185.220.101.5 194.26.29.112 45.154.255.88 ... If you ever accidentally ban yourself, unban your IP from another session with: $ sudo fail2ban-client set sshd unbanip 203.0.113.50 5. Package Management & Automatic Security Patching Unpatched software vulnerabilities are one of the most common vectors for server compromises. Production servers should receive critical security patches automatically without requiring manual sysadmin intervention. Configure Unattended Upgrades (Debian / Ubuntu

Read on DEV Community ↗ ← Back to News

Comments

No comments yet. Start the discussion.