Linux security basics for sysadmins
In this series (15 parts)
- What is Linux and how it differs from other OSes
- Installing Linux and setting up your environment
- The Linux filesystem explained
- Users, groups, and permissions
- Essential command line tools
- Shell scripting fundamentals
- Processes and job control
- Standard I/O, pipes, and redirection
- The Linux networking stack
- Package management and software installation
- Disk management and filesystems
- Logs and system monitoring
- SSH and remote access
- Cron jobs and task scheduling
- 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 = 86400blocks the IP for 24 hours (for SSH)findtime = 600looks at a 10-minute windowmaxretry = 3bans after 3 failed SSH attemptsbanaction = ufwuses 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