Setting up a new root server: checklist for the first 30 minutes

Published on 16 min read

The first 30 minutes on a new root server decide how it will run. Nine steps in the right order, including the Debian 13 and Ubuntu 24.04 pitfalls that most guides leave out.

A new root server is reachable from the first second and gets scanned from the first minute. In practice, the automated login attempts on port 22 start before you have logged in yourself for the very first time. This list brings a fresh server into a state you can leave running with a clear conscience, in roughly half an hour.

All commands assume that you are working as root, exactly as you are right after provisioning. Once you are logged in with your new user, prefix every command with sudo. The order has been verified on Debian 13 (trixie), Debian 12 (bookworm), Ubuntu 24.04 LTS and Ubuntu 22.04 LTS. Wherever the four differ, it is noted.

Step 0: Secure your way back before you change anything

The one mistake in this list that you cannot repair over SSH is the one that takes SSH away from you. So for the next 30 minutes there is a single iron rule: open a second terminal window with an active SSH connection and do not close it. An existing SSH session survives both a restart of the SSH service and the moment you enable the firewall. If a new connection stops working after a change, undo that change in the session that is still open.

You should also know where to find console access to your server before you actually need it. At KernelHost you reach it in the customer panel, independently of SSH and independently of the firewall. Searching for that path while you are already locked out costs time.

Two error messages are worth telling apart, because they point to completely different causes:

  • ssh: connect to host 203.0.113.10 port 22: Connection refused means the packet arrived, but nobody is listening on that port. The SSH service is not running, or it is listening on a different port.
  • ssh: connect to host 203.0.113.10 port 22: Connection timed out means the packet was dropped. That is almost always the firewall.
  • Permission denied (publickey) means the service is running and the firewall lets you through, only your key does not match.

Step 1: Update the system

A freshly installed image is rarely current. Weeks often pass between the build of the image and your order, and security updates get published in that time.

cat /etc/os-release
apt update
apt full-upgrade -y

full-upgrade instead of upgrade is a deliberate choice here: on a fresh system, apt is allowed to remove packages if a dependency requires it. On a running production system you would first check what is about to be removed.

On Ubuntu 22.04 and 24.04, needrestart is preinstalled and interrupts the upgrade with a colorful full-screen dialog asking which services should be restarted. If you do not want that, for example inside a script:

NEEDRESTART_MODE=a DEBIAN_FRONTEND=noninteractive apt full-upgrade -y

Then clean up and check whether a reboot is needed:

apt autoremove --purge -y
apt list --upgradable
test -f /var/run/reboot-required && echo "reboot required" || echo "no reboot required"

Difference between the distributions: Only Ubuntu reliably creates the file /var/run/reboot-required, which comes from the package update-notifier-common. Debian does not report a required reboot at all by default. On Debian you install needrestart for that purpose, and it tells you on every run whether a new kernel is waiting. Debian 13 also ships a newer apt generation with colored, column-formatted output. That is not a fault, just unfamiliar.

Success check: apt list --upgradable prints nothing except the header Listing.... If the update reports Release file for ... is not valid yet, your server's clock is wrong: jump to step 5 and then repeat step 1.

More on this, including how to deal with held-back packages and third-party repositories: Updating Linux servers with apt.

Step 2: Create a user instead of working as root

You do not work as root, because every typo immediately hits the entire system, and because every attacker already knows the username root. On minimal Debian images, sudo is often not even installed:

apt install -y sudo
adduser --disabled-password --gecos "" kernel
usermod -aG sudo kernel

The --disabled-password switch creates the user without a password, which is exactly right for a key-only login. If you also want to set a password, for example for sudo over the console, use passwd kernel.

Difference between the distributions: On Debian and Ubuntu the administrator group is called sudo. Only if you come from a RHEL-style system will you look for wheel, which does not exist here.

Now the public key. Create the directory with the correct permissions, because wrong permissions are the most common reason for a rejected key login:

mkdir -p /home/kernel/.ssh
chmod 700 /home/kernel/.ssh
touch /home/kernel/.ssh/authorized_keys
chmod 600 /home/kernel/.ssh/authorized_keys
chown -R kernel:kernel /home/kernel/.ssh

Put the contents of your public key into authorized_keys. It is more convenient to do this from your workstation with ssh-copy-id kernel@203.0.113.10.

Success check, and do it before you touch the SSH configuration:

id kernel
sudo -l -U kernel

The second line has to contain (ALL : ALL) ALL. Then log in as kernel in a third window and run sudo -v once. Only when that works do you continue. Details: Creating a user and setting up sudo and Creating and installing SSH keys.

Step 3: Harden SSH

On very lean Debian images the SSH server is not even installed, so fetch it first:

apt install -y openssh-server

All four systems covered here read additional configuration from /etc/ssh/sshd_config.d/. So do not edit the big sshd_config, create your own file instead. It survives package updates without any prompt.

One detail that almost every guide gets wrong: in the SSH configuration, the first value found wins, not the last. The line Include /etc/ssh/sshd_config.d/*.conf sits right at the top on Debian and Ubuntu, and the files in that directory are read in alphabetical order. On Ubuntu images there is often already a 50-cloud-init.conf containing PasswordAuthentication yes. A file named 99-... would therefore have no effect at all. Check what is already there first:

ls -l /etc/ssh/sshd_config.d/
cat > /etc/ssh/sshd_config.d/10-kernelhost.conf <<'EOF'
PermitRootLogin prohibit-password
PasswordAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes
X11Forwarding no
MaxAuthTries 3
EOF

prohibit-password instead of no is a deliberate decision: root can still log in with a key, but never with a password. That saves you if something goes wrong with the sudo user. If you want to lock things down further, set no, but then you should really have tested console access.

Check the syntax before every restart of the service:

ssh-keygen -A
sshd -t && echo "configuration ok"
sshd -T | grep -E "^(permitrootlogin|passwordauthentication|pubkeyauthentication|port) "

sshd -T shows the values that are actually in effect, after all include files have been resolved. That is the only reliable proof that your change has landed. If the command reports sshd: no hostkeys available -- exiting, the host keys are missing and ssh-keygen -A creates them. If the check aborts with Missing privilege separation directory: /run/sshd instead, the service has never run since the system booted and the runtime directory is missing. An mkdir -p /run/sshd or a systemctl restart ssh creates it, after which sshd -t evaluates your configuration again.

One piece of output causes confusion regularly: for PermitRootLogin prohibit-password, sshd -T prints the line permitrootlogin without-password. That is the same value under its older name, and not a sign that your setting failed to take effect.

Only after that:

systemctl restart ssh

The socket trap on Ubuntu 24.04 and Debian 13

Since Ubuntu 22.10, and therefore in 24.04 as well, SSH is started through socket activation. The consequence: a Port line in the sshd configuration is ignored, the port comes from ssh.socket. If you want to change the port, you need a systemd overlay:

systemctl edit ssh.socket

Put this into it, where the empty first line clears the default:

[Socket]
ListenStream=
ListenStream=0.0.0.0:2222
ListenStream=[::]:2222

Then systemctl daemon-reload and systemctl restart ssh.socket. Ubuntu 22.04 does not know this mechanism yet, and there the Port line in the configuration is enough.

On Debian 13 a related oddity shows up. Some fresh images have ssh.socket active, systems upgraded from Debian 12 do not. If both are active at the same time, a reload fails with fatal: Cannot bind any address., because the service and the socket fight over port 22. Check it, and decide if in doubt:

systemctl is-enabled ssh.socket
systemctl disable --now ssh.socket
systemctl enable --now ssh.service

Success check: ss -tlnp | grep ssh shows the expected port, and a new connection attempt from a fresh window works. In detail: Hardening SSH and disabling the root login and Changing the SSH port.

Step 4: Enable the firewall

On Ubuntu, ufw is installed but inactive. On minimal Debian images it is missing entirely. The order is vital here: allow SSH first, then switch it on.

apt install -y ufw
ufw default deny incoming
ufw default allow outgoing
ufw allow 22/tcp
ufw allow 80/tcp
ufw allow 443/tcp
ufw enable

The last command is the most important one in the whole list. Rules and default policies on their own filter nothing, only ufw enable arms the firewall. Leave it out and you end up with a fully configured but completely ineffective firewall.

When you switch it on, ufw asks: Command may disrupt existing ssh connections. Proceed with operation (y|n)?. That warning is meant seriously, but if the rule for port 22 (or your changed port) is already in place, nothing happens, and that is exactly why ufw allow 22/tcp appears above ufw enable in the list. If you changed the port in step 3, this line has to read ufw allow 2222/tcp, otherwise you lock yourself out. In a script or in a non-interactive session, use ufw --force enable, which skips the prompt.

Success check:

ufw status verbose

What you want to see is Status: active, below it Default: deny (incoming), allow (outgoing), and in the rule list a line 22/tcp ALLOW IN for your SSH port. If it still says Status: inactive, then ufw enable is missing and nothing is protected, no matter how complete the rules look. Afterwards, open a new window and connect before you close the old one.

A blocking mechanism against repeated login attempts belongs with it:

apt install -y fail2ban python3-systemd
cat > /etc/fail2ban/jail.local <<'EOF'
[DEFAULT]
backend = systemd
bantime = 1h
findtime = 10m
maxretry = 5

[sshd]
enabled = true
EOF

Why backend = systemd: Since version 12, Debian no longer ships rsyslog, so there is no /var/log/auth.log. The default backend = auto looks for exactly that file, and fail2ban then does not start at all, reporting Failed during configuration: Have not found any log file for sshd jail. The package python3-systemd is the prerequisite for journal access to work in the first place. On Ubuntu 22.04 and 24.04 the file still exists thanks to rsyslog, but the systemd variant works there as well and is the future-proof choice. You can check with:

test -f /var/log/auth.log && echo "auth.log present" || echo "no auth.log, backend systemd required"
fail2ban-client -t

Success check: fail2ban-client status sshd shows a line Currently banned. If you get Sorry but the jail 'sshd' does not exist instead, the configuration was not loaded. More in Setting up the UFW firewall and Setting up Fail2ban.

Step 5: Time zone and clock

A wrong clock makes logs worthless, breaks certificate checks and can block apt with Release file is not valid yet. On a real server:

timedatectl set-timezone Europe/Vienna
timedatectl status

Two lines in the output have to be right: Time zone: Europe/Vienna and System clock synchronized: yes, plus NTP service: active. If it says NTP service: inactive, no time synchronization is running. Ubuntu ships systemd-timesyncd by default, minimal Debian images often do not:

DEBIAN_FRONTEND=noninteractive apt install -y systemd-timesyncd tzdata
date

Many operators deliberately run their servers on UTC so that logs from different locations stay comparable. Both are defensible, what matters is that you know which one applies. If timedatectl is not available in a container environment, the classic way works too:

ln -sf /usr/share/zoneinfo/Europe/Vienna /etc/localtime
dpkg-reconfigure -f noninteractive tzdata

Going deeper: Setting up the time zone and time synchronization.

Step 6: Set the hostname

The hostname shows up in logs, in outgoing email and in monitoring alerts. Set it early, otherwise all of your servers will end up with the same name later on.

hostnamectl set-hostname srv01.ihre-domain.de
hostname -f

A matching entry in /etc/hosts belongs with it, otherwise every sudo call greets you with sudo: unable to resolve host srv01: Name or service not known and a noticeable delay. The line reads roughly like 127.0.1.1 srv01.ihre-domain.de srv01.

The trap: On images with cloud-init, which is the rule on Ubuntu, the hostname is reset on the next reboot. The switch against that:

command -v cloud-init || echo "cloud-init not installed"
mkdir -p /etc/cloud/cloud.cfg.d
printf 'preserve_hostname: true\n' > /etc/cloud/cloud.cfg.d/99_hostname.cfg

Success check: After a reboot, hostnamectl still returns your name. Details: Changing the hostname permanently on Linux.

Step 7: Automatic security updates

The most dangerous server is the one nobody ever touches again. Automatic security updates are the single most effective step in this list.

apt install -y unattended-upgrades
cat > /etc/apt/apt.conf.d/20auto-upgrades <<'EOF'
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";
EOF

Installing the package alone is not enough everywhere, only this file arms the daily run. Your own fine-tuning belongs in a file with a higher number than the shipped 50unattended-upgrades, so that it wins and does not get overwritten by a package update:

cat > /etc/apt/apt.conf.d/52unattended-upgrades-local <<'EOF'
Unattended-Upgrade::Automatic-Reboot "false";
Unattended-Upgrade::Remove-Unused-Kernel-Packages "true";
Unattended-Upgrade::MinimalSteps "true";
EOF

By default, on both distributions the tool pulls only from the security repository, not the regular updates. That is intentional and usually exactly right for production systems. If you set Automatic-Reboot "true", make sure you also set a time, otherwise the server reboots whenever the timer happens to fire.

Success check: A dry run shows which packages would qualify, without installing anything. Watch out for the singular in the command name:

unattended-upgrade --dry-run --debug
apt-config dump | grep -iE "unattended|periodic"

The log under /var/log/unattended-upgrades/ has to contain something after the first run. If it stays empty, the configuration is not taking effect. In detail: Setting up automatic security updates.

Step 8: Monitoring

In the first 30 minutes, monitoring does not mean building a Grafana. It means that you find out when the server goes down or the disk fills up.

apt install -y htop tmux curl
df -h /
free -m

Three things are enough to begin with. First, an external reachability check that tests from the outside and notifies you, because a server that has crashed no longer sends any warning about itself. Second, a disk space alert, because a full filesystem is the most common cause of outages that nobody saw coming. Third, a look into the journal whenever something seems odd:

journalctl -p 3 -b --no-pager | tail -n 30
systemctl --failed

systemctl --failed should print 0 loaded units listed. Every line there is a service that does not start, and one you want to repair now rather than in three months. More on this: Setting up server monitoring.

Step 9: Backup, before there is anything to lose

The best moment for the first backup is before any data exists. That way you practice the procedure without pressure. Two things are worth writing away immediately, because reconstructing them costs the most time: the configuration under /etc and the list of installed packages.

tar -czf /root/etc-backup-$(date +%F).tar.gz /etc
dpkg --get-selections > /root/pakete.txt
tar -tzf /root/etc-backup-$(date +%F).tar.gz | wc -l

That does not give you a backup yet, only a copy on the same storage device. A backup lives on a different system, ideally in a different location. A tool with encryption and deduplication of identical blocks pays off from day one:

apt install -y borgbackup
borg --version

The uncomfortable truth: a backup that nothing has ever been restored from is an assumption. Book a slot for your first restore and recover a single file. The way there is described in Backup strategy for root servers.

If you have locked yourself out

It happens, usually in step 3 or 4. The way back is always the same: log in through the console in the customer panel, using username and password instead of a key. Then, depending on the cause:

  • Firewall too strict: ufw disable, correct the rule, ufw enable.
  • SSH configuration broken: rm /etc/ssh/sshd_config.d/10-kernelhost.conf, then sshd -t and systemctl restart ssh.
  • Wrong port after a socket change: systemctl revert ssh.socket resets the overlay, then systemctl daemon-reload and systemctl restart ssh.socket.
  • Banned by fail2ban: fail2ban-client set sshd unbanip 203.0.113.10. To keep that from happening again, add your static address under ignoreip in jail.local.
  • Key gets rejected: Almost always permissions. chmod 700 on the directory, chmod 600 on the file, and both have to belong to the user, not to root.

The final check

A command that returns no error does not mean it had any effect. These six checks show the actual state:

  1. sshd -T | grep -E "^(permitrootlogin|passwordauthentication|port) " shows the values in effect, not the ones you wanted.
  2. ufw status verbose reports Status: active with a rule for your SSH port.
  3. timedatectl status reports System clock synchronized: yes.
  4. systemctl --failed lists nothing.
  5. unattended-upgrade --dry-run --debug runs through without an error message.
  6. A new SSH connection from a freshly opened window works, with a key and without a password prompt.

Only once point six is solid may you close the old terminal window.

What comes next

A word on the choice of distribution, because it decides the next few years. Debian 12 has been out of regular support since July 2026 and will be maintained by the LTS team until mid-2028, with a reduced set of packages. Anyone setting up a new system today takes Debian 13 or Ubuntu 24.04 LTS. Ubuntu 22.04 LTS is still in standard support until 2027, but for a server that is meant to run for years it is no longer the first choice. Debian 10 and Ubuntu 20.04 have been finished since June 2024 and May 2025 respectively, and belong on no new server.

The version differences in the package repositories matter in practice as well. Debian never ships mysql-server, MariaDB is the standard there. If you need a specific PHP, Node or Java version, it is better to check in advance what the distribution provides than to bolt on third-party repositories later. And if you do add third-party repositories: apt-key is deprecated, keys belong in /etc/apt/keyrings/ and are referenced from the source entry with signed-by.

With that you have a server that is up to date, keeps itself up to date, lets only you in and reports when something is wrong. Everything else, web server, database, certificates, builds on top of that and not next to it.

Frequently asked questions

In what order should I set up a new root server?
First update the system, then create a user with sudo rights and a stored SSH key, then harden SSH, and only after that enable the firewall. Time zone, hostname, automatic security updates, monitoring and backup follow afterwards. Only one thing really matters: the user and the key have to work before you disable password authentication, and the SSH rule has to be in the firewall before you switch it on.
Why does my change in sshd_config have no effect?
On Debian and Ubuntu the line Include /etc/ssh/sshd_config.d/*.conf sits right at the top of sshd_config, and in the SSH configuration the first value found wins. A file such as 50-cloud-init.conf therefore beats both the main file and your own file with a higher number. Use sshd -T to check which values are actually in effect, and give your file a lower number, for example 10-kernelhost.conf.
Why does my SSH port not change on Ubuntu 24.04?
Since Ubuntu 22.10, SSH is started through socket activation. The port then comes from ssh.socket, and the Port line in the sshd configuration is ignored. You need a systemd overlay via systemctl edit ssh.socket, with an empty ListenStream line that clears the default, followed by the value you want. On Ubuntu 22.04 the Port line is still enough.
Fail2ban does not start and reports that it found no log file for the sshd jail. What now?
Since version 12, Debian no longer ships rsyslog, so /var/log/auth.log does not exist. Set backend = systemd under [DEFAULT] in /etc/fail2ban/jail.local and install the package python3-systemd. After that, fail2ban reads directly from the journal. You can verify it with fail2ban-client -t and then fail2ban-client status sshd.
Why is my hostname gone again after a reboot?
On images with cloud-init, which is the rule on Ubuntu, the hostname is set again on every boot. Create /etc/cloud/cloud.cfg.d/99_hostname.cfg with the line preserve_hostname: true, and your setting stays in place. Also add the matching entry in /etc/hosts, otherwise sudo reports on every call that it cannot resolve the host.
Is apt upgrade enough or do I need apt full-upgrade?
On a freshly provisioned server, full-upgrade is the better choice, because it also applies updates for which packages have to be removed or replaced. On a running production system you should first check what would happen, using apt list --upgradable and a simulation, before you run full-upgrade.

Root Server Server Setup Linux SSH Firewall Debian Ubuntu Server Security Checklist