diff options
| -rw-r--r-- | .gitignore | 3 | ||||
| -rw-r--r-- | CLAUDE.md | 33 | ||||
| -rw-r--r-- | README.md | 36 | ||||
| -rwxr-xr-x | deploy.sh | 19 | ||||
| -rw-r--r-- | modules/adguard.nix | 27 | ||||
| -rw-r--r-- | modules/common.nix | 13 |
6 files changed, 81 insertions, 50 deletions
@@ -1 +1,2 @@ -*.pub
\ No newline at end of file +*.pub +.env
\ No newline at end of file @@ -18,20 +18,14 @@ Check the flake evaluates without deploying: nix flake check ``` -Deploy a change (from an x86_64-linux machine): +Deploy a change: ```bash -nixos-rebuild switch --flake .#eurovm \ - --target-host admin@<server-ipv4> \ - --elevate=sudo -``` - -Deploy from a non-x86_64-linux machine (e.g. Apple Silicon) — the server builds its own closure: -```bash -nixos-rebuild switch --flake .#eurovm \ - --target-host admin@<server-ipv4> \ - --build-host admin@<server-ipv4> \ - --elevate=sudo +./deploy.sh ``` +`deploy.sh` reads `SERVER_IP` from a git-ignored `.env` file (copy `.env.example` to start) — +the server's Tailscale IP — and runs `nixos-rebuild switch --flake .#eurovm` against it with +both `--target-host` and `--build-host` set to that address, so the server always builds its +own closure regardless of the local machine's architecture. First-time provisioning of a fresh VM (partitions the disk via disko, installs NixOS): ```bash @@ -71,8 +65,9 @@ service module: **Service modules (`modules/`)** are one file per concern, each self-contained and imported by `hosts/eurovm/default.nix`: -- `common.nix` — SSH hardening (key-only, no root login), firewall (22/80/443/53 only), timezone, - nix settings (flakes, auto-gc, auto-optimise-store), base packages, disabled autoUpgrade. +- `common.nix` — SSH hardening (key-only, no root login), firewall (22/80/443/853/8443 only — + plain DNS on 53 is deliberately closed, see `adguard.nix`), timezone, nix settings (flakes, + auto-gc, auto-optimise-store), base packages, disabled autoUpgrade. - `sops.nix` — sops-nix wiring. The host's own age key is derived at boot from `/etc/ssh/ssh_host_ed25519_key`, so no separate host key material needs to be provisioned. Declares which secrets exist, their owning user/group, and file mode. @@ -88,9 +83,13 @@ service module: 8080, 8086) are bound to `127.0.0.1` and never exposed directly — Caddy is the public HTTP(S) entry point for everything except AdGuard's encrypted-DNS ports, and handles Let's Encrypt automatically for its own certs. -- `adguard.nix` — AdGuard Home; plain DNS on `0.0.0.0:53`, DNS-over-TLS on `0.0.0.0:853`, - DNS-over-HTTPS on `0.0.0.0:8443` (path `/dns-query`), web UI on `0.0.0.0:3000` (port 3000 is - never opened in the firewall, so it's still effectively internal-only, reached via Caddy). +- `adguard.nix` — AdGuard Home; plain DNS (`dns.port`) is set to `0` and deliberately not opened + in the firewall — CERT-Bund/BSI flagged this host as an open DNS resolver abusable for UDP + reflection/amplification DDoS (spoofed source IP, no handshake needed), so plain DNS is + disabled entirely and DNS-over-TLS (`0.0.0.0:853`) / DNS-over-HTTPS (`0.0.0.0:8443`, path + `/dns-query`) — both TLS-handshake-based and therefore not spoofable the same way — are the + only supported client paths. Web UI on `0.0.0.0:3000` (port 3000 is never opened in the + firewall, so it's still effectively internal-only, reached via Caddy). `http.address` binds `0.0.0.0` rather than `127.0.0.1` specifically because AdGuard's HTTPS/DoH listener reuses that same bind host internally (confirmed from AdGuard's source) — narrowing it back to loopback would silently make DoH unreachable. TLS for DoH/DoT uses the @@ -12,8 +12,9 @@ A flake-based, modular NixOS configuration for a personal server running in Euro | https://git.karanj.com | cgit (git repository browser) | All services are reverse-proxied by **Caddy** with automatic TLS via Let's Encrypt. -AdGuard also listens directly on **port 53 (UDP + TCP)** for plain DNS, **853/tcp** for -DNS-over-TLS, and **8443/tcp** for DNS-over-HTTPS - see [AdGuard Home](#adguard-home) below. +AdGuard also listens directly on **853/tcp** for DNS-over-TLS and **8443/tcp** for +DNS-over-HTTPS - see [AdGuard Home](#adguard-home) below. Plain DNS (port 53) is disabled; +see the note there for why. --- @@ -205,16 +206,13 @@ Re-encrypt secrets with the new recipient: sops updatekeys secrets/secrets.yaml ``` -Commit and push `.sops.yaml` and the re-encrypted `secrets/secrets.yaml`, then deploy: +Commit and push `.sops.yaml` and the re-encrypted `secrets/secrets.yaml`, then deploy. Copy +`.env.example` to `.env`, set `SERVER_IP` to the server's Tailscale IP (the server must be +joined to your tailnet first), then: ```bash -nixos-rebuild switch --flake .#eurovm \ - --target-host admin@<server-ipv4> \ - --build-host admin@<server-ipv4> \ - --elevate=sudo +./deploy.sh ``` -If your local machine is also x86_64-linux you can omit `--build-host`. - ### Step 7 - First login to each service | Service | URL | Action | @@ -224,7 +222,8 @@ If your local machine is also x86_64-linux you can omit `--build-host`. | Actual Budget | https://budget.karanj.com | Set a server password in the browser on first visit - no pre-configuration needed | | cgit | https://git.karanj.com | No login needed - public read-only | -To use the server as your device's DNS resolver, point its DNS settings to `<server-ipv4>`. +To use the server as your device's DNS resolver, configure DNS-over-TLS or DNS-over-HTTPS as +described under [AdGuard Home](#adguard-home) below - plain DNS (port 53) is disabled. ### Step 8 - (Optional) enable automatic OS updates @@ -238,13 +237,12 @@ If you push this repo to GitHub/Forgejo/cgit, update the `autoUpgrade.flake` lin ### Deploy updates ```bash -nixos-rebuild switch --flake .#eurovm \ - --target-host admin@<server-ipv4> \ - --build-host admin@<server-ipv4> \ - --elevate=sudo +./deploy.sh ``` -If your local machine is also x86_64-linux you can omit `--build-host`. +Reads `SERVER_IP` from `.env` (copy `.env.example` if you haven't already) and runs +`nixos-rebuild switch` against it over Tailscale, with `--build-host` so the server always +builds its own closure regardless of your local machine's architecture. ### Edit secrets ```bash @@ -280,7 +278,9 @@ To reset the password: # Then: systemctl restart adguardhome ``` -**Encrypted DNS client setup.** Point client devices at: +**Encrypted DNS client setup.** Plain DNS (port 53) is disabled - CERT-Bund/BSI flagged it as +an abusable open resolver (spoofable UDP reflection/amplification DDoS vector). Point client +devices at encrypted DNS instead: - **DNS-over-TLS:** `tls://dns.karanj.com:853` - **DNS-over-HTTPS:** `https://dns.karanj.com:8443/dns-query` @@ -289,6 +289,9 @@ automatically via `security.acme` - see `modules/acme.nix`), independently of Ca certificate for the same domain. Nothing to configure manually; the cert renews on its own and restarts `adguardhome` automatically when it does. +On iOS, an app like **DNS Override** can install the DoT/DoH settings as a signed +configuration profile (Settings -> General -> VPN & Device Management) without needing an MDM. + ### Miniflux To reset the password from the server: @@ -325,7 +328,6 @@ Caddy logs: `journalctl -u caddy -f` | 22 | TCP | SSH (admin + git push) | | 80 | TCP | HTTP (Caddy redirects to HTTPS; also serves the ACME HTTP-01 webroot) | | 443 | TCP | HTTPS (all web services, via Caddy) | -| 53 | TCP + UDP | Plain DNS (AdGuard Home) | | 853 | TCP | DNS-over-TLS (AdGuard Home) | | 8443 | TCP | DNS-over-HTTPS (AdGuard Home) | diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 0000000..c5ae6b6 --- /dev/null +++ b/deploy.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "${BASH_SOURCE[0]}")" + +if [ ! -f .env ]; then + echo "Missing .env - copy .env.example to .env and set SERVER_IP (the server's Tailscale IP)." >&2 + exit 1 +fi + +set -a +source .env +set +a + +: "${SERVER_IP:?SERVER_IP must be set in .env}" + +nixos-rebuild switch --flake .#eurovm \ + --target-host "admin@${SERVER_IP}" \ + --build-host "admin@${SERVER_IP}" \ + --elevate=sudo diff --git a/modules/adguard.nix b/modules/adguard.nix index e66d072..35cafc3 100644 --- a/modules/adguard.nix +++ b/modules/adguard.nix @@ -2,12 +2,17 @@ # AdGuard Home - DNS resolver + optional network-wide ad blocker. # -# Plain DNS listens on port 53 (udp/tcp) directly on the public IP. -# DNS-over-TLS (853/tcp) and DNS-over-HTTPS (8443/tcp, path /dns-query) are -# also served directly by AdGuard itself, terminating TLS with their own -# independent Let's Encrypt cert (modules/acme.nix) - NOT via Caddy, since -# Caddy already owns port 443 for its own reverse-proxied vhosts and -# AdGuard's HTTPS/DoH listener needs its own port. +# Plain DNS (port 53, udp/tcp) is disabled (dns.port = 0 below): CERT-Bund/BSI +# flagged this host as an open DNS resolver, abusable for UDP reflection/ +# amplification DDoS since a spoofed source IP gets an amplified response with +# no handshake required. DNS-over-TLS (853/tcp) and DNS-over-HTTPS (8443/tcp, +# path /dns-query) are unaffected by this - both require a real TLS handshake +# with the real client IP, so they aren't spoofable the same way - and remain +# the only supported way to use this resolver. They're served directly by +# AdGuard itself, terminating TLS with their own independent Let's Encrypt +# cert (modules/acme.nix) - NOT via Caddy, since Caddy already owns port 443 +# for its own reverse-proxied vhosts and AdGuard's HTTPS/DoH listener needs +# its own port. # # AdGuard's HTTPS/DoH listener binds to the same host as the plain `http` # listener below (confirmed in AdGuardHome's source: both come from @@ -45,11 +50,11 @@ dns = { bind_hosts = [ "0.0.0.0" ]; - port = 53; - # Clients configure this server's public IPv4 address (not - # dns.karanj.com - that hostname only resolves to the HTTPS web UI - # via Caddy) as their plain DNS resolver, e.g. <server-ipv4>:53. - # For encrypted DNS, see the client setup notes in README.md. + # port = 0 disables the plain UDP/TCP:53 listener entirely; it does + # not affect the DoT/DoH listeners below, which are configured + # separately under `tls` and stay on. Clients use DoT/DoH exclusively + # - see the client setup notes in README.md. + port = 0; # Upstream DNS resolvers (privacy-respecting) upstream_dns = [ "https://dns.quad9.net/dns-query" diff --git a/modules/common.nix b/modules/common.nix index e27d276..7f46af8 100644 --- a/modules/common.nix +++ b/modules/common.nix @@ -15,14 +15,19 @@ }; }; - # Firewall: SSH, HTTP/HTTPS (Caddy), plain DNS, and AdGuard's own - # DNS-over-TLS (853) + DNS-over-HTTPS (8443) listeners. + # Firewall: SSH, HTTP/HTTPS (Caddy), and AdGuard's own DNS-over-TLS (853) + + # DNS-over-HTTPS (8443) listeners. + # Plain DNS (port 53) is deliberately NOT opened: CERT-Bund/BSI flagged this + # host as an open DNS resolver abusable for UDP reflection/amplification + # DDoS (spoofed source IP, no handshake). AdGuard's plain listener is + # disabled entirely in modules/adguard.nix (dns.port = 0) - all clients use + # DoT/DoH instead, which require a real TLS handshake with the real client + # IP and so aren't spoofable the same way. # Port 3000 (AdGuard's plain-HTTP web UI) is deliberately NOT opened here - # see modules/adguard.nix for why it still binds 0.0.0.0 anyway. networking.firewall = { enable = true; - allowedTCPPorts = [ 22 80 443 53 853 8443 ]; - allowedUDPPorts = [ 53 ]; + allowedTCPPorts = [ 22 80 443 853 8443 ]; }; # Nix settings: flakes, auto-gc, auto-optimise |
