netcup VPS & Root Server Security Hardening Guide (SSH Keys, Fail2ban & Docker UFW Bypass Fix)
- Anti-Lockout Rule: Never close your current SSH window before verifying your new SSH key and custom port in a second terminal window
- Modern Cryptography: Replace legacy RSA keys with compact, high-security Ed25519 SSH keys (ssh-keygen -t ed25519)
- Docker UFW Bypass Trap: Docker manipulates iptables PREROUTING directly, bypassing UFW; always bind internal ports to 127.0.0.1:PORT:PORT
- Debian 12 Fail2ban Fix: Minimal Debian 12 uses systemd-journald instead of /var/log/auth.log; set backend = systemd in jail.local
- Hardware Console Fallback: If locked out by a firewall typo, use SCP Screen (HTML5 VNC) to log in locally and roll back changes
Table of Contents14 sections
Any newly provisioned Linux server connected to a public IPv4 address will be targeted by automated global botnet port scanners and SSH brute-force attacks within 15 minutes of booting.
Leaving TCP port 22 open with password authentication enabled exposes your server to thousands of daily credential-stuffing attempts against root, admin, and ubuntu.
This guide walks through a battle-tested production security hardening workflow for Debian 12 and Ubuntu 24.04, including Debian 12's systemd logging quirks and the notorious Docker UFW firewall bypass vulnerability.
0. The Golden Rule Before Starting: Never Lock Yourself Out#
Whenever modifying SSH configurations or firewall rules, follow two strict habits:
- Keep your current root SSH session open! Never close your active terminal window until you have opened a second, independent terminal window and confirmed you can log in using your new user, SSH key, and custom port.
- Know your out-of-band rescue console: If a firewall rule ever blocks your public connection, log into the netcup Server Control Panel (SCP) and open
Screen(HTML5 VNC Console) to access the server directly as if sitting at a physical monitor.
1. System Updates & Creating a Non-Root Privilege User#
Log in with your initial root credentials, apply all security patches, and create a dedicated administrative user:
# 1. Update package lists and apply all latest OS security patches
apt update && apt upgrade -y
# 2. Install essential security utilities
apt install -y sudo curl wget ufw fail2ban
# 3. Create an administrative user (e.g., deploy) and grant sudo privileges
adduser deploy
usermod -aG sudo deploy
# 4. Verify sudo elevation works properly
su - deploy
sudo whoami # Must output: root
exit
2. Deploying Modern Ed25519 SSH Keys#
Retire legacy RSA-2048 keys and password logins in favor of elliptic-curve Ed25519 keys, which offer superior cryptographic resistance and faster authentication.
1. Generate an Ed25519 Key Pair on Your Local Workstation (macOS / Linux / Windows PowerShell)#
# Generate an Ed25519 key pair (add a strong passphrase when prompted)
ssh-keygen -t ed25519 -C "[email protected]"
This creates two files in ~/.ssh/: id_ed25519 (your private key—never share this) and id_ed25519.pub (your public key).
2. Copy Your Public Key to the Server#
# Run on your local workstation to install your public key into ~/.ssh/authorized_keys
ssh-copy-id -i ~/.ssh/id_ed25519.pub deploy@YOUR_SERVER_IP
3. Hardening the SSH Daemon (sshd_config.d Best Practices)#
Instead of editing the main /etc/ssh/sshd_config file (which can cause merge conflicts during OS upgrades), create a clean drop-in configuration file inside /etc/ssh/sshd_config.d/:
cat << 'EOF' > /etc/ssh/sshd_config.d/99-security.conf
# 1. Move SSH to a custom high port (eliminates 99% of automated port-22 noise)
Port 22222
# 2. Disable direct root login over SSH
PermitRootLogin no
# 3. Disable password authentication completely (enforce SSH keys only)
PasswordAuthentication no
KbdInteractiveAuthentication no
# 4. Enable public key authentication
PubkeyAuthentication yes
# 5. Disconnect idle sessions after 10 minutes of inactivity
ClientAliveInterval 300
ClientAliveCountMax 2
# 6. Restrict SSH access strictly to your administrative user
AllowUsers deploy
EOF
Reload SSH and Verify the Listener#
# Validate syntax before restarting (must return no errors)
sshd -t
# Reload systemd units and restart the SSH service
systemctl daemon-reload
systemctl restart ssh
# Confirm sshd is listening on port 22222
ss -tulpn | grep ssh
Mandatory Verification Checkpoint:
Open a new terminal window on your workstation and test logging in:
ssh -p 22222 deploy@YOUR_SERVER_IP
Do not proceed until you have confirmed passwordless key login works on the new port!
4. Configuring UFW & Fixing the Docker Firewall Bypass Trap#
1. Configure Baseline UFW Rules#
# Deny all incoming connections by default, allow all outbound traffic
ufw default deny incoming
ufw default allow outgoing
# Allow your custom SSH port FIRST
ufw allow 22222/tcp comment 'Custom SSH Port'
# Allow HTTP and HTTPS for web servers / reverse proxies
ufw allow 80/tcp comment 'HTTP'
ufw allow 443/tcp comment 'HTTPS'
ufw allow 443/udp comment 'HTTP/3 QUIC'
# Enable UFW at boot
ufw enable
# Verify active rules
ufw status verbose
2. Critical Security Trap: How Docker Bypasses UFW by Default#
One of the most common causes of compromised databases on self-hosted Linux servers is Docker's interaction with iptables:
- Why It Happens:
By default, the Docker daemon inserts its own DOCKER chain directly into the kernel's iptables PREROUTING and FORWARD tables. Because PREROUTING is evaluated before UFW's INPUT rules, publishing a container port with -p 3306:3306 exposes that port to the entire public internet (0.0.0.0), completely ignoring ufw deny 3306!
- Dangerous Configuration (
docker-compose.yml):
ports:
- "5432:5432" # DANGER: Exposes PostgreSQL on 0.0.0.0:5432 to the public internet!
The Production Fix: Bind Internal Container Ports to 127.0.0.1#
For all databases (PostgreSQL, MySQL/MariaDB, Redis) and internal admin dashboards, explicitly bind host ports to the loopback interface (127.0.0.1):
ports:
- "127.0.0.1:5432:5432" # SECURE: Only reachable locally on the host or via reverse proxy
Only your edge reverse proxy (Caddy, Nginx, or Traefik on ports 80 and 443) should ever bind to 0.0.0.0.
5. Deploying Fail2ban on Debian 12 (systemd Backend Fix)#
On minimal Debian 12 installations, rsyslog is no longer installed by default—authentication logs are managed exclusively by systemd-journald. Without specifying backend = systemd, Fail2ban will fail to start because /var/log/auth.log does not exist.
Create /etc/fail2ban/jail.local#
cat << 'EOF' > /etc/fail2ban/jail.local
[DEFAULT]
# Ban offending IPs for 1 hour (3600 seconds)
bantime = 3600
# Count failures within a 10-minute window
findtime = 600
# Ban after 3 failed authentication attempts
maxretry = 3
# Required on Debian 12 / Ubuntu 24.04 to read from systemd journal
backend = systemd
[sshd]
enabled = true
port = 22222
EOF
# Restart and enable Fail2ban
systemctl restart fail2ban
systemctl enable fail2ban
# Check active SSH jail status
fail2ban-client status sshd
6. Enabling Automated Security Updates (unattended-upgrades)#
Ensure critical kernel and OpenSSL zero-day patches are applied automatically overnight:
apt install -y unattended-upgrades
dpkg-reconfigure -plow unattended-upgrades
7. Final Production Security Checklist#
Before putting your server into production, verify every item below:
- [ ] All OS packages updated via
apt update && apt upgrade -y - [ ] Dedicated non-root
sudouser created - [ ]
Ed25519SSH public key authentication verified - [ ] Default SSH port changed and
PermitRootLogin no+PasswordAuthentication noenforced - [ ] UFW firewall enabled with only SSH,
80, and443exposed publicly - [ ] All Docker database and internal service ports bound strictly to
127.0.0.1 - [ ]
fail2ban-client status sshdshows active monitoring viasystemd - [ ]
unattended-upgradesenabled for automated security patching - [ ] Verified access to SCP
Screen(VNC) for emergency out-of-band recovery