Setting up a new root server: checklist for the first 30 minutes
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 refusedmeans 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 outmeans 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, thensshd -tandsystemctl restart ssh. - Wrong port after a socket change:
systemctl revert ssh.socketresets the overlay, thensystemctl daemon-reloadandsystemctl 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 underignoreipinjail.local. - Key gets rejected: Almost always permissions.
chmod 700on the directory,chmod 600on 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:
sshd -T | grep -E "^(permitrootlogin|passwordauthentication|port) "shows the values in effect, not the ones you wanted.ufw status verbosereportsStatus: activewith a rule for your SSH port.timedatectl statusreportsSystem clock synchronized: yes.systemctl --failedlists nothing.unattended-upgrade --dry-run --debugruns through without an error message.- 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?
Why does my change in sshd_config have no effect?
Why does my SSH port not change on Ubuntu 24.04?
Fail2ban does not start and reports that it found no log file for the sshd jail. What now?
Why is my hostname gone again after a reboot?
Is apt upgrade enough or do I need apt full-upgrade?
2026 KernelHost GmbH. All rights reserved. This guide is protected by copyright. Republishing it on other websites, in whole, in part or in edited form, is not permitted without our written consent. Quoting with a source credit and a link is expressly welcome.

