Linux Server Security Baseline: What Every DevOps Engineer Should Apply

Linux Server Security Baseline: What Every DevOps Engineer Should Apply

· 14 min read
linux

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

  1. Update the system — patch known CVEs before anything else goes on the box
  2. Create a non-root sudo user — eliminate direct root exposure over SSH
  3. Set up SSH key authentication — replace passwords with cryptographic keys
  4. Disable root login and password auth — lock down the SSH daemon
  5. Change the SSH port — reduce automated scanner noise (+ when NOT to do it)
  6. Configure UFW firewall — default-deny all incoming traffic
  7. 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.

💡 Test Before You Trust

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
Don't Lock Out Root Yet

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 "[email protected]"

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"
Cloud Instances — Key Injection at Launch

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
DirectiveEC2 Debian 13 DefaultWhy set explicitly
PermitRootLogin noprohibit-password — root login allowed with a keyno eliminates root SSH access entirely, regardless of what keys exist
PasswordAuthentication noAlready no on EC2Keeps it locked even if a package update or dpkg-reconfigure resets defaults
PubkeyAuthentication yesyes by defaultSelf-documenting — makes the intent clear to anyone reading the config
LogLevel VERBOSEINFOLogs the key fingerprint used on each login — essential for auditing who authenticated and when

Restart the SSH service:

sudo systemctl restart sshd
Do Not Close Your Current Session

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:

ScenarioWhy to Keep Port 22
Behind a VPNThe VPN is the perimeter — port obscurity adds nothing when the server is unreachable without it
AWS SSM / GCP OS LoginNo open SSH port needed at all — Session Manager (AWS) or OS Login (GCP) gives shell access without it
Kubernetes nodesCluster tooling often assumes port 22 — changing it can break node provisioning and management
Bastion / jump host setupThe bastion controls who gets in; internal servers behind it can stay on 22
Strict corporate Security GroupsSome orgs lock outbound rules to port 22 only — a non-standard port may be blocked at the network level
Port Change is About Noise Reduction, Not Security

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
UFW Rule Comes Next

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.

💡 Document Your Port

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

ActionCommand
Allow HTTPsudo ufw allow 80/tcp
Allow HTTPSsudo ufw allow 443/tcp
Allow from specific IP onlysudo ufw allow from 10.0.0.5 to any port 2222
Delete a rulesudo ufw delete allow 80/tcp

Only open what you actually use. Every open port is attack surface.

Allow SSH Before Enabling UFW

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
Fail2ban Deep Dive

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

StepWhat It Does
System updates + unattended-upgradesPatches known CVEs automatically
Non-root sudo userNo direct root exposure over SSH
SSH key authenticationEliminates password brute force
Root login disabled, password auth offForces key-only access
Custom SSH portRemoves automated port-22 scanner noise
UFW default-denyOnly explicitly allowed traffic gets through
Fail2ban on SSHBans 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.

What You Need
  • AWS Console access
  • An EC2 launched with:
    • Image: Debian 13
    • Security group: inbound SSH (port 22) allowed
    • Key pair: .pem file 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
Safe for Most Servers — Think Before Applying to Critical Services

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
Update the AWS Security Group Before Restarting sshd

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
Going Deeper with Fail2ban

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 AppliedState
System fully patched, auto-security-updates onDone
Non-root admin user (AWS default)Already present
SSH key auth only — key injected at launchAlready present
Root SSH login disabled, password auth offConfigured
SSH moved off port 22 → 2233Configured
UFW default-deny, port 2233 openConfigured
Fail2ban watching SSH port 2233Running

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.log and fail2ban logs to a SIEM or log aggregator so you have visibility without SSH-ing in
  • Fail2ban production configFail2ban on Debian 13: The Right Config for OpenSSH 9.x