Kuuko / Build it yourself

Build it yourself · Part 1Checked: 30 October 2026

Chapter 3: A small server in the EU

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:

    ProtocolPortFromWhy
    TCP22your home IP onlySSH, for you
    ICMP–anyping, for checks
    TCP80anycertificate renewal (chapter 5)
    TCP443anyencrypted 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>.

    #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 · Next: Chapter 4: The resolver

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