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
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!).
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
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
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
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):
- 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).
- Create a custom rule: description
SSH beispielserver, destination port4022, protocol TCP. - 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. - 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.
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.