On this page
Any server with a public IP is probed for SSH logins and open services around the clock. The checklist below is the baseline to apply to a fresh Ubuntu 24.04 LTS or Debian 12/13 server before installing anything else. It takes about 15 minutes, and if a step goes wrong, the out-of-band console still gets you in.
Run the commands as root, or with sudo once your user exists. Replace deploy with your user name and 203.0.113.10 with your server's IP address.
| Step | Time |
|---|---|
| Updates and automatic security updates | 3 min |
| Sudo user with your SSH key | 2 min |
| Key-only SSH, no root login | 2 min |
| Firewall baseline | 2 min |
| fail2ban | 1 min |
| Time sync and service review | 2 min |
| Monitoring, IPMI hygiene and backups | 3 min |
Update everything
Start from current packages and reboot once, since the first upgrade on a new image often includes a kernel:
apt update && apt full-upgrade -y
rebootUbuntu Server enables unattended-upgrades for security updates out of the box. On Debian, install and enable it:
apt install -y unattended-upgrades
dpkg-reconfigure -plow unattended-upgrades # answer Yes
unattended-upgrade --dry-run --debug 2>&1 | tail -n 5The reconfigure step writes /etc/apt/apt.conf.d/20auto-upgrades. If kernel updates should reboot the server at a quiet hour, add a local override instead of editing the packaged file:
cat > /etc/apt/apt.conf.d/52unattended-upgrades-local <<'EOF'
Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-Time "04:00";
EOFCreate a sudo user with your SSH key
On your workstation, create a key if you do not have one:
ssh-keygen -t ed25519 -C "you@workstation"Protect the private key with a passphrase and load it into ssh-agent so you type it once per session. If you own a FIDO2 security key, ssh-keygen -t ed25519-sk creates a key that only works with the hardware token plugged in.
On the server, create the user and grant sudo. On a minimal Debian install, run apt install sudo first.
adduser deploy
usermod -aG sudo deployIf you logged in as root with a key, copy it to the new account:
install -d -m 700 -o deploy -g deploy /home/deploy/.ssh
install -m 600 -o deploy -g deploy /root/.ssh/authorized_keys /home/deploy/.ssh/authorized_keysIf you logged in with a password, run ssh-copy-id deploy@203.0.113.10 from your workstation instead; the SSH access documentation covers both cases. Then open a second terminal and confirm that ssh deploy@203.0.113.10 works and that sudo -v accepts your password. Keep the root session open until the next step is verified.
Allow keys only and block root logins
On Ubuntu 24.04 and Debian, /etc/ssh/sshd_config includes /etc/ssh/sshd_config.d/*.conf near the top, and for each setting the first value read wins. A file whose name sorts first therefore overrides later ones, including 50-cloud-init.conf on images that enable passwords:
cat > /etc/ssh/sshd_config.d/00-hardening.conf <<'EOF'
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes
MaxAuthTries 3
X11Forwarding no
AllowUsers deploy
EOF
sshd -t && systemctl restart ssh
sshd -T | grep -Ei 'permitrootlogin|passwordauthentication|allowusers'Test a fresh login from another terminal before closing anything. If you do lock yourself out, the KVM console in the out-of-band tools and rescue mode still get you in.
Moving SSH to another port cuts log noise but is not a security control. If you do it on Ubuntu 24.04, which starts SSH through ssh.socket, run systemctl daemon-reload and restart ssh.socket afterwards.
Firewall baseline
Pick one tool and do not run both. UFW is the simplest on Ubuntu and Debian; nftables suits people who prefer explicit rules.
UFW
apt install -y ufw
ufw default deny incoming
ufw default allow outgoing
ufw limit 22/tcp
ufw allow 80,443/tcp
ufw enable
ufw status verboselimit denies a source address that opens 6 or more connections within 30 seconds. UFW applies the same rules to IPv6.
nftables
The same policy in /etc/nftables.conf:
flush ruleset
table inet filter {
chain input {
type filter hook input priority filter; policy drop;
ct state established,related accept
ct state invalid drop
iif "lo" accept
ip protocol icmp accept
meta l4proto ipv6-icmp accept
tcp dport { 22, 80, 443 } accept
}
chain forward {
type filter hook forward priority filter; policy drop;
}
}nft -c -f /etc/nftables.conf && systemctl enable --now nftables
nft list rulesetnft -c checks the syntax before anything is applied. Accepting ICMPv6 keeps IPv6 neighbor discovery working; without it, IPv6 breaks.
fail2ban
fail2ban bans addresses that keep failing authentication. Reading the systemd journal works on both distributions, including Debian systems without /var/log/auth.log:
apt install -y fail2ban python3-systemd
cat > /etc/fail2ban/jail.local <<'EOF'
[DEFAULT]
bantime = 1h
findtime = 10m
maxretry = 5
ignoreip = 127.0.0.1/8 ::1 198.51.100.7
[sshd]
enabled = true
backend = systemd
EOF
systemctl restart fail2ban
fail2ban-client status sshdPut your own office or VPN address in ignoreip. With key-only SSH, fail2ban mainly cuts log noise and wasted handshakes; it matters more for services that still accept passwords, such as mail or web logins, which have their own jails.
Time sync
Correct time matters for TLS, logs, and anything that uses tokens or certificates:
timedatectl
timedatectl set-timezone UTCLook for System clock synchronized: yes. Both distributions normally sync with systemd-timesyncd. If NTP service shows n/a, install it, or install chrony, which replaces it and adds chronyc tracking for diagnostics. UTC keeps logs comparable across servers.
Keep services to a minimum
Every listening port is attack surface. See what is listening and what is running:
ss -tulpn
systemctl list-units --type=service --state=runningStop what you do not need with systemctl disable --now NAME and remove packages you will never use. Services that only talk to other servers, such as databases, metrics exporters and admin panels, should listen on 127.0.0.1 or on your private network, never on the public address.
Check from the outside
Rules on paper and rules in effect can differ. From another machine, scan the server and compare the open ports with the list you intended:
nmap -Pn -p- 203.0.113.10 # every TCP port; takes a few minutesAnything open that you did not plan for is either a service to bind privately or a firewall rule to fix. FlexDedi's always-on DDoS protection filters attack traffic before it reaches the server, but it does not decide which ports are open; the host firewall does.
Monitoring basics
- Disk health: install
smartmontoolsandnvme-cli.smartdthen watches the drives, andnvme smart-log /dev/nvme0shows wear (percentage_used) and media errors. If you use software RAID, set up alerts as described in software RAID with mdadm. - Metrics:
apt install prometheus-node-exporterserves CPU, memory, disk and network metrics on port 9100. Allow that port only from your monitoring host, for exampleufw allow from 10.0.0.5 to any port 9100 proto tcp. - External checks: an uptime probe from outside your network catches what local tools cannot, such as a broken route or an expired certificate.
- Logs:
journalctl -p warning -blists warnings and errors since boot. Ship logs to another machine so an intruder cannot erase the trail.
IPMI and KVM credentials
The out-of-band controller can power-cycle the server, mount ISOs and open a console, so treat its credentials like root's:
- If you hold IPMI credentials, replace the initial password with a long, unique one kept in a password manager. The IPMI and KVM documentation covers access.
- Never reuse that password for SSH, the control panel or anything else.
- Limit who has it, revoke access when people leave, and close console sessions when you finish.
- Protect the control panel login the same way, since it can reinstall the server.
Backups
A hardened server can still lose data to a bad deploy or a failed disk. Back up to another machine, ideally in another location, and test restores. restic gives you encrypted, deduplicated backups over SFTP:
apt install -y restic
openssl rand -base64 32 > /root/.restic-password && chmod 600 /root/.restic-password
export RESTIC_REPOSITORY="sftp:backup@10.0.0.20:/srv/restic/web01"
export RESTIC_PASSWORD_FILE=/root/.restic-password
restic init
restic backup /etc /home /var/www --exclude-caches
restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --prune
restic restore latest --target /tmp/restore-test --include /etc/sshKeep a copy of the password somewhere other than the server, because without it the repository cannot be read. Dump databases to files before each run (pg_dump, mysqldump) instead of copying live data files, run the job from a systemd timer or cron, and alert when it fails. A storage server in the same location receives backups over the free private network.
After the first 15 minutes
A checklist works once; a script works every time. Capture these steps in configuration management, such as Ansible or cloud-init, so every new server starts from the same baseline and drift becomes visible. Then look for what this baseline does not cover:
- Run an audit tool such as Lynis (
apt install lynis, thenlynis audit system) and work through its suggestions. - Keep AppArmor enabled. It is on by default in Ubuntu and Debian, and
aa-statuslists the loaded profiles. - Review user accounts, sudo rights and
authorized_keysfiles whenever someone joins or leaves the team.
Run it on FlexDedi
Every FlexDedi server comes with the pieces this baseline relies on: IPMI and an HTML5 KVM console for when SSH is unreachable, rescue mode and one-click reinstalls in the control panel, always-on DDoS protection on every IP, and free private networking for backup traffic. For a typical web or API server, Core 16 ($299/mo) is the usual starting point; pair it with Storage 264 ($699/mo) or a second Core 16 as the backup target.
More guides
Comparison
Bare metal vs VPS vs cloud: which one do you need?
Gaming
Hosting game servers on dedicated hardware
Buying guide