Every time you provision a Linux server — an EC2 instance, a GCP VM, a DigitalOcean droplet, a bare-metal box — it ships with the same insecure defaults: root login enabled, port 22 wide open, password auth on. Bots will find it within hours.
These seven steps are what I run on every new Debian or Ubuntu server before anything else goes on it.
Environment: Debian / Ubuntu, using apt.
What We’ll Cover
- Update the system — patch known CVEs before anything else goes on the box
- Create a non-root sudo user — eliminate direct root exposure over SSH
- Set up SSH key authentication — replace passwords with cryptographic keys
- Disable root login and password auth — lock down the SSH daemon
- Change the SSH port — reduce automated scanner noise (+ when NOT to do it)
- Configure UFW firewall — default-deny all incoming traffic
- Install Fail2ban — auto-ban IPs that brute-force your SSH port
Step 1: Update the System
Patch everything before you do anything else. Newly provisioned images are often weeks behind on security updates.
sudo apt update && sudo apt full-upgrade -y
sudo reboot now
Reboot if a kernel update was applied — you want the running kernel to match what was just patched.
Enable Automatic Security Updates
Manual updates get skipped. unattended-upgrades handles security patches automatically so you do not have to remember.
sudo apt install -y unattended-upgrades apt-listchanges
sudo dpkg-reconfigure --priority=low unattended-upgrades
Answer Yes when prompted. This enables automatic installs for the Debian-Security origin only — conservative and safe.
Run sudo unattended-upgrade --dry-run --debug to confirm what would be installed without applying anything. Good sanity check after setup.
Step 2: Create a Non-Root Sudo User
Logging in as root directly is risky. One compromised session, one fat-finger command, and you have unrestricted access to the entire system. Create a dedicated user and use sudo only when elevated privileges are actually needed.
sudo adduser deploy
sudo usermod -aG sudo deploy
Replace deploy with whatever username you prefer. Verify:
groups deploy
# deploy : deploy sudo
Keep the root account intact. You will disable root SSH login in step 4, but you still need root for local or cloud console access if something goes wrong.
Step 3: Set Up SSH Key Authentication
Password-based SSH is one brute-force attack away from being compromised. Key authentication eliminates that risk entirely — and it is faster to use day-to-day.
Generate a Key Pair on Your Local Machine
Run this on your local machine, not the server:
ssh-keygen -t ed25519 -C "your-email@example.com"
ed25519 is the modern default — smaller keys, faster operations, equivalent security to RSA 4096. Set a strong passphrase.
Copy the Public Key to the Server
ssh-copy-id -i ~/.ssh/id_ed25519.pub deploy@your-server-ip
If ssh-copy-id is not available locally:
cat ~/.ssh/id_ed25519.pub | ssh deploy@your-server-ip \
"mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys"
AWS, GCP, and most cloud providers inject your SSH public key at instance launch. If you used that flow, your key is already in ~/.ssh/authorized_keys for the default user (ubuntu, admin, ec2-user, etc.). Verify it is there before proceeding.
Verify Key Login Works
Open a new terminal and confirm key-based login succeeds before touching anything else:
ssh -i ~/.ssh/id_ed25519 deploy@your-server-ip
Do not proceed to the next step until this works.
Step 4: Disable Root SSH Login and Password Auth
With key login confirmed, lock down the SSH daemon.
EC2 Debian 13 already ships with PasswordAuthentication no explicitly set, but the other directives are either commented out or rely on compiled defaults that are not strict enough. Set them all explicitly so your intent is clear and survives package updates.
sudo nano /etc/ssh/sshd_config
Find and set (or add) these four directives:
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
LogLevel VERBOSE
| Directive | EC2 Debian 13 Default | Why set explicitly |
|---|---|---|
PermitRootLogin no | prohibit-password — root login allowed with a key | no eliminates root SSH access entirely, regardless of what keys exist |
PasswordAuthentication no | Already no on EC2 | Keeps it locked even if a package update or dpkg-reconfigure resets defaults |
PubkeyAuthentication yes | yes by default | Self-documenting — makes the intent clear to anyone reading the config |
LogLevel VERBOSE | INFO | Logs the key fingerprint used on each login — essential for auditing who authenticated and when |
Restart the SSH service:
sudo systemctl restart sshd
Keep your existing SSH session open while restarting sshd. Open a second terminal and verify you can still connect before closing anything. If something is wrong, your current session is your lifeline back in.
Step 5: Change the SSH Port
Port 22 is the default, and every bot scanner knows it. Changing it does not stop a targeted attack, but it eliminates virtually all automated noise.
When NOT to Change the SSH Port
Changing the port is a good default, but skip this step if any of the following applies:
| Scenario | Why to Keep Port 22 |
|---|---|
| Behind a VPN | The VPN is the perimeter — port obscurity adds nothing when the server is unreachable without it |
| AWS SSM / GCP OS Login | No open SSH port needed at all — Session Manager (AWS) or OS Login (GCP) gives shell access without it |
| Kubernetes nodes | Cluster tooling often assumes port 22 — changing it can break node provisioning and management |
| Bastion / jump host setup | The bastion controls who gets in; internal servers behind it can stay on 22 |
| Strict corporate Security Groups | Some orgs lock outbound rules to port 22 only — a non-standard port may be blocked at the network level |
Changing the SSH port is not a security control — it is noise reduction. A port scan will find it. The real controls are key-only auth (step 3), Fail2ban (step 7), and network-level restrictions like Security Groups or a VPN.
Edit sshd_config
sudo nano /etc/ssh/sshd_config
Change or add the Port directive:
Port 2222
Restart sshd:
sudo systemctl restart sshd
Port 2222 is not yet open in the firewall — UFW is configured in the next step. If you already have UFW active from a prior setup, run sudo ufw allow 2222/tcp now before restarting. For a fresh install, continue — Step 6 will open the port as part of the firewall setup.
Connect Using the New Port
ssh -p 2222 deploy@your-server-ip
Once confirmed, update ~/.ssh/config locally so you do not have to type the port every time:
Host myserver
HostName your-server-ip
User deploy
Port 2222
IdentityFile ~/.ssh/id_ed25519
Then: ssh myserver.
Store the custom SSH port in a password manager or team secrets vault. Easy to forget across multiple servers.
Step 6: Configure UFW Firewall
A default-deny firewall means only traffic you explicitly allow can reach the server. Get in the habit of documenting which ports you open and why — it forces you to understand exactly what is running, prevents ports from accumulating over time, and cuts down the attack surface to only what is necessary.
sudo apt install -y ufw
Set the baseline policy:
sudo ufw default deny incoming
sudo ufw default allow outgoing
Allow your SSH port:
sudo ufw allow 2222/tcp
Enable:
sudo ufw enable
Verify:
sudo ufw status verbose
Expected output:
Status: active
Default: deny (incoming), allow (outgoing)
To Action From
-- ------ ----
2222/tcp ALLOW IN Anywhere
2222/tcp (v6) ALLOW IN Anywhere (v6)
Common Rules Reference
| Action | Command |
|---|---|
| Allow HTTP | sudo ufw allow 80/tcp |
| Allow HTTPS | sudo ufw allow 443/tcp |
| Allow from specific IP only | sudo ufw allow from 10.0.0.5 to any port 2222 |
| Delete a rule | sudo ufw delete allow 80/tcp |
Only open what you actually use. Every open port is attack surface.
Always add your SSH port rule before running ufw enable. Enabling UFW with no SSH rule will immediately cut your connection.
Step 7: Install Fail2ban
UFW controls which ports are reachable. Fail2ban watches authentication logs and bans IPs that are actively probing them.
sudo apt install -y fail2ban
Create a minimal jail.local — do not copy jail.conf wholesale, it pulls in jails for services that don’t exist on your system and will crash fail2ban on startup:
sudo nano /etc/fail2ban/jail.local
[DEFAULT]
ignoreip = 127.0.0.1/8 ::1 <your-ip>
[sshd]
enabled = true
port = 2222
backend = systemd
journalmatch = _SYSTEMD_UNIT=ssh.service
bantime = 1h
findtime = 10m
maxretry = 5
Start and enable:
sudo systemctl enable --now fail2ban
sudo fail2ban-client status sshd
There is more to get right with Fail2ban on Debian 13 — OpenSSH 9.x splits session handling into a separate sshd-session process, which breaks the default journalmatch filter. Fail2ban on Debian 13: The Right Config for OpenSSH 9.x covers the correct config, testing it against real wrong-key attempts, and tuning ban times for production.
What You Have Now
| Step | What It Does |
|---|---|
| System updates + unattended-upgrades | Patches known CVEs automatically |
| Non-root sudo user | No direct root exposure over SSH |
| SSH key authentication | Eliminates password brute force |
| Root login disabled, password auth off | Forces key-only access |
| Custom SSH port | Removes automated port-22 scanner noise |
| UFW default-deny | Only explicitly allowed traffic gets through |
| Fail2ban on SSH | Bans IPs that actively probe your SSH port |
This is a baseline, not a complete hardening spec. The next layers: auditing with Lynis, AppArmor profile tuning, centralized log forwarding, and rootkit detection with rkhunter.
Practical Walkthrough: AWS EC2 + Debian 13
This section walks through applying every step above on a real AWS EC2 running Debian 13.
- AWS Console access
- An EC2 launched with:
- Image: Debian 13
- Security group: inbound SSH (port 22) allowed
- Key pair:
.pemfile downloaded locally
Connect to the Instance
chmod 400 <path-to-private-key>
ssh -i <path-to-private-key> admin@<ec2-public-ip>
On first connection SSH asks you to verify the host fingerprint — type yes:
The authenticity of host '<ec2-public-ip>' can't be established.
ED25519 key fingerprint is SHA256:rMAZNXzm4.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '<ec2-public-ip>' (ED25519) to the list of known hosts.
Fix Locale Warnings (EC2-Specific)
A fresh Debian 13 EC2 greets you with a wall of locale warnings on login. Run the reconfiguration tool and select en_US.UTF-8 UTF-8 when prompted:
sudo dpkg-reconfigure locales
The perl: warning: Setting locale failed lines at the start are expected — they appear because the locale isn’t configured yet when the tool runs. The last lines confirm success:
Generating locales (this might take a while)...
en_US.UTF-8... done
Generation complete.
Exit and reconnect — the warnings are gone.
Step 1: Update the System
sudo apt update && sudo apt full-upgrade -y
sudo reboot now
After the reboot, reconnect and confirm everything is up to date:
sudo apt update
All packages are up to date.
Then enable automatic security updates — answer Yes when prompted:
sudo apt install -y unattended-upgrades apt-listchanges
sudo dpkg-reconfigure --priority=low unattended-upgrades
unattended-upgrades only applies security patches from the Debian-Security origin, not all package upgrades. For most servers this is the right default. For servers running databases or version-sensitive middleware, review whether automatic reboots (for kernel updates) fit your maintenance window policy.
Step 2: Non-Root Sudo User
AWS Debian 13 images ship with a non-root admin user already configured. This step is done — no action needed.
Step 3: SSH Key Authentication
The key pair was assigned at EC2 launch and is already in ~/.ssh/authorized_keys for the admin user. This step is done — verify it is there before proceeding:
cat ~/.ssh/authorized_keys
Steps 4 & 5: Lock Down SSH and Change the Port
sudo nano /etc/ssh/sshd_config
Set these four directives:
Port 2233
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
LogLevel VERBOSE
Add a new inbound TCP rule for port 2233 in the EC2 Security Group before restarting sshd. If you restart first, you will be locked out.
Restart the SSH service:
sudo systemctl restart sshd
Open a new terminal and verify the new port works before closing the existing session:
ssh -i "key.pem" admin@<ec2-public-ip> -p 2233
Step 6: Configure UFW Firewall
Follow the UFW setup commands from the guide above, using port 2233 instead of 2222. Once configured:
sudo ufw status verbose
Status: active
Default: deny (incoming), allow (outgoing), disabled (routed)
To Action From
-- ------ ----
2233/tcp ALLOW IN Anywhere
2233/tcp (v6) ALLOW IN Anywhere (v6)
Step 7: Install Fail2ban
Follow the Fail2ban setup commands from the guide above — set port = 2233 in jail.local to match the SSH port used in this walkthrough. Then verify the jail is active:
sudo fail2ban-client status sshd
The default Fail2ban setup silently misses SSH failures on Debian 13 — two non-obvious bugs mean the jail shows zero failures even while your server is being probed. Fail2ban on Debian 13: The Right Config for OpenSSH 9.x covers the correct config, how to verify it against real wrong-key attempts, and tuning for production.
Wrapping Up
At this point the EC2 instance is hardened to the same baseline covered in the guide:
| What Was Applied | State |
|---|---|
| System fully patched, auto-security-updates on | Done |
Non-root admin user (AWS default) | Already present |
| SSH key auth only — key injected at launch | Already present |
| Root SSH login disabled, password auth off | Configured |
| SSH moved off port 22 → 2233 | Configured |
| UFW default-deny, port 2233 open | Configured |
| Fail2ban watching SSH port 2233 | Running |
This is a solid baseline for a public-facing server, but it is not a complete hardening spec. The next logical steps:
- Lynis — full system audit (
sudo lynis audit system) with scored recommendations - AppArmor — profile mandatory access controls on high-risk services
- rkhunter — rootkit and integrity scanning
- Centralized log forwarding — ship
/var/log/auth.logand fail2ban logs to a SIEM or log aggregator so you have visibility without SSH-ing in - Fail2ban production config — Fail2ban on Debian 13: The Right Config for OpenSSH 9.x