How to Set Up and Secure a Hetzner Server

A walkthrough of purchasing a Hetzner cloud server, then locking it down with SSH hardening, Tailscale, firewalls and automatic updates.

KT

By Kenny Tran

  • how-to
  • hetzner
  • linux
  • security
  • self-hosting
  • vps

By the end of this post you’ll have:

  • A small cloud server running in a data centre for about €7 a month
  • A setup where only you can log in, using a key on your laptop rather than a password
  • A private network (Tailscale) so the server is invisible to the rest of the internet
  • Two firewalls and automatic security updates, so it stays locked down without you babysitting it

Once it’s done, you’ve got a secure little box you can use for all sorts of things, like running Hermes.

I’m on a Mac, so the keyboard shortcuts and the commands you run on your own computer assume that. On Windows or Linux, those local bits differ (opening a terminal, copying your key, installing Tailscale). From Step 4 onwards nearly everything runs on the server, and that part is identical for everyone.

What you’ll need

  • About an hour
  • A credit card or PayPal for Hetzner
  • The Terminal app on your Mac. Press Cmd + Space, type “Terminal”, and hit Enter. That’s where you’ll type the commands in this guide.

Whenever you see a code box like the one below, you can copy it and paste it into Terminal, then press Enter.

Terminal window
echo "Like this"

Step 1: Create a Hetzner account

Head to hetzner.com/cloud and sign up. It’s a standard sign-up form. Depending on where you are, Hetzner may ask you to verify your identity before you can create a server, so don’t be surprised if there’s a short wait.

Once you’re in, open the Cloud Console and create a new Project. A project is just a folder for your servers, so call it whatever you like.

Step 2: Create an SSH key

Instead of logging into your server with a password, you’re going to use an SSH key. Think of it as a physical key and lock:

  • The private key stays on your laptop and never leaves it. That’s your key.
  • The public key goes on the server. That’s the lock.

Only your key opens that lock, and unlike a password, nobody can guess it.

Hetzner has a clear step-by-step guide for creating one: How to use SSH keys (Hetzner Community). Follow Step 1 and Step 2 of that guide. When it asks what type of key to create, use ed25519, which is the modern recommended type.

When you’re done, you’ll have a file on your Mac at ~/.ssh/id_ed25519.pub. That’s your public key, and it’s what Hetzner needs. You can copy it straight to your clipboard with:

Terminal window
pbcopy < ~/.ssh/id_ed25519.pub

Keep the private key private. The file without .pub on the end (~/.ssh/id_ed25519) should never be shared, emailed or uploaded anywhere.

Step 3: Create your server

In your project, click Add Server. You’ll get one long page of options. Here’s what to pick:

Option What to choose Why
Location Whichever is closest to you. I used Nuremberg. Closer means snappier.
Image Ubuntu 26.04 Ubuntu is the most common server system, so any help you search for online will usually apply to you.
Type Shared vCPU → CX23 (2 CPUs, 4 GB RAM, 40 GB disk) Plenty for running a handful of tools, and cheap. You can upgrade later with a couple of clicks.
SSH keys Click Add SSH key, paste the public key you copied, give it a name This installs your “lock” on the server.
Everything else Leave it as it is The defaults are fine.

Before you click the button, check the price summary. Mine came to €7.19 a month. Prices vary a little by location and change from time to time. Hetzner bills by the hour, so if you delete the server you only pay for the time it existed.

Click Create & Buy now. After a few seconds your server is up, and Hetzner shows you its IP address, a string of numbers like 203.0.113.42. That’s your server’s address on the internet. Copy it; you’ll need it next.

Step 4: Log in for the first time

Back in Terminal, type the following, replacing IP_ADDRESS with the IP you just copied:

Terminal window
ssh root@IP_ADDRESS

This says “open a secure connection to this server, logging in as the user root.” root is the all-powerful admin account every new server starts with.

The first time you connect, you’ll see a message saying the authenticity of the host can’t be established, and asking whether you want to continue. That’s normal. Your Mac has never met this server before. Type yes and press Enter. Your Mac will remember it from now on.

If everything worked, your prompt changes to something like root@your-server-name:~#. You’re now typing commands on your server, not your Mac. Nice work.

Tip: To leave the server and get back to your Mac, type exit.

Step 5: Update everything

Your server was built from an image that’s probably a few weeks old, so the first job is to install the latest updates, including security fixes. Run this:

Terminal window
apt update -y && apt upgrade -y && apt autoremove -y

That’s three commands chained together with &&, which means “do this, and if it worked, do the next thing”:

  1. apt update checks for the latest list of available software. apt is Ubuntu’s app store for the command line.
  2. apt upgrade installs any newer versions it found.
  3. apt autoremove cleans up old leftovers that are no longer needed.

The -y on each one answers “yes” automatically, so you don’t have to confirm each step.

You’ll see a lot of text scroll past. That’s fine, and you don’t need to read it. Wait until you get your prompt back.

Step 6: Create your own user

Right now you’re logged in as root. Root can do anything, including delete the entire server with one typo, and every automated attack on the internet tries to log in as root first. So the next step is to create an everyday account for yourself and stop using root.

Pick a username (short, lowercase, no spaces). In this guide I’ll write yourname; swap in your own username every time you see it:

Terminal window
adduser yourname

Ubuntu will ask you a few questions:

  1. New password: Choose a strong password and type it twice. Nothing appears on screen while you type; that’s normal, not broken. You won’t use this password to log in (your SSH key handles that), but you will need it when you run admin commands, so save it in your password manager.
  2. Full Name, Room Number, Work Phone…: These are leftovers from the days when many people shared one computer. Press Enter to skip each one.
  3. Is the information correct? Type Y and press Enter.

You now have a user called yourname with its own home folder on the server.

Give yourself admin rights

Your new user is an ordinary user, so it can’t install software or change system settings yet. You fix that by adding it to the sudo group:

Terminal window
usermod -aG sudo yourname

In plain English: “modify the user yourname and add them to the Group called sudo.” Get the -aG exactly right. If you leave off the a, the user gets removed from every other group they’re in.

usermod doesn’t print anything if it works. In Linux, no news is good news.

sudo is short for “superuser do.” From now on, whenever you need to do something admin-level, you put sudo in front of the command, for example sudo apt update, and Ubuntu asks for the password you just set. You get root’s powers only when you ask for them, one command at a time. That’s much safer than being root all day.

Let your SSH key open your new account too

Remember the public key (the “lock”) you gave Hetzner in Step 3? Hetzner installed it on the root account only. Your new user doesn’t have it yet, so your key won’t let you in as yourname.

The simplest fix is to copy root’s SSH folder across to your new user:

Terminal window
rsync --archive --chown=yourname:yourname ~/.ssh /home/yourname

Broken down:

  • rsync copies files.
  • --archive keeps everything exactly as it is, including the strict file permissions SSH insists on.
  • --chown=yourname:yourname makes your new user the owner of the copies. Without this, root would still own them, and SSH would refuse to use them.
  • ~/.ssh is the folder to copy (root’s SSH folder), and /home/yourname is where it goes.

Watch the slashes. Type ~/.ssh exactly as shown, with no slash on the end. With a trailing slash, rsync copies the contents of the folder instead of the folder itself, and your key ends up in the wrong place.

Test it before you go any further

Never close your working connection until you’ve proved the new way in works. If something’s wrong and you’ve logged out, you can lock yourself out of your own server.

So leave your current Terminal window alone, open a new one (Cmd + N), and log in as your new user:

Terminal window
ssh yourname@IP_ADDRESS

You should get straight in with no password prompt, because your key did the work. Then check that sudo works:

Terminal window
sudo whoami

Enter your password when asked. If it prints root, you’re all set: your everyday user can borrow admin powers when needed.

From here on, log in as yourname, not root. Next, you’ll switch root logins off entirely.

Step 7: Lock the front door

Right now your server still allows two risky things: logging in as root, and logging in with a password. You don’t need either, since you log in as yourname with a key, but bots all over the internet are hammering on your server trying exactly those two things, around the clock. Time to shut both off.

SSH’s settings live in one file. Open it with:

Terminal window
sudo nano /etc/ssh/sshd_config

This opens nano, a simple text editor that runs inside Terminal. There’s no mouse here. The few keys you need are:

Keys What it does
Arrow keys Move around
Ctrl + W Search for a word
Ctrl + O, then Enter Save
Ctrl + X Exit

The file is long and mostly made up of lines starting with #. A # means “this line is a note and is ignored.” Many of the settings you want are already there but switched off with a # in front. For each setting below, search for it with Ctrl + W. If you find it, delete the # and set the value shown. If it isn’t there, add the line at the bottom of the file.

PermitRootLogin no
PubkeyAuthentication yes
AuthorizedKeysFile .ssh/authorized_keys
PasswordAuthentication no
KbdInteractiveAuthentication no
X11Forwarding no
AllowUsers yourname

In plain English:

  • PermitRootLogin no: Nobody can log in as root, full stop.
  • PubkeyAuthentication yes and AuthorizedKeysFile …: Log in with SSH keys, and look for them in the usual place. These are already Ubuntu’s defaults; spelling them out just makes your intent clear.
  • PasswordAuthentication no and KbdInteractiveAuthentication no: Turn off both ways SSH can ask for a password. Keys only.
  • X11Forwarding no: Turns off a feature for showing graphical apps over SSH. You don’t need it, and fewer features means fewer ways in.
  • AllowUsers yourname: The strongest line here. Only this one user can log in over SSH, however many other accounts exist on the server.

Save (Ctrl + O, Enter) and exit (Ctrl + X).

Check your work before it goes live

First, check the file for typos:

Terminal window
sudo sshd -t

If it prints nothing, the file is fine. If it prints an error, reopen the file and fix the line it mentions.

Ubuntu can have extra settings files tucked away in /etc/ssh/sshd_config.d/ that override yours, so next, ask SSH which settings it will actually use:

Terminal window
sudo sshd -T | grep -E '^(permitrootlogin|passwordauthentication|kbdinteractiveauthentication|allowusers) '

You want to see exactly this (with your username):

permitrootlogin no
passwordauthentication no
kbdinteractiveauthentication no
allowusers yourname

If any line says yes when it should say no, one of those hidden files is overriding you. Look in /etc/ssh/sshd_config.d/ and change the offending line there too.

Switch it on, then test

Restart SSH so it picks up the new settings:

Terminal window
sudo systemctl restart ssh

Like most commands that work, it prints nothing.

Now the test. As before, keep your current window open, and in a new Terminal window, check that:

  1. You can still get in: ssh yourname@IP_ADDRESS should log you straight in.
  2. Root is turned away: ssh root@IP_ADDRESS should fail with Permission denied.

If both behave, your front door is locked and only your key opens it. If you can’t get in, don’t panic. Use your still-open window to reopen the config file and fix it.

Wondering about changing the SSH port? Some guides suggest moving SSH from port 22 to something like 2222. It cuts down on bot noise, but it also adds complexity and extra ways to lock yourself out. In the next two steps you’ll hide SSH from the public internet entirely, which makes the port change pointless, so this guide skips it.

Step 8: Put your server on a private network with Tailscale

Right now, your server has one door facing the whole internet: SSH on its public IP address. It’s well locked, but it’s still a door anyone can walk up to and rattle.

Tailscale lets you get rid of that door entirely. It creates a private network between your own devices (your Mac, your server, your phone if you like) that works wherever they are. Once your server is on it, you can reach it over Tailscale while it’s invisible to everyone else. Tailscale’s free personal plan is more than enough for this.

On your Mac

  1. Create an account at tailscale.com. You can sign up with Google, Microsoft or GitHub.
  2. Download the Tailscale app for Mac from tailscale.com/download, install it, and sign in with the same account.

You’ll see a small Tailscale icon in your menu bar. Your Mac is now on your private network (your “tailnet”).

On your server

Back in your server window (logged in as yourname), install Tailscale using its official install script:

Terminal window
curl -fsSL https://tailscale.com/install.sh | sh

This is two things joined by a | (a “pipe”): curl downloads Tailscale’s installer from their website, and sh runs it. The script works out which version of Ubuntu you’re on and installs the right package. It’ll print a fair bit of text as it goes; you don’t need to read it. Wait for your prompt to come back.

A word on curl … | sh: Running a script straight from the internet means trusting whoever wrote it. That’s fine here, since it’s Tailscale’s own official installer from their own domain, but it’s a good habit to only do this with sources you trust.

Now connect the server to your Tailscale account:

Terminal window
sudo tailscale up

This prints a link that starts with https://login.tailscale.com/…. Copy it into your browser on your Mac, sign in with the same Tailscale account, and approve the server. Back in Terminal, the command finishes by itself. Your server is now on your tailnet.

Turn off key expiry for your server (don’t skip this!)

By default, every device’s Tailscale login expires after 180 days. On your laptop that’s a minor annoyance: you sign in again. On your server, it’s a real problem. In the next step you’re going to make Tailscale the only way into the server, so if its login expires one day, you’re locked out.

To prevent that:

  1. Open the Tailscale admin console. You’ll see your Mac and your server listed under Machines.
  2. Click the ⋯ menu next to your server.
  3. Choose Disable key expiry.

Test your new private route in

While you’re in the admin console, note the name Tailscale gave your server. It’s usually the same name the server has in the Hetzner console. Then, keeping your current window open, open a new Terminal window and log in over Tailscale:

Terminal window
ssh yourname@your-server-name

Tailscale’s MagicDNS feature (on by default) lets you use the server’s name instead of an IP address, and it should log you straight in. If the name doesn’t work, use the server’s Tailscale IP instead: the one starting with 100. shown in the admin console, or run tailscale ip -4 on the server.

If you get in, you’re now connected over your private network, not the public internet. Your SSH lockdown from Step 7 still applies too: same key, same user, root still refused. Tailscale just adds a private tunnel in front of it.

Why not “Tailscale SSH”? Tailscale has its own SSH feature (tailscale up --ssh) that replaces SSH keys with your Tailscale login. It’s handy, but it bypasses the hardening you did in Step 7. This guide keeps regular SSH and, in the next step, makes it reachable only through Tailscale.

Step 9: Close the public door with a firewall

Your server can now be reached two ways: over Tailscale, and over its public IP address. You only need the first. A firewall is a bouncer that decides what traffic gets in, so let’s tell it: “Tailscale traffic only.”

Ubuntu comes with a friendly firewall tool called UFW (“Uncomplicated Firewall”). It’s installed but switched off.

Run these one at a time:

Terminal window
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow in on tailscale0
sudo ufw enable

Here’s what each line tells the bouncer:

  1. default deny incoming: Nobody gets in unless they’re on the list.
  2. default allow outgoing: The server itself can still reach out to the internet, for downloading updates, calling AI APIs and so on.
  3. allow in on tailscale0: Here’s the list. tailscale0 is the name of Tailscale’s network connection on your server, so anything arriving through Tailscale gets in.
  4. enable: Switch it on. UFW warns you that this “may disrupt existing ssh connections” and asks you to confirm. Because you’re connected over Tailscale, which you just allowed, you’re safe. Type y and press Enter.

Check that it’s working:

Terminal window
sudo ufw status verbose

You should see Status: active, Default: deny (incoming), allow (outgoing), and a rules table like this:

To Action From
-- ------ ----
Anywhere on tailscale0 ALLOW IN Anywhere
Anywhere (v6) on tailscale0 ALLOW IN Anywhere (v6)

There are two lines because UFW covers both the old style of internet address (IPv4) and the new one (IPv6).

Test it

In a new Terminal window, try to log in the old way, using the public IP:

Terminal window
ssh yourname@IP_ADDRESS

This should now hang and eventually time out. Press Ctrl + C to give up. That’s exactly what you want: to the rest of the internet, your server no longer answers at all. Meanwhile, ssh yourname@your-server-name over Tailscale still works.

Heads-up if you ever use Docker: Docker has a habit of punching its own holes through UFW. If you run something in Docker and “publish” a port, it can be reachable from the internet even though UFW says otherwise. Nothing in this guide needs Docker, but if you add it later, look up “Docker UFW” before you do.

Step 10: Make sure security updates install themselves

Back in Step 5 you installed all the latest updates by hand. But new security fixes come out every week, and you’re not going to log in every week to install them. You shouldn’t have to.

Ubuntu has a built-in feature called unattended-upgrades that installs security updates automatically in the background. On Hetzner’s Ubuntu image it’s usually already switched on, so this step is mostly checking. Run:

Terminal window
cat /etc/apt/apt.conf.d/20auto-upgrades

cat prints a file’s contents to the screen. You’re looking for these two lines, both set to "1" (meaning “on”):

APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";

The first line checks for new updates every day; the second installs them.

Then check that the service doing the work is running:

Terminal window
systemctl status unattended-upgrades --no-pager

You want to see active (running) in green. Press q if the output doesn’t give you your prompt back.

If either check fails, turn it on with:

Terminal window
sudo apt install unattended-upgrades -y
sudo dpkg-reconfigure --priority=low unattended-upgrades

A blue screen appears asking whether to automatically download and install stable updates. Choose Yes.

What about restarts? Most updates apply straight away, but a few (mainly updates to Ubuntu’s core, the “kernel”) only take effect after a reboot. Automatic updates won’t reboot your server for you. When you log in, Ubuntu tells you if a restart is needed with a message like *** System restart required ***. When you see it, run sudo reboot, wait a minute, and log back in.

Step 11: Add a second wall with Hetzner’s Cloud Firewall

UFW is a firewall that runs on your server. Hetzner also offers a free firewall that sits in front of your server, at the edge of their network. Traffic it blocks never even reaches your machine.

Having both is belt and braces. If you ever make a mistake with UFW, or some software you install quietly opens a port (hello, Docker), the Hetzner firewall still has your back. And because you manage it from the Hetzner website rather than on the server, a mistake here can’t lock you out for good; you can always log in to Hetzner and change it.

  1. In the Hetzner Cloud Console, open your project and click Firewalls in the left-hand menu.
  2. Click Create Firewall.
  3. Inbound rules: Hetzner pre-fills two rules for you, one allowing SSH and one allowing “ping”. Delete both. You want the inbound list completely empty.
  4. Outbound rules: Leave these alone. Your server still needs to reach the internet.
  5. Name: Something that describes what it does, so you can reuse it on future servers. I went with tailscale-only.
  6. Click Create Firewall.
  7. Now attach it to your server. Open your new firewall, go to Apply to (or Resources), choose Server, and select yours. A firewall does nothing until it’s attached to something.

An empty inbound list won’t block Tailscale. Your server reaches out to Tailscale, your Mac does the same, and the two meet in the middle. Nothing ever needs to connect in to your server from the public internet.

Give it a few seconds to apply, then do one last check from a new Terminal window:

Terminal window
ssh yourname@your-server-name

If that still works, you’re done.

What you’ve built

Your server now:

  • Accepts logins from one user, with one key, and never as root
  • Can only be reached over your private Tailscale network
  • Sits behind two firewalls, one on the server and one in front of it
  • Installs its own security updates

The only upkeep is the occasional restart. When Ubuntu shows *** System restart required *** at login, run sudo reboot.