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.
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.
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:
pbcopy < ~/.ssh/id_ed25519.pubKeep 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:
ssh root@IP_ADDRESSThis 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:
apt update -y && apt upgrade -y && apt autoremove -yThat’s three commands chained together with &&, which means “do this, and if it worked, do the next thing”:
apt updatechecks for the latest list of available software.aptis Ubuntu’s app store for the command line.apt upgradeinstalls any newer versions it found.apt autoremovecleans 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:
adduser yournameUbuntu will ask you a few questions:
- 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.
- Full Name, Room Number, Work Phone…: These are leftovers from the days when many people shared one computer. Press Enter to skip each one.
- Is the information correct? Type
Yand 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:
usermod -aG sudo yournameIn 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:
rsync --archive --chown=yourname:yourname ~/.ssh /home/yournameBroken down:
rsynccopies files.--archivekeeps everything exactly as it is, including the strict file permissions SSH insists on.--chown=yourname:yournamemakes your new user the owner of the copies. Without this, root would still own them, and SSH would refuse to use them.~/.sshis the folder to copy (root’s SSH folder), and/home/yournameis 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:
ssh yourname@IP_ADDRESSYou should get straight in with no password prompt, because your key did the work. Then check that sudo works:
sudo whoamiEnter 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:
sudo nano /etc/ssh/sshd_configThis 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 noPubkeyAuthentication yesAuthorizedKeysFile .ssh/authorized_keysPasswordAuthentication noKbdInteractiveAuthentication noX11Forwarding noAllowUsers yournameIn plain English:
PermitRootLogin no: Nobody can log in asroot, full stop.PubkeyAuthentication yesandAuthorizedKeysFile …: 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 noandKbdInteractiveAuthentication 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.
Double-check the AllowUsers line. It must be your username, spelled
exactly right. Get it wrong and nobody, including you, can log in.
Save (Ctrl + O, Enter) and exit (Ctrl + X).
Check your work before it goes live
First, check the file for typos:
sudo sshd -tIf 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:
sudo sshd -T | grep -E '^(permitrootlogin|passwordauthentication|kbdinteractiveauthentication|allowusers) 'You want to see exactly this (with your username):
permitrootlogin nopasswordauthentication nokbdinteractiveauthentication noallowusers yournameIf 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:
sudo systemctl restart sshLike 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:
- You can still get in:
ssh yourname@IP_ADDRESSshould log you straight in. - Root is turned away:
ssh root@IP_ADDRESSshould fail withPermission 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
- Create an account at tailscale.com. You can sign up with Google, Microsoft or GitHub.
- 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:
curl -fsSL https://tailscale.com/install.sh | shThis 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:
sudo tailscale upThis 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:
- Open the Tailscale admin console. You’ll see your Mac and your server listed under Machines.
- Click the ⋯ menu next to your server.
- Choose Disable key expiry.
Only do this for your server, not your laptop or phone. If a laptop is lost or stolen, you want its access to expire.
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:
ssh yourname@your-server-nameTailscale’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.
Do this step from your Tailscale login (ssh yourname@your-server-name), not a public-IP one. If you’re connected over
the public IP when the firewall switches on, you’ll be disconnected.
Run these one at a time:
sudo ufw default deny incomingsudo ufw default allow outgoingsudo ufw allow in on tailscale0sudo ufw enableHere’s what each line tells the bouncer:
default deny incoming: Nobody gets in unless they’re on the list.default allow outgoing: The server itself can still reach out to the internet, for downloading updates, calling AI APIs and so on.allow in on tailscale0: Here’s the list.tailscale0is the name of Tailscale’s network connection on your server, so anything arriving through Tailscale gets in.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. Typeyand press Enter.
Check that it’s working:
sudo ufw status verboseYou should see Status: active, Default: deny (incoming), allow (outgoing), and a rules table like this:
To Action From-- ------ ----Anywhere on tailscale0 ALLOW IN AnywhereAnywhere (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:
ssh yourname@IP_ADDRESSThis 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:
cat /etc/apt/apt.conf.d/20auto-upgradescat 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:
systemctl status unattended-upgrades --no-pagerYou 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:
sudo apt install unattended-upgrades -ysudo dpkg-reconfigure --priority=low unattended-upgradesA 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.
- In the Hetzner Cloud Console, open your project and click Firewalls in the left-hand menu.
- Click Create Firewall.
- 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.
- Outbound rules: Leave these alone. Your server still needs to reach the internet.
- Name: Something that describes what it does, so you can reuse it on future servers. I went with
tailscale-only. - Click Create Firewall.
- 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:
ssh yourname@your-server-nameIf 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.
