Debian Server: Baseline Hardening After Installation

Debian Server: Baseline Hardening After Installation

A freshly installed Debian server receives baseline hardening – with user access, SSH hardening, a firewall, Fail2Ban, and automatic security updates.

Starting Point: Freshly provisioned server (root access, provider default installation)

Goal: Solid baseline configuration for later operation - secure access, keep the system up to date, enable basic protection

Tested: Debian 13 (trixie) (2026-08-08)

All examples use fictional data: server beispielserver.example.com, user anna, mail relay mailrelay.example.com.

Starting Point

Root login via SSH, password authentication, default port 22 - this is how a server arrives fresh from the provider. Before any services are installed, we secure access and bring the system up to a clean state.

1. Create a User and Add an SSH Key

Root remains off-limits for daily access. First, create an admin user:

adduser anna
usermod -aG sudo anna
mkdir -p /home/anna/.ssh
chown -R anna:anna /home/anna/.ssh
chmod 700 /home/anna/.ssh

On your own computer, generate a dedicated key for each server (a passphrase is not required, but recommended):

ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519_beispielserver -C "anna@beispielserver"

Add the public key on the server:

echo "ssh-ed25519 AAAA... anna@beispielserver" >> /home/anna/.ssh/authorized_keys
chmod 600 /home/anna/.ssh/authorized_keys

Test the login (keep the root session open while doing this!):

ssh -i ~/.ssh/id_ed25519_beispielserver anna@beispielserver.example.com
sudo whoami

If it returns root, sudo is configured.

2. Harden SSH

Back up the configuration, then modify it:

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

Set the following in /etc/ssh/sshd_config:

Port 4022
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes

Always test before restarting:

sshd -t
systemctl restart ssh
⚠️ Important

Do not close the old root session until the login on the new port has been confirmed in a second window:

ssh -p 4022 anna@beispielserver.example.com

Only end the old session once this works.

3. Update the System

apt update
apt list --upgradable
apt full-upgrade -y

If a new kernel is included, a reboot follows. Beforehand, check whether rescue access is available from the provider (as a safeguard in case something goes wrong after the reboot).

reboot

After the restart, connect again via SSH and check:

uname -r

4. Set Up the Firewall

apt install -y ufw
ufw default deny incoming
ufw default allow outgoing
ufw allow 4022/tcp comment 'SSH'
ufw show added

Check the rules before enabling them. Then:

ufw enable

Immediately test the login again in a second window (keep the existing session open!).

ℹ️ Info
Debian 13: ufw status can report “active” while systemctl status ufw shows “inactive (dead)” - this is normal; the rules run directly in the kernel. For monitoring scripts, check the ufw status output, not the systemd status.

5. Set Up Fail2Ban

apt install -y fail2ban

Create a separate configuration file; never edit the default file directly:

tee /etc/fail2ban/jail.local << 'EOF'
[DEFAULT]
bantime  = 1h
findtime = 10m
maxretry = 5

[sshd]
enabled = true
port    = 4022
EOF

systemctl restart fail2ban
fail2ban-client status sshd

6. Configure the Mail Relay

For system mail (cron jobs, health checks), a simple relay client is sufficient; there is no need for a full mail server. Example using OpenSMTPD, relaying through an existing mail server:

apt install -y opensmtpd

Store the credentials:

tee /etc/secrets << 'EOF'
relaylabel anna@mailrelay.example.com:DEIN_PASSWORT
EOF
chmod 600 /etc/secrets

Configuration under /etc/smtpd.conf (Debian path, not /etc/mail/):

tee /etc/smtpd.conf << 'EOF'
table aliases file:/etc/aliases
table secrets file:/etc/secrets
table localdomains { "beispielserver.example.com", "beispielserver", "localhost" }

listen on localhost

action "local" maildir alias <aliases>
action "outbound" relay host smtp+tls://relaylabel@mailrelay.example.com:587 auth <secrets> mail-from beispielserver@example.com

match for domain <localdomains> action "local"
match from local for any action "outbound"
EOF

smtpd -n
systemctl restart opensmtpd
🪨 Pitfall

If root is listed in the localdomains table, the system tries to deliver locally to the system user root - this fails on Debian with permission denied. Solution: Redirect root to an external address in /etc/aliases:

tee -a /etc/aliases << 'EOF'
root: admin-beispielserver@example.com
EOF
systemctl restart opensmtpd

For testing, a mail command is required; it is not installed by default on Debian:

apt install -y mailutils

During installation, a Debconf dialog for mail server configuration may appear - select “No configuration” there, since OpenSMTPD is running separately as a relay.

Test it:

echo "Testmail" | mail -s "Testbetreff" root
journalctl -u opensmtpd -n 15 --no-pager
ℹ️ Info
systemctl reload opensmtpd fails with this unit (“Job type reload is not applicable”) - after configuration changes, use systemctl restart opensmtpd.

7. Set Up a Health Check

A daily script that checks disk space, firewall and Fail2Ban status, as well as available updates, and reports the results by email - a basic framework that can be extended with additional metrics (memory, load, services) depending on the server:

tee /usr/local/bin/beispielserver-health.sh << 'SCRIPT'
#!/bin/bash
HOSTNAME=$(hostname)
DATE=$(date '+%Y-%m-%d %H:%M:%S')
STATUS="OK"

DISK=$(df -h / | awk 'NR==2 {print $5}')
UFW=$(ufw status | grep -q "Status: active" && echo active || echo inactive)
FAIL2BAN=$(systemctl is-active fail2ban)
UPDATES=$(apt list --upgradable 2>/dev/null | grep -c upgradable)

[[ "$UFW" != "active" ]] && STATUS="WARN"
[[ "$FAIL2BAN" != "active" ]] && STATUS="WARN"
[[ "$UPDATES" -gt 0 ]] && STATUS="WARN"

REPORT="$HOSTNAME Health Check - $DATE
Disk: $DISK
UFW: $UFW
Fail2Ban: $FAIL2BAN
Updates verfuegbar: $UPDATES
"

echo "$REPORT" | mail -s "[$STATUS] $HOSTNAME Health Check" root
SCRIPT
chmod +x /usr/local/bin/beispielserver-health.sh

As a daily cron job at 06:00:

echo "0 6 * * * root /usr/local/bin/beispielserver-health.sh" | tee /etc/cron.d/beispielserver-health
🪨 Pitfall

Debian 13 does not include cron in the minimal installation. A cron job created under /etc/cron.d/ will therefore be present but never executed - without an error message. Before the first production cron job, check:

which cron
systemctl status cron --no-pager

If the service is missing, install and enable it:

apt install -y cron
systemctl enable --now cron

The cron job will then run as expected.

8. Additionally Enable the Provider Firewall

In addition to the server firewall (ufw), many providers offer an upstream network firewall in their customer panel. This is useful as an additional layer (“Defense in Depth”): traffic is filtered before it reaches the server, reducing the load on the network interface and kernel - and providing a fallback layer if ufw is accidentally misconfigured or disabled.

Procedure in the customer panel (example using a typical provider interface in “Expert” mode):

  1. Disable all supplied default rules that are not needed (Plesk, POP3/IMAP, SMTP, HTTP/HTTPS, FTP, MySQL - on a fresh server, usually none of these are externally accessible).
  2. Create a custom rule: description SSH beispielserver, destination port 4022, protocol TCP.
  3. Before saving, check: The source port remains set to the full client range (e.g., 1024:65535) - this is the port the local computer connects from, not the server port. Only the destination port is restricted to 4022.
  4. Save the change, then immediately test the login in a second window (keep the existing session open!):
ssh -p 4022 anna@beispielserver.example.com

Only close the old session once the login has been confirmed.

🪨 Pitfall
Provider firewalls usually operate on the network in front of the server. An error here can lock out access completely; ufw on the server itself cannot help in that case - the traffic never reaches it. Therefore, always create and test the new rule first before disabling the old one.

Result

Server access secured (key-only, no root login, dedicated port), system up to date, firewall protection provided at two levels (ufw + provider firewall) and Fail2Ban active, mail relay operational, daily health check configured. The foundation for further expansion is in place.

Michael of the Dragons

develops books, software, and open frameworks around technical systems, digital independence, and durable software architectures.
More about Michael →

Open Knowledge Needs People

We develop ideas, share knowledge, and create open-source software and tools. With your support, you help us continue this work for years to come.
Get involved and support our work →
Debian Server: Baseline Hardening After Installation
Previous Article → OpenBSD: Pending Patches in the Daily Health Check