Part 2 of Linux security hardening, here I’ll talk about securing SSH, network-facing components, firewall configuration, and some good system maintenance options.

linux-hardening.webp



SSH Server Hardening

Protecting SSH is key to prevent access to a system. Default SSH configurations are designed for convenience, not security. If not protected and exposed, many scripts/bots and will attempt to login to your system.

Disable password-based authentication entirely, restrict access to specific users, and add some basic rate limiting to reduce log noise from automated brute-force attacks.


Disable Password Authentication

Password authentication over SSH is insecure. Passwords can be brute-forced, phished, or leaked in data breaches. SSH key authentication, on the other hand, relies on keypairs which is much more secure.

The private key stays on your local machine and never leaves. The public key gets copied to the server. When you connect, the server challenges your client to prove it has the corresponding private key without ever transmitting the key itself. No password is sent over the network, and there’s nothing for an attacker to brute-force.


Generate an SSH key pair on your local machine (not the server).

# Ed25519 is the modern standard (shorter keys, better performance)w
ssh-keygen -t ed25519 -C "[email protected]"

# If you need compatibility with older systems, use RSA with 4096-bit keys
ssh-keygen -t rsa -b 4096 -C "[email protected]"

# Keys are saved to ~/.ssh/id_ed25519 (private) and ~/.ssh/id_ed25519.pub (public)

Copy your public key to the server.

# The easy way (automatically adds to ~/.ssh/authorized_keys)
ssh-copy-id [email protected]

Test key-based authentication in a “new terminal window” before making changes.

# This should log you in without asking for a password
ssh [email protected]

# If it still asks for a password, check server logs for debugging
ssh -v [email protected]

Once its working, disable password authentication.

# Edit SSH daemon configuration
sudo vim /etc/ssh/sshd_config

# Set these directives to disable password auth and responses
PasswordAuthentication no
ChallengeResponseAuthentication no

# Apply changes
sudo systemctl restart sshd

Disable Root SSH Login

Even with SSH key authentication, allowing direct root login over SSH is unnecessary. If you need root access, log in as your regular user and use sudo. This creates an audit trail showing who ran what command as root, which password-less root login completely bypasses.

I covered disabling the root account entirely in Part 1, but even if you keep root enabled for local console access, you should block it over SSH.

# In /etc/ssh/sshd_config
PermitRootLogin no

# Restart sshd to implement changes
sudo systemctl restart sshd

Some people use PermitRootLogin prohibit-password which allows root login with keys but not passwords. I don’t do this as i find no real reason to SSH login as root.


Restrict SSH Access to Specific Users

By default, any user account on the system can attempt to SSH in. That’s fine for single-user systems, but on multi-user servers you probably only want a few accounts to have remote access.

The AllowUsers and AllowGroups directives let you whitelist exactly who can connect via SSH.

# In /etc/ssh/sshd_config

# Method 1: Whitelist specific users
AllowUsers cloud admin

# Method 2: Whitelist a group
AllowGroups sshusers

# You can combine both directives if needed

If using groups, create the SSH users group and add your authorized users.

# Create SSH users group
sudo groupadd sshusers

# Add your user to the group
sudo usermod -aG sshusers cloud

# Verify group membership
groups cloud

There are also DenyUsers and DenyGroups directives for blacklisting, but I prefer whitelisting with Allow directives. Explicit allow-lists are clearer and harder to misconfigure, much easier to see who has access.


Rate Limit with fail2ban

Fail2ban monitors log files for failed authentication attempts and automatically creates temporary firewall rules to block repeat offenders. This reduces server load from connection attempts and cuts down on log noise from automated SSH brute-force attacks scanning the internet.

It’s not strictly necessary if you’ve already disabled password auth (since there’s nothing to brute-force), but you might still want to run it because it keeps the logs cleaner and reduces wasted CPU cycles from bots trying many failed connections per day.


Install fail2ban.

# Fedora/RHEL
sudo dnf install fail2ban fail2ban-firewalld

# Enable and start service
sudo systemctl enable --now fail2ban

Create a local /etc/fail2ban/jail.local config file.

# Create local override file
sudo vim /etc/fail2ban/jail.local

# Add SSH jail configuration
[DEFAULT]
# Ban duration in seconds (10 minutes)
bantime = 600

# Time window to count failures (10 minutes)
findtime = 600

# Number of failures before ban
maxretry = 5

# Ignore localhost and your trusted IPs
ignoreip = 127.0.0.1/8 ::1 192.168.1.0/24

[sshd]
enabled = true
port = ssh
logpath = /var/log/secure
backend = systemd

If you changed your SSH port from the default 22, update the port.

[sshd]
enabled = true
port = 2222  # Your custom SSH port
logpath = /var/log/secure
backend = systemd

Restart fail2ban to apply the config.

# Restart service
sudo systemctl restart fail2ban

Fail2ban works by parsing authentication logs and creating firewalld rich rules (or iptables rules, depending on your backend configuration). You can see active bans in your firewall.

# View firewalld rich rules created by fail2ban
sudo firewall-cmd --list-rich-rules

Change the Default SSH Port?

I don’t change the default SSH port from 22 because laziness… Changing the port does reduce log noise from automated scans, but someone who really wants access or some advanced bots will figure out when SSH is in another port.

If you’re behind a firewall or using something like Tailscale or Headscale (which I am), the SSH port isn’t even exposed to the public internet anyway. But if you’re running a directly internet-exposed server and want to reduce the constant scanning noise in your logs, here’s how to change it.

# In /etc/ssh/sshd_config
Port 2222  # Choose a high port number (1024-65535)

# Update firewall rules (remove the firewalld service ssh on port 22 and add the custom port)
sudo firewall-cmd --permanent --add-port=2222/tcp
sudo firewall-cmd --permanent --remove-service=ssh
sudo firewall-cmd --reload

# Restart SSH
sudo systemctl restart sshd

The main downside is you have to remember to specify the port every time you connect, which is annoying, but you can also specify it in as a default config your local ~/.ssh/config file.

Host myserver
    HostName your-server.com
    Port 2222
    User cloud

Then you can just run ssh myserver and it automatically uses the configured port and username.



Firewall Configuration

Linux uses firewalld (on RHEL/Fedora) or ufw (on Debian/Ubuntu) as the frontend to netfilter, the kernel’s packet filtering framework. I’m using firewalld, which groups firewall rules into zones and services for easier management compared to iptables.

The default firewall configuration is usually pretty good, but there are some small changes I like to make.


What are Firewalld Zones?

Firewalld uses “zones” to group network interfaces and define trust levels. Each zone has a default policy for incoming traffic. The most commonly used zones are:

  • drop: Drop all incoming packets without reply (most restrictive)
  • block: Reject all incoming packets with icmp-host-prohibited messages
  • public: Default zone for untrusted networks, allows specific services only
  • trusted: Allow all traffic (use carefully, typically for VPN interfaces)

Check your current firewall configuration.

# Show active zones and their interfaces
sudo firewall-cmd --get-active-zones

# Check default zone
sudo firewall-cmd --get-default-zone

# List all rules in current zone
sudo firewall-cmd --list-all

# View all zones and their settings
sudo firewall-cmd --list-all-zones

Restrict your allowed ports and services to only accept traffic that you are expecting. Block everything else.


Managing Firewall Services

Firewalld comes with predefined service definitions for common applications like HTTP, HTTPS, SSH, and DNS. These are easier to work with than remembering port numbers.

# List available predefined services
sudo firewall-cmd --get-services

# Allow a service permanently
sudo firewall-cmd --permanent --add-service=http
sudo firewall-cmd --permanent --add-service=https

# Remove a service
sudo firewall-cmd --permanent --remove-service=dhcpv6-client

# Add a custom port if no predefined service exists
sudo firewall-cmd --permanent --add-port=8080/tcp

# Remove a port
sudo firewall-cmd --permanent --remove-port=8080/tcp

# Reload firewall to apply all changes
sudo firewall-cmd --reload

The --permanent option writes changes to disk so they persist across reboots. Without it, rules only apply until the firewall is reloaded or the system reboots.


ICMP Filtering

ICMP is used for network diagnostics like ping and path MTU discovery. The common security advice is to “disable ping” by blocking ICMP echo-request packets. This prevents someone from easily enumerating active hosts on your network through ping sweeps.

Beware as blocking ICMP can break legitimate network functions, but allowing all ICMP types enables network reconnaissance. So it’s a trade-off like everything in life.


If you want to block ping requests.

# Block ICMP echo-request (incoming pings)
sudo firewall-cmd --permanent --add-icmp-block=echo-request

# Block echo-reply as well (responses to outgoing pings)
sudo firewall-cmd --permanent --add-icmp-block=echo-reply

# Apply changes
sudo firewall-cmd --reload

# Test from another machine
ping your-server.com  # Should timeout

Personally, I don’t block ICMP as troubleshooting network issues without ping is a pain and port scanners don’t rely only on ping.


Enable Firewall Logging

By default, firewalld doesn’t log dropped packets. Enabling logging lets you see what’s being blocked, which is useful for troubleshooting misconfigurations and detecting scanning activity from bots:

# Log all dropped packets
sudo firewall-cmd --set-log-denied=all
sudo firewall-cmd --permanent --set-log-denied=all

# Options: off, all, unicast, broadcast, multicast
# 'unicast' is less noisy than 'all' but still useful

# View dropped packets in system logs
sudo journalctl -f -u firewalld
sudo tail -f /var/log/messages | grep -i REJECT


Automated Security Updates

Apply security updates as soon as they become available with either a systemd timer or dnf-automatic.

Some might not like it but I’m lazy.


DNF Automatic

DNF Automatic is the official tool in Fedora and RHEL-based systems to automate the download and install of packages.

I use it to automatically install security updates while leaving regular feature updates for manual review.


Install DNF Automatic.

# Install package
sudo dnf install dnf-automatic

Configure update behavior in /etc/dnf/automatic.conf.

# Edit configuration
sudo vim /etc/dnf/automatic.conf

# Key settings to modify:
[commands]
# What to do with updates
# apply = download and install
# download = download only
# check = just check for updates
upgrade_type = security
apply_updates = yes

[emitters]
# How to get notified
emit_via = stdio

[email]
# Email configuration if you want email notifications (requires postfix or similar)
email_from = root@localhost
email_to = [email protected]
email_host = localhost

I set upgrade_type = security and apply_updates = yes. This automatically installs security updates but leaves regular feature updates for manual review. You might prefer upgrade_type = default to install everything automatically, which is very convenient.


Enable and start the systemd timer.

# Enable timer (runs daily by default)
sudo systemctl enable --now dnf-automatic.timer

# Check timer status
systemctl status dnf-automatic.timer

# View timer schedule
systemctl list-timers dnf-automatic.timer

# Manually trigger an update check
sudo systemctl start dnf-automatic.service

Check logs to verify it’s working.

# View DNF automatic logs
sudo journalctl -u dnf-automatic.service

# Last run output
sudo journalctl -u dnf-automatic.service -n 50

Flatpak Automatic Updates

If you are using Flatpaks and want automated updates you can use Gnome’s software center app to turn on auto-updates or just create a simple systemd timer to update at regular intervals.

# Create systemd service for Flatpak updates
sudo vim /etc/systemd/system/flatpak-update.service

[Unit]
Description=Update Flatpak packages
After=network-online.target
Wants=network-online.target

[Service]
Type=oneshot
ExecStart=/usr/bin/flatpak update -y

Create a timer to run it.

# Create systemd timer
sudo vim /etc/systemd/system/flatpak-update.timer

[Unit]
Description=Daily Flatpak update

[Timer]
OnCalendar=daily
Persistent=true

[Install]
WantedBy=timers.target

Enable the timer.

# Enable timer
sudo systemctl enable --now flatpak-update.timer


Remove Unused Services

Every running service is a potential attack surface. If you’re not actively using a service, disable it.

Check which services are currently running and enabled:

# List all enabled services (will start on boot)
systemctl list-unit-files --state=enabled

# List currently running services
systemctl list-units --type=service --state=running

Before disabling a service, check what depends on it to make sure you’re not breaking something critical:

# See what would be affected by disabling a service
systemctl list-dependencies --reverse servicename.service

# Check if anything is actively using it
systemctl status servicename.service

Google (or as AI) about unfamiliar services before disabling them. Some services have very weird names but are actually legit. A quick web search for “what is [servicename] Fedora” usually show a lot of info to see if it’s safe to disable.


Disable services you don’t need.

# Disable and stop a service
sudo systemctl disable --now ModemManager.service


What’s Next?

Link to Part 1: Linux Security Hardening Part.1 - LUKS, PAM, and User Lockdown.

Next part should be SELinux, kernel hardening, systemd security…