Securing a Debian Server From Default Install to Production-Ready
Problem
A small business had its server exposed to the internet with barely any protection. The system was running with defaults and no firewall was configured. Small companies who don't have a sysadmin have such an common setup like this, which leaves them wide open to attackers.
Did you know: Port 22 is one of the most scanned ports on the internet.
I set up a Debian 13 "trixie" VM for this example on VirtualBox with SSH enabled. After install, this is what I saw:
lain@navi:~$ ss -tuln Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port udp UNCONN 0 0 0.0.0.0:5353 0.0.0.0:* udp UNCONN 0 0 0.0.0.0:44410 0.0.0.0:* udp UNCONN 0 0 [::]:57048 [::]:* udp UNCONN 0 0 [::]:5353 [::]:* tcp LISTEN 0 4096 127.0.0.1:631 0.0.0.0:* tcp LISTEN 0 128 0.0.0.0:22 0.0.0.0:* tcp LISTEN 0 128 [::]:22 [::]:* tcp LISTEN 0 4096 [::1]:631 [::]:* lain@navi:~$ grep -E '^(PermitRootLogin|PasswordAuthentication)' /etc/ssh/sshd_config lain@navi:~$ sudo iptables -L -n Chain INPUT (policy ACCEPT) target prot opt source destination Chain FORWARD (policy ACCEPT) target prot opt source destination Chain OUTPUT (policy ACCEPT) target prot opt source destination lain@navi:~$ systemctl status unattended-upgrades Unit unattended-upgrades.service could not be found.
Explanation: Respectively, command prompts hint that SSH is wide open on port 22, default config allowing root login (it's commented by default), no firewall rules exist, and unattended updates disabled.
Solution
We'll just need to do some basic hardening with a few simple steps:
- Moved SSH to a non-standard port (2222), restarting and testing SSH with ssh -p 2222 lain@navi
- Disabled root SSH login in /etc/ssh/sshd_config
- Enabled a firewall with UFW
- Installed and enabled security updates
So now, we get these outputs using the same commands we've tried before:
lain@navi:~$ ss -tuln Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port udp UNCONN 0 0 0.0.0.0:40623 0.0.0.0:* udp UNCONN 0 0 0.0.0.0:5353 0.0.0.0:* udp UNCONN 0 0 [::]:36504 [::]:* udp UNCONN 0 0 [::]:5353 [::]:* tcp LISTEN 0 128 0.0.0.0:2222 0.0.0.0:* tcp LISTEN 0 4096 127.0.0.1:631 0.0.0.0:* tcp LISTEN 0 128 [::]:2222 [::]:* tcp LISTEN 0 4096 [::1]:631 [::]:* lain@navi:~$ grep -E '^(PermitRootLogin|PasswordAuthentication)' /etc/ssh/sshd_config PermitRootLogin no PasswordAuthentication yes lain@navi:~$ sudo ufw status verbose Status: active Logging: on (low) Default: deny (incoming), allow (outgoing), disabled (routed) New profiles: skip To Action From -- ------ ---- 2222/tcp ALLOW IN Anywhere 2222/tcp (v6) ALLOW IN Anywhere (v6) lain@navi:~$ systemctl status unattended-upgrades ● unattended-upgrades.service - Unattended Upgrades Shutdown Loaded: loaded (/usr/lib/systemd/system/unattended-upgrades.service; enabled; preset: enabled) Active: active (running) since Thu 2025-09-04 12:31:43 CET; 1min 50s ago Invocation: f94b8a91656541c0a6c91892b20ff6f0 Docs: man:unattended-upgrade(8) Main PID: 927 (unattended-upgr) Tasks: 2 (limit: 2297) Memory: 27.2M (peak: 29.2M) CPU: 361ms CGroup: /system.slice/unattended-upgrades.service └─927 /usr/bin/python3 /usr/share/unattended-upgrades/unattended-upgrade-shutdown --wait-for-signal Warning: some journal files were not opened due to insufficient permissions.
We can also add some monitoring to know when things go down. For that we'll deploy my own tiny Lua-based tool servemon.
We start our HTTP server and add it to that servemon:
python3 -m http.server 8080 luajit servemon.lua add localweb http://127.0.0.1:8080
Then we run the monitor once and output when it was up/down:
lain@navi:~/servemon$ luajit servemon.lua add localweb http://127.0.0.1:8080 Site added: localweb http://127.0.0.1:8080 lain@navi:~/servemon$ luajit servemon.lua run --once 2025-09-05 12:23:09 | localweb | http://127.0.0.1:8080 | OK 2025-09-05 12:23:09 | localweb | http://127.0.0.1:8080 | OK 2025-09-05 12:23:09 | localweb | http://127.0.0.1:8080 | OK lain@navi:~/servemon$ luajit servemon.lua run --once 2025-09-05 12:23:44 | localweb | http://127.0.0.1:8080 | DOWN 2025-09-05 12:23:44 | localweb | http://127.0.0.1:8080 | DOWN 2025-09-05 12:23:44 | localweb | http://127.0.0.1:8080 | DOWN lain@navi:~/servemon$ luajit servemon.lua summary === Server Summary Report === 2025-09-05 12:24:10 | localweb | http://127.0.0.1:8080 | DOWN DOWN - localweb (http://127.0.0.1:8080) - Uptime: 65.5% 2025-09-05 12:24:10 | localweb | http://127.0.0.1:8080 | DOWN DOWN - localweb (http://127.0.0.1:8080) - Uptime: 63.3% 2025-09-05 12:24:10 | localweb | http://127.0.0.1:8080 | DOWN DOWN - localweb (http://127.0.0.1:8080) - Uptime: 61.3%
servemon logs results into SQLite (monitor.db) and can send alerts via Telegram or ntfy.sh if desired.