# Build it yourself · Part 1

## Chapter 3: A small server in the EU

> Markdown copy of https://kuuko.app/build/server/ for agents; the HTML page is the source. Part 1 of Kuuko's guide to building the core yourself. Text: CC BY-SA 4.0 · Commands and configs: CC0.

*Checked: 30 October 2026*

---

### Why

Your devices will send every name they look up to this machine. It should be one you control, in a place whose privacy law you know, and reachable only the way you decide.

### Our choice, and the alternatives

We use **Hetzner Cloud**, in Germany and Finland. Any provider that gives you a small Ubuntu server with a public IPv4 and IPv6 address and a firewall in front of it will do; look for one in the EU if that matters to you.

A resolver for a household is light work. In our setup, the resolver with a few large block lists used about 190 MB of memory. The smallest server is enough.

### What you need

- An SSH key on your Mac. If you don't have one:

  ```
  ssh-keygen -t ed25519 -C "dns-server"
  ```

  Press Enter to accept the file name, and choose a passphrase. Your public key is now in `~/.ssh/id_ed25519.pub`.
- Your home's public IP address, for the firewall. Any "what is my IP" page shows it.
- About 30 minutes (our estimate).

### Steps

1. **Create a separate project** in the Hetzner Cloud console, only for this server. It keeps its firewall and keys apart from anything else you run.
2. **Add your SSH public key** to the project (Security → SSH keys). Paste the contents of `~/.ssh/id_ed25519.pub`.
3. **Create a firewall** (Firewalls → Create firewall) with these inbound rules:

   | Protocol | Port | From | Why |
   |---|---|---|---|
   | TCP | 22 | your home IP only | SSH, for you |
   | ICMP | – | any | ping, for checks |
   | TCP | 80 | any | certificate renewal (chapter 5) |
   | TCP | 443 | any | encrypted DNS for your devices (chapter 4) |

   Leave outbound open: your resolver needs to ask other servers.
4. **Create the server:** location in the EU, image **Ubuntu** (the newest LTS version), type **CX23** or the smallest available. Tick your SSH key. Under **Firewalls**, choose the one from step 3. Under **Cloud config**, paste the text below, with your own public key in place of `<your-ssh-public-key>`.

   ```yaml
   #cloud-config
   users:
     - name: dns
       groups: [sudo]
       shell: /bin/bash
       sudo: "ALL=(ALL) NOPASSWD:ALL"
       lock_passwd: true
       ssh_authorized_keys:
         - <your-ssh-public-key>
   disable_root: true
   ssh_pwauth: false
   timezone: Etc/UTC
   package_update: true
   package_upgrade: true
   package_reboot_if_required: true
   packages:
     - unattended-upgrades
   write_files:
     - path: /etc/ssh/sshd_config.d/10-dns.conf
       permissions: "0644"
       content: |
         PermitRootLogin no
         PasswordAuthentication no
         KbdInteractiveAuthentication no
         AllowUsers dns
   runcmd:
     - systemctl restart ssh
   ```

   What each part does:
   - `users`: creates one user, `dns`, that logs in only with your key (`lock_passwd: true` means it has no password) and may use `sudo`.
   - `disable_root`, `ssh_pwauth: false`: no login as root, no login with a password.
   - `package_update`, `package_upgrade`, `package_reboot_if_required`: the server starts fully updated.
   - `unattended-upgrades`: installs security updates from Ubuntu by itself from now on.
   - `write_files`: an SSH setting that refuses root, passwords and every user except `dns`, even if something else changes the defaults later.
5. **Check that the firewall is attached:** open Firewalls → your firewall. It must say **"Applied to 1 resource"**. If it says 0, apply it to the server now (see Pitfalls).
6. **Switch on delete protection** for the server (server → Settings), so a wrong click can't remove it.
7. **Note the server's IPv4 and IPv6 addresses.** You need them in chapters 5 and 8. The console may show the IPv6 as a whole network ending in `::/64`; you need the server's own address. After logging in (below), check it with `ip -6 addr show scope global`. At Hetzner it usually ends in `::1`.

### How you know it worked

Wait two or three minutes after creating the server, so the cloud config can finish. Then, from your Mac:

```
ssh dns@<your-server-ip>
```

You're logged in. Now check that the doors you closed are really closed:

```
ssh root@<your-server-ip>
```

must be refused (`Permission denied`), and

```
ssh -o PubkeyAuthentication=no dns@<your-server-ip>
```

must be refused too: no password login.

From another network (for example your phone's hotspot), `ssh dns@<your-server-ip>` must not connect at all. That's the firewall.

### Pitfalls

- **The firewall can stay unattached** even if you chose it while creating the server. Our server ran with every port open until we noticed. Always check "Applied to 1 resource".
- **Your home IP can change.** Many home connections get a new address from time to time. If SSH suddenly stops working, update the firewall rule with your new address before suspecting anything else.
- **The cheapest servers are sometimes sold out.** In October 2026 the CX line was at times unavailable. Take the next size, or try again later.

### Time and cost

- About 30 minutes (our estimate).
- Hetzner CX23: €5.49 a month plus €0.50 for an IPv4 address, without VAT (prices as of October 2026).

---

Previous: [Chapter 2: DNS hosting with DNSSEC](https://kuuko.app/build/dnssec/) · Next: [Chapter 4: The resolver](https://kuuko.app/build/resolver/)

Text: CC BY-SA 4.0 · Commands and configs: CC0
