Search…
Linux from Scratch · Part 15

Linux security basics for sysadmins

In this series (15 parts)
  1. What is Linux and how it differs from other OSes
  2. Installing Linux and setting up your environment
  3. The Linux filesystem explained
  4. Users, groups, and permissions
  5. Essential command line tools
  6. Shell scripting fundamentals
  7. Processes and job control
  8. Standard I/O, pipes, and redirection
  9. The Linux networking stack
  10. Package management and software installation
  11. Disk management and filesystems
  12. Logs and system monitoring
  13. SSH and remote access
  14. Cron jobs and task scheduling
  15. Linux security basics for sysadmins

A fresh Linux server is not secure by default. It accepts password-based SSH logins, has no firewall, does not block brute force attempts, and runs services with more privileges than they need. This article walks through hardening a server step by step, applying the principle of least privilege at every layer.

Prerequisites

This article ties together concepts from the entire Linux series. You should understand users and permissions, SSH, networking and firewalls, processes, and logs.

The principle of least privilege

Every user, process, and service should have only the minimum permissions needed to do its job. No more. This means:

  • Do not use root for daily tasks
  • Do not run services as root unless absolutely necessary
  • Do not open ports you do not need
  • Do not install software you do not need
  • Do not give users more sudo access than they need

When something gets compromised, and eventually something will, least privilege limits the damage. A compromised web server process that runs as www-data with no sudo access can do far less than one running as root.

Step 1: Secure user accounts

Disable root login

# Lock the root account (password login disabled)
sudo passwd -l root

# Verify
sudo passwd -S root

Output:

root L 2026-05-31 0 99999 7 -1

The L means locked. Root can still be accessed via sudo from authorized users.

Configure sudo properly

# Edit sudoers safely (syntax checking)
sudo visudo

Good sudoers configuration:

# Only specific users get sudo
pratik ALL=(ALL:ALL) ALL

# Deploy user can only restart specific services
deploy ALL=(root) NOPASSWD: /usr/bin/systemctl restart nginx, /usr/bin/systemctl restart postgresql

# Require password for sudo (default, keep this)
Defaults    env_reset
Defaults    mail_badpass
Defaults    secure_path="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"

# Log all sudo commands
Defaults    logfile="/var/log/sudo.log"
# Verify sudo configuration
sudo -l

Output:

User pratik may run the following commands on devbox:
    (ALL : ALL) ALL

Remove unnecessary users

# List users with login shells
grep -v "nologin\|false" /etc/passwd

# Lock unused accounts
sudo usermod -L olduser

# Set shell to nologin for service accounts
sudo usermod -s /usr/sbin/nologin serviceaccount

Step 2: Harden SSH

Apply these changes to /etc/ssh/sshd_config:

sudo cp /etc/ssh/sshd_config /etc/ssh/sshd_config.bak

sudo cat > /etc/ssh/sshd_config.d/hardening.conf << 'EOF'
# Disable root login via SSH
PermitRootLogin no

# Key-based auth only
PasswordAuthentication no
PubkeyAuthentication yes

# Disable other auth methods
ChallengeResponseAuthentication no
UsePAM yes

# Limit attempts
MaxAuthTries 3
MaxSessions 3

# Disconnect idle sessions (5 minutes)
ClientAliveInterval 300
ClientAliveCountMax 0

# Only allow specific users
AllowUsers pratik deploy

# Disable X11 forwarding (unless you need it)
X11Forwarding no

# Disable TCP forwarding (unless you need it)
AllowTcpForwarding no

# Log level
LogLevel VERBOSE
EOF

Test and apply:

# Check syntax
sudo sshd -t

# Restart SSH
sudo systemctl restart ssh

# Verify you can still connect (keep current session open!)
ssh pratik@localhost

⚠ Always test SSH changes from a new session while keeping your current session open. If you lock yourself out, the existing session is your lifeline.

Step 3: Configure the firewall

# Reset to defaults
sudo ufw reset

# Default policies
sudo ufw default deny incoming
sudo ufw default allow outgoing

# Allow SSH from trusted networks only
sudo ufw allow from 10.0.0.0/24 to any port 22 proto tcp comment "SSH from office"

# Allow web traffic
sudo ufw allow 80/tcp comment "HTTP"
sudo ufw allow 443/tcp comment "HTTPS"

# Enable
sudo ufw enable

# Verify
sudo ufw status verbose

Output:

Status: active
Logging: on (low)
Default: deny (incoming), allow (outgoing), disabled (routed)

To                         Action      From
--                         ------      ----
22/tcp                     ALLOW IN    10.0.0.0/24                # SSH from office
80/tcp                     ALLOW IN    Anywhere                   # HTTP
443/tcp                    ALLOW IN    Anywhere                   # HTTPS

Step 4: Install and configure fail2ban

fail2ban monitors log files for repeated failed authentication attempts and automatically blocks the offending IP addresses.

# Install
sudo apt install -y fail2ban

# Create local config (don't edit the main file)
sudo cat > /etc/fail2ban/jail.local << 'EOF'
[DEFAULT]
bantime = 3600
findtime = 600
maxretry = 5
banaction = ufw

[sshd]
enabled = true
port = ssh
logpath = /var/log/auth.log
maxretry = 3
bantime = 86400

[nginx-http-auth]
enabled = true
port = http,https
logpath = /var/log/nginx/error.log
maxretry = 5
EOF

Configuration explained:

  • bantime = 86400 blocks the IP for 24 hours (for SSH)
  • findtime = 600 looks at a 10-minute window
  • maxretry = 3 bans after 3 failed SSH attempts
  • banaction = ufw uses ufw to create the block rule
# Start fail2ban
sudo systemctl enable fail2ban
sudo systemctl start fail2ban

# Check status
sudo fail2ban-client status

Output:

Status
|- Number of jail:      2
`- Jail list:   nginx-http-auth, sshd
# Check the SSH jail
sudo fail2ban-client status sshd

Output:

Status for the jail: sshd
|- Filter
|  |- Currently failed: 2
|  |- Total failed:     47
|  `- Journal matches:  _SYSTEMD_UNIT=sshd.service + _COMM=sshd
`- Actions
   |- Currently banned: 3
   |- Total banned:     8
   `- Banned IP list:   203.0.113.5 198.51.100.23 192.0.2.100
# Unban an IP if needed
sudo fail2ban-client set sshd unbanip 10.0.0.50

Step 5: Enable unattended upgrades

Security patches should be applied automatically:

# Install
sudo apt install -y unattended-upgrades

# Enable
sudo dpkg-reconfigure -plow unattended-upgrades

Check the config:

cat /etc/apt/apt.conf.d/50unattended-upgrades | grep -v "^//" | grep -v "^$" | head -20

Output:

Unattended-Upgrade::Allowed-Origins {
    "${distro_id}:${distro_codename}";
    "${distro_id}:${distro_codename}-security";
    "${distro_id}ESMApps:${distro_codename}-apps-security";
    "${distro_id}ESM:${distro_codename}-infra-security";
};
Unattended-Upgrade::Remove-Unused-Dependencies "true";
Unattended-Upgrade::Automatic-Reboot "false";

For production, keep Automatic-Reboot as false and handle reboots during maintenance windows. You can schedule reboot checks as a cron job.

Example 1: Harden a fresh Ubuntu server step by step

Here is a complete hardening script for a fresh Ubuntu server:

#!/bin/bash
set -euo pipefail

echo "=== Ubuntu Server Hardening ==="

# 1. Update everything
echo "[1/8] Updating system packages..."
apt update && apt upgrade -y

# 2. Create admin user (if not exists)
echo "[2/8] Setting up admin user..."
if ! id pratik &>/dev/null; then
    useradd -m -s /bin/bash -G sudo pratik
    echo "Set password for pratik:"
    passwd pratik
fi

# 3. Lock root
echo "[3/8] Locking root account..."
passwd -l root

# 4. Install essential security tools
echo "[4/8] Installing security tools..."
apt install -y ufw fail2ban unattended-upgrades

# 5. Configure firewall
echo "[5/8] Configuring firewall..."
ufw default deny incoming
ufw default allow outgoing
ufw allow 22/tcp
ufw allow 80/tcp
ufw allow 443/tcp
echo "y" | ufw enable

# 6. Configure fail2ban
echo "[6/8] Configuring fail2ban..."
cat > /etc/fail2ban/jail.local << 'JAIL'
[sshd]
enabled = true
maxretry = 3
bantime = 86400
JAIL
systemctl enable fail2ban
systemctl restart fail2ban

# 7. Harden SSH
echo "[7/8] Hardening SSH..."
cat > /etc/ssh/sshd_config.d/hardening.conf << 'SSHD'
PermitRootLogin no
MaxAuthTries 3
ClientAliveInterval 300
ClientAliveCountMax 0
X11Forwarding no
SSHD
sshd -t && systemctl restart ssh

# 8. Enable automatic security updates
echo "[8/8] Enabling unattended upgrades..."
echo 'APT::Periodic::Update-Package-Lists "1";' > /etc/apt/apt.conf.d/20auto-upgrades
echo 'APT::Periodic::Unattended-Upgrade "1";' >> /etc/apt/apt.conf.d/20auto-upgrades

echo ""
echo "=== Hardening complete ==="
echo "Next steps:"
echo "  1. Copy your SSH public key to this server"
echo "  2. Set PasswordAuthentication no in sshd_config"
echo "  3. Restart SSH and verify key-based login works"
echo "  4. Review open ports: sudo ss -tlnp"

⚠ Do not disable PasswordAuthentication until you have confirmed key-based login works. Otherwise you will lock yourself out.

Example 2: Configure fail2ban for SSH with monitoring

Set up fail2ban with email notifications and custom ban rules:

sudo cat > /etc/fail2ban/jail.local << 'EOF'
[DEFAULT]
bantime = 3600
findtime = 600
maxretry = 5
banaction = ufw
destemail = admin@example.com
sender = fail2ban@devbox
mta = sendmail
action = %(action_mwl)s

[sshd]
enabled = true
port = ssh
logpath = /var/log/auth.log
maxretry = 3
bantime = 86400
findtime = 300

[sshd-aggressive]
enabled = true
port = ssh
logpath = /var/log/auth.log
filter = sshd[mode=aggressive]
maxretry = 1
bantime = 604800
findtime = 86400
EOF

sudo systemctl restart fail2ban

The aggressive jail bans after a single failed attempt with invalid usernames, for a full week. This catches automated scanners immediately.

Monitor fail2ban activity:

# Watch bans in real time
sudo tail -f /var/log/fail2ban.log

Output:

2026-05-31 10:15:23,456 fail2ban.actions [1234]: NOTICE  [sshd] Ban 203.0.113.5
2026-05-31 10:15:45,789 fail2ban.actions [1234]: NOTICE  [sshd] Ban 198.51.100.23
# Get a summary of all bans
sudo fail2ban-client status sshd
# Check which IPs are currently banned
sudo ufw status | grep "DENY"

Security checklist

After hardening, verify:

✓ Root login is disabled (passwd -S root shows L) ✓ SSH uses key-based auth only ✓ Firewall is active with minimal open ports ✓ fail2ban is running and monitoring SSH ✓ Unattended upgrades are enabled ✓ No unnecessary services are running (ss -tlnp) ✓ File permissions are correct on sensitive files ✓ Logs are being rotated and monitored

What comes next

This is the last article in the Linux from Scratch series. You now have a solid foundation for working with Linux systems.

To continue your learning, the Cybersecurity from Scratch series builds on these Linux skills and teaches you how attackers think, how to find vulnerabilities, and how to defend systems professionally.

For offensive security techniques that exploit the Linux concepts covered in this series, see Linux privilege escalation.

Future article ideas:

  • SELinux and AppArmor (mandatory access control)
  • Linux kernel hardening (sysctl parameters)
  • Container security (Docker and Kubernetes)
  • Network security monitoring with Suricata/Zeek
Start typing to search across all content
navigate Enter open Esc close