djspace

Securing a Linux home server

October 25, 2026 · 14 min read · linux, ubuntu, ufw, ssh, systemd

New here? Start here for what my homelab is and how this site is organized.

My home server runs a lot: monitoring, logs, a password manager, a media server. Out of the box, Ubuntu answers anyone on the network who asks, and it lets you log in over SSH with just a password. This guide is the first thing I’d do on any new home server, in the order I’d do it.

You don’t need to be a Linux expert. If you can open a terminal and copy and paste, you can follow it. Plan on about an hour the first time.

What you’ll build

By the end, your server will:

What gets in and what gets out: your laptop reaches SSH through the firewall, home devices reach the web app but are blocked from SSH, the internet reaches only the exposed app, and the exposed app sits in a systemd box that can’t reach the router’s admin page. Security updates install every day.

What you need

How to use this guide

Step 0: Find your own values

You’ll need five values. Write them down somewhere, you’ll use them all through the guide.

PlaceholderWhat it isExample
<server-ip>the server’s address on your network192.168.10.5
<user>your username on the serveralex
<router-ip>your router’s address192.168.10.1
<lan>your whole home network192.168.10.0/24
<laptop-ip>your laptop’s address192.168.10.40

On the server:

hostname -I
whoami
ip route | grep default

The first line prints the server’s address (if it prints several, use the one that starts like your other home devices, usually 192.168. something). The second prints your username. The third prints something like default via 192.168.10.1 dev enp3s0: the address after “via” is your router.

For <lan>, take the router’s address, replace the last number with 0, and add /24. So a router at 192.168.10.1 gives 192.168.10.0/24. That’s right for almost every home network.

On your laptop, find its address:

One important thing: home routers hand out addresses that can change. The firewall rules below are tied to addresses, so if your laptop’s address changes next month, you’ll be locked out of remote login. Open your router’s admin page and give the server and the laptop a fixed address. Routers call it “DHCP reservation”, “static lease” or “fixed IP”. Do this before going on.

Step 1: Turn on automatic security updates

Most break-ins on home servers use bugs that were fixed months ago, on machines that never installed the fix. So first, let the server install security fixes on its own.

On the server:

sudo apt update && sudo apt full-upgrade -y
sudo apt install -y unattended-upgrades
cat /etc/apt/apt.conf.d/20auto-upgrades

The first line downloads the list of available updates and installs them all; it can take several minutes the first time. The second installs the tool that does this automatically from now on (it’s often there already). The third shows its settings.

You should see both lines set to “1”:

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

If the last command says “No such file”, or one of the lines says “0”, run this to create the file with the right settings:

printf 'APT::Periodic::Update-Package-Lists "1";\nAPT::Periodic::Unattended-Upgrade "1";\n' | sudo tee /etc/apt/apt.conf.d/20auto-upgrades

From now on the server installs security updates every day. It doesn’t restart by itself, though. Some updates (the Linux kernel, for example) only take effect after a restart, and when that happens a file called /var/run/reboot-required appears. Once a week or so, check for it:

ls /var/run/reboot-required

“No such file or directory” means no restart is needed. If the file is there, restart at a quiet time with sudo reboot.

One warning: this only covers software that comes from Ubuntu. If you install an app from its maker’s own repository later, keep an eye on it. I learned that the hard way: one of my apps had quietly stopped updating for a month because its maker moved their repository. Run sudo apt update now and then and read any errors it prints.

Step 2: Turn on the firewall

A firewall decides who may connect to the server. Ubuntu comes with one called UFW, but it’s switched off. The idea is simple: refuse everything coming in, then open only what you use, and only for the devices that use it.

Each program on a server listens on a numbered “port”. SSH (remote login) uses port 22. A web app might use 8080. If you’re not sure which ports your apps use, this lists everything listening on the server, with the program name at the end of each line:

sudo ss -tlnp

On the server, start with the default rules and allow SSH from your laptop. Do this first, so you don’t lock yourself out when the firewall switches on:

sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow from <laptop-ip> to any port 22 proto tcp comment 'SSH from my laptop'

The first line blocks everything coming in. The second lets the server itself reach out (for updates, for example). The third makes the one exception for SSH from your laptop. The comment is just a label so you remember later why the rule is there.

Then add one line for each app other people at home use, from your home network. For example, a web app on port 8080:

sudo ufw allow from <lan> to any port 8080 proto tcp comment 'my web app'

Now switch it on and look at the result:

sudo ufw enable
sudo ufw status verbose

UFW asks Command may disrupt existing ssh connections. Proceed with operation (y|n)?. Type y and press Enter. That’s safe because you already allowed your laptop.

You should see Status: active, then Default: deny (incoming), allow (outgoing), then a list with your rules.

One catch if you use Docker: when a container publishes a port (for example -p 8080:80), Docker opens it around UFW, to everyone. Write -p 127.0.0.1:8080:80 instead, so only the server itself can reach the container, and put a reverse proxy in front of it if other devices need it.

Step 3: Log in with a key instead of a password

Passwords can be guessed, and if someone tries enough of them, eventually one works. A key is a pair of files. The private half stays on your laptop, the public half goes on the server, and logging in proves you have the private half. Nobody can guess it.

How key login works: the private key never leaves your laptop and is locked by a passphrase; the server only holds the public key. Once, you copy the public key to the server. At every login the server sends a challenge, the laptop answers it with the private key, and you’re in. No password crosses the network.

Do this in order: make the key, copy it, test it, and only then turn passwords off. If you turn passwords off first, you lock yourself out.

On your laptop, make the key:

ssh-keygen -t ed25519 -C "my laptop"

It asks three questions. “Enter file in which to save the key”: just press Enter to accept the default. “Enter passphrase”: type a passphrase (a short sentence is good) and press Enter, then type it again. Like sudo, nothing shows while you type. The passphrase protects the key if someone ever copies it off your laptop.

Copy the public half to the server. On your laptop, on Mac or Linux:

ssh-copy-id <user>@<server-ip>

On Windows, PowerShell doesn’t have ssh-copy-id. This one line does the same thing:

type $env:USERPROFILE\.ssh\id_ed25519.pub | ssh <user>@<server-ip> "mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 700 ~/.ssh && chmod 600 ~/.ssh/authorized_keys"

The first time you connect to the server, you’ll see Are you sure you want to continue connecting (yes/no)?. Type yes and press Enter. Then type your server password. This is the last time you’ll need it for SSH.

Now test it. On your laptop:

ssh <user>@<server-ip>

It should log you in without asking for the server password. It will ask for the key’s passphrase instead, which is what you want. If it still asks for the server password, the key didn’t get copied; go back and run the copy command again.

Leave this window logged in until the end of this step. If anything goes wrong with the next part, this open session is your way back in.

Now, on the server (you can use the window you just opened), look at the SSH settings folder:

ls /etc/ssh/sshd_config.d/

On many Ubuntu installs you’ll see a file called 50-cloud-init.conf. The installer often puts PasswordAuthentication yes in it, which turns password login on. SSH reads the files in this folder in alphabetical order and keeps the first value it finds for each setting. So we put our settings in a file whose name starts with 10-, which comes first and wins.

Remember to replace <user> in the box below with your username:

sudo tee /etc/ssh/sshd_config.d/10-hardening.conf <<'EOF'
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitRootLogin no
AllowUsers <user>
MaxAuthTries 3
X11Forwarding no
EOF
sudo sshd -t && sudo systemctl try-reload-or-restart ssh
sudo sshd -T | grep -E '^(passwordauthentication|permitrootlogin|allowusers)'

What the settings mean, in order: no passwords, no “type a code” logins either, nobody can log in directly as root, only your user may log in at all, three wrong tries and the connection is dropped, and no forwarding of graphical windows (you won’t miss it).

The last two lines check the file for mistakes, apply it, and print the result. You should see:

permitrootlogin no
passwordauthentication no
allowusers <user>

with your real username on the last line. If sshd -t printed an error instead, nothing was applied: read the error, fix the file with sudo nano /etc/ssh/sshd_config.d/10-hardening.conf, and run the last two lines again.

Before you close your open session, open a second terminal on your laptop and log in again with ssh <user>@<server-ip>. If that works, you’re done with this step.

Step 4: Put a box around what you open to the internet

You can skip this step if nothing on your server can be reached from the internet. If you’ve set up port forwarding on your router so friends or family can reach an app from outside (a media server is the usual example), keep reading.

Any app can have a security bug. If yours is reachable from the internet, assume that one day someone will find one. The question is what they can do next. Linux’s service manager, systemd, can put limits around the app without changing the app itself: it can’t change system files, can’t gain extra privileges, and can’t talk to the addresses you forbid, like your router’s admin page.

First, find the name of the app’s service. On the server:

systemctl list-units --type=service --state=running

Look for your app in the list. For example, Jellyfin’s service is called jellyfin.service. In the boxes below, replace every myapp with that name, without the .service part.

Note the app’s current safety score, so you can compare later:

systemd-analyze security myapp.service | tail -1

Now create the box. On the server (replace myapp, and <router-ip>):

sudo mkdir -p /etc/systemd/system/myapp.service.d
sudo tee /etc/systemd/system/myapp.service.d/20-box.conf <<'EOF'
[Service]
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=full
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectControlGroups=yes
RestrictSUIDSGID=yes
IPAddressDeny=<router-ip>/32
EOF
sudo systemctl daemon-reload
sudo systemctl restart myapp

This adds an extra settings file next to the app’s own, so app updates don’t overwrite it. In plain words, the settings say: the app can’t give itself more rights than it started with, gets its own private temporary folder, can’t change system programs or settings, can’t load or tweak kernel features, and can’t talk to your router at all. The /32 means “this one address”.

A word on IPAddressDeny: it blocks traffic in both directions. Only list addresses the app never needs: your router, and whole networks where none of its users are (for example, a separate network for your work laptop, written like 192.168.20.0/24). Never list the network your users are on, or they won’t be able to connect.

Now use the app the way you normally do and check it still works. Then look at the score again:

systemd-analyze security myapp.service | tail -1

The number should be lower than before. Don’t expect a great score: on my test server it went from 8.2 to 7.5 and still said “EXPOSED”. That’s fine. The score counts dozens of settings, and most of them would break a typical app. The ones above are safe for almost any app, and the network rule matters most.

Test it

On your laptop, try to log in with a password on purpose:

ssh -o PubkeyAuthentication=no <user>@<server-ip>

You should get Permission denied (publickey). That means passwords are refused.

On the server, check that the network box really blocks the router. These two lines run a quick ping to the router, the first time with the same rule as the box, the second time without it:

sudo systemd-run --wait --pipe -p IPAddressDeny=<router-ip>/32 ping -c1 -W2 <router-ip>
sudo systemd-run --wait --pipe ping -c1 -W2 <router-ip>

The first should fail: 100% packet loss or Operation not permitted. The second should say 0% packet loss.

Last, restart the server once (sudo reboot), wait a minute, and check that everything comes back: log in from your laptop with SSH, run sudo ufw status (it should say active), and use your app. Problems that only show up after a restart are the worst kind to discover months later.

If it doesn’t work

How to undo it

Each step can be undone on its own: