Every server Unmetered traffic, DDoS protection and IPMI included. No setup fees, no contract. Unmetered traffic on every server See pricing

Guide · Security

Secure a new Linux dedicated server in 15 minutes

A 15-minute hardening baseline for Ubuntu 24.04 and Debian servers covering updates, a sudo user, SSH keys, firewall, fail2ban, monitoring and backups.

  • 7 min read
  • Updated
  • FlexDedi Engineering

Run this guide on

Core 16

  • AMD EPYC 4545P · 16 cores
  • 128 GB DDR5 ECC
  • 2 × 1.92 TB NVMe Gen4
  • 10 Gbps unmetered
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.

StepTime
Updates and automatic security updates3 min
Sudo user with your SSH key2 min
Key-only SSH, no root login2 min
Firewall baseline2 min
fail2ban1 min
Time sync and service review2 min
Monitoring, IPMI hygiene and backups3 min

Update everything

Start from current packages and reboot once, since the first upgrade on a new image often includes a kernel:

bash
apt update && apt full-upgrade -y
reboot

Ubuntu Server enables unattended-upgrades for security updates out of the box. On Debian, install and enable it:

bash
apt install -y unattended-upgrades
dpkg-reconfigure -plow unattended-upgrades   # answer Yes
unattended-upgrade --dry-run --debug 2>&1 | tail -n 5

The 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:

bash
cat > /etc/apt/apt.conf.d/52unattended-upgrades-local <<'EOF'
Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-Time "04:00";
EOF

Create a sudo user with your SSH key

On your workstation, create a key if you do not have one:

bash
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.

bash
adduser deploy
usermod -aG sudo deploy

If you logged in as root with a key, copy it to the new account:

bash
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_keys

If 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:

bash
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

bash
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 verbose

limit 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:

text
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;
    }
}
bash
nft -c -f /etc/nftables.conf && systemctl enable --now nftables
nft list ruleset

nft -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:

bash
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 sshd

Put 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:

bash
timedatectl
timedatectl set-timezone UTC

Look 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:

bash
ss -tulpn
systemctl list-units --type=service --state=running

Stop 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:

bash
nmap -Pn -p- 203.0.113.10   # every TCP port; takes a few minutes

Anything 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 smartmontools and nvme-cli. smartd then watches the drives, and nvme smart-log /dev/nvme0 shows 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-exporter serves CPU, memory, disk and network metrics on port 9100. Allow that port only from your monitoring host, for example ufw 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 -b lists 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:

bash
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/ssh

Keep 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, then lynis audit system) and work through its suggestions.
  • Keep AppArmor enabled. It is on by default in Ubuntu and Debian, and aa-status lists the loaded profiles.
  • Review user accounts, sudo rights and authorized_keys files 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.

Your next server is minutes away.

Deploy a Core 16 for $299/mo, or let our engineers size the right fleet for you.

  • No setup fees
  • Unmetered traffic
  • Cancel anytime