Secure Linux server setup from scratch: SSH hardening, UFW firewall, Fail2Ban, automatic updates, Docker installation, and monitoring — a complete guide for Ubuntu and Debian in 2026.
Setting up a secure Linux server is the first thing you do after provisioning — before deploying applications, before opening extra ports, before anything else. A fresh Ubuntu or Debian VPS is exposed the moment it goes online: brute-force bots hit new IPs within minutes, scanning for default credentials and known vulnerabilities. This guide walks through the essential steps to take a bare server from vulnerable to production-ready, with real commands and config files you can use on Ubuntu 22.04+ and Debian 12+.
Why the First Hour Matters Most
Most compromised servers are breached within the first 24 hours. Automated scanners constantly probe IP ranges for open SSH ports with password authentication, unpatched services, and default configurations. I have seen auth.log files with thousands of failed login attempts within minutes of a new VPS going live.
The fix is methodical: update packages, lock down SSH, enable a firewall, then automate maintenance. Each step builds on the previous one, and the order matters — if you enable the firewall before allowing SSH, you lock yourself out. If you disable password auth before uploading your SSH key, same result. Follow this guide in sequence.
Initial System Updates and Essential Packages
Connect to your server with the root credentials from your provider (Hetzner, DigitalOcean, Linode, Vultr). The first commands close known CVEs in the base system:
sudo apt update && sudo apt upgrade -y
sudo apt install -y ufw fail2ban unattended-upgrades curl
These four packages form the security foundation: ufw for the firewall, fail2ban for brute-force protection, unattended-upgrades for automatic security patches, and curl for testing connectivity later. On a minimal server image, some of these may already be installed — apt install handles that gracefully.
Should I create a non-root user before doing anything else?
Yes — but not before updating. Running as root for the initial update and package install is fine. Immediately after, create a dedicated admin user so you have a proper audit trail for all future actions:
adduser deploy
usermod -aG sudo deploy
Test that deploy can run sudo commands in a separate terminal before proceeding. From this point forward, work as deploy and use sudo when you need elevated privileges.
SSH Hardening — Locking Down Remote Access
SSH is the primary attack surface on any Linux server. The default configuration allows password authentication, which is trivially brute-forced. Switch to key-based authentication and restrict the daemon configuration.
How do I set up SSH key authentication on a new server?
On your local machine, generate an ed25519 key if you don't have one. Ed25519 is fast, secure, and has short key strings:
ssh-keygen -t ed25519 -C "[email protected]" -f ~/.ssh/id_ed25519_server -N ""
Copy the public key to your server. Replace deploy and the IP with your values:
ssh-copy-id -i ~/.ssh/id_ed25519_server.pub [email protected]
Open a second terminal and confirm key-based login works before touching the SSH config. This is critical — if key login fails after you disable passwords, you're locked out.
What should my sshd_config look like for production?
Edit /etc/ssh/sshd_config with sudo nano /etc/ssh/sshd_config and set these values:
# /etc/ssh/sshd_config
PermitRootLogin no
PubkeyAuthentication yes
PasswordAuthentication no
PermitEmptyPasswords no
MaxAuthTries 3
ClientAliveInterval 300
ClientAliveCountMax 2
X11Forwarding no
AllowUsers deploy
PermitRootLogin no forces access through a named user — every action is now traceable. AllowUsers deploy rejects connections for any other username before authentication even begins. MaxAuthTries 3 limits brute-force attempts per connection.
Validate and apply:
sudo sshd -t && sudo systemctl restart sshd
The sshd -t test catches syntax errors. If it fails, fix the config before restarting — a broken restart can drop your connection. On some Ubuntu versions the service is named ssh instead of sshd; check with systemctl status ssh. Keep your current session open until you verify a new login works in a separate terminal.
Firewall Configuration with UFW
A firewall blocks everything you don't explicitly allow. UFW (Uncomplicated Firewall) is the standard interface for iptables on Ubuntu and Debian, and the syntax maps directly to intent.
What is the minimum UFW configuration for a web server?
Default-deny all incoming traffic, then open only what you need. For a server running a web application, that means SSH, HTTP, and HTTPS — nothing else:
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp comment 'SSH'
sudo ufw allow 80/tcp comment 'HTTP'
sudo ufw allow 443/tcp comment 'HTTPS'
sudo ufw --force enable
Verify with sudo ufw status verbose. The comment flag documents why each rule exists — invaluable when you revisit this server in six months. Do not open additional ports until you have a service actively listening. If you later move SSH to a non-standard port, add the new rule before removing port 22.
For more advanced firewall patterns and rate limiting, see the VPS security guide which covers UFW rate limiting for SSH brute-force protection.
Fail2Ban for Brute-Force Protection
How does Fail2Ban protect my server?
Fail2Ban monitors log files in real time and temporarily bans IP addresses that show malicious behavior — repeated failed logins, suspicious patterns, or known exploit signatures. For SSH, it watches /var/log/auth.log and adds iptables rules to block offending IPs. Even with key-only SSH, Fail2Ban reduces log noise and blocks scanners from wasting server resources.
Create a local override at /etc/fail2ban/jail.local (never edit jail.conf directly — it gets overwritten on updates):
# /etc/fail2ban/jail.local
[DEFAULT]
bantime = 3600
findtime = 600
maxretry = 3
[sshd]
enabled = true
port = ssh
filter = sshd
logpath = /var/log/auth.log
This bans an IP for one hour (3600 seconds) after three failed attempts within 10 minutes. For repeat offenders, you can enable bantime.increment = true in the [DEFAULT] section to double the ban duration each time. Restart and verify:
sudo systemctl restart fail2ban
sudo fail2ban-client status sshd
The status command shows currently banned IPs and the total ban count. I typically see 20-50 bans per day on a public-facing server — all automated scanners that never get past the first three attempts.
Automatic Security Updates with Unattended-Upgrades
Can I safely enable automatic updates on a production server?
Yes — if you limit them to security patches. The unattended-upgrades package on Ubuntu and Debian installs updates from the -security origin only by default, which means you get CVE fixes without unexpected feature changes or breaking updates.
sudo dpkg-reconfigure -plow unattended-upgrades
Select "Yes" when prompted. The configuration lives at /etc/apt/apt.conf.d/50unattended-upgrades. Key settings to review:
# /etc/apt/apt.conf.d/50unattended-upgrades
Unattended-Upgrade::Automatic-Reboot "false";
Unattended-Upgrade::Remove-Unused-Dependencies "true";
Unattended-Upgrade::Mail "[email protected]";
I keep Automatic-Reboot set to "false" — kernel updates that require a reboot can wait for a scheduled maintenance window. Remove-Unused-Dependencies cleans up old packages automatically. The Mail directive sends you a summary of what was installed, so you stay informed without logging in daily.
Check that it's working after a day or two by reviewing the log:
sudo cat /var/log/unattended-upgrades/unattended-upgrades.log
Docker Setup for Containerized Deployments
If you are deploying containerized applications, install Docker using the official repository — not the docker.io package from Ubuntu's repos, which is often several versions behind:
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker deploy
sudo systemctl enable docker
Log out and back in for the group change to take effect. Verify with docker run hello-world.
What Docker security practices should I follow?
Run containers as non-root users whenever possible. Limit container capabilities with --cap-drop=ALL and add back only what the application needs. Never expose database or admin ports to the host — use Docker Compose networks to isolate services. Set resource limits (CPU, memory) so a single container can't starve the host. For a complete production Docker setup, see my Docker Compose in production guide.
Monitoring and Ongoing Maintenance
A secure server without monitoring is a server you hope is still secure. At minimum, set up basic health checks:
- Check
sudo fail2ban-client status sshdweekly for ban patterns - Review open ports with
sudo ss -tlnpafter any change - Monitor disk space — full disks break logging and backups
- Verify
unattended-upgradeslogs for successful security patches
What should I monitor on a Linux server?
For a single server, start with the essentials: uptime (is it reachable?), disk space (will logs fill it?), and authentication failures (is someone probing?). A simple cron job that checks these and sends an alert on failure is better than no monitoring at all. For a more complete setup with dashboards and alerting, Prometheus with Grafana is the standard — both can run as Docker containers alongside your application stack.
For a deeper dive into host-level security beyond this initial setup, follow the Linux server hardening checklist which covers kernel tuning, audit logging, SUID auditing, and CIS benchmark alignment.