From 053198d013dbeb6217f3eec2ae885b18fe672164 Mon Sep 17 00:00:00 2001 From: ashisgreat22 Date: Wed, 18 Mar 2026 18:57:54 +0100 Subject: [PATCH] docs: add AdGuard Home module design spec Design for private DoH server with ClientID-based access control. Co-Authored-By: Claude Opus 4.6 --- .../specs/2026-03-18-adguard-home-design.md | 216 ++++++++++++++++++ 1 file changed, 216 insertions(+) create mode 100644 docs/superpowers/specs/2026-03-18-adguard-home-design.md diff --git a/docs/superpowers/specs/2026-03-18-adguard-home-design.md b/docs/superpowers/specs/2026-03-18-adguard-home-design.md new file mode 100644 index 0000000..9c5c42f --- /dev/null +++ b/docs/superpowers/specs/2026-03-18-adguard-home-design.md @@ -0,0 +1,216 @@ +# AdGuard Home Module Design + +**Date:** 2026-03-18 +**Status:** Approved + +## Overview + +Add a NixOS module for AdGuard Home that provides private DNS-over-HTTPS (DoH) with ClientID-based access control. Only requests with recognized ClientIDs are processed; unknown requests are dropped. + +## Requirements + +- AdGuard Home running in Podman container +- DoH via nginx at `dns.ashisgreat.xyz` +- ClientID-based authentication (acts as password for DNS) +- ClientIDs stored as SOPS secrets +- Upstream DNS: Mullvad DoH (`https://dns.mullvad.net/dns-query`) +- Port 53 blocked externally (already handled by existing firewall) +- No admin UI exposed externally + +## Module Options + +```nix +myModules.adguard = { + enable = lib.mkEnableOption "AdGuard Home DNS server"; + + domain = lib.mkOption { + type = lib.types.str; + example = "dns.example.com"; + description = "Public domain name for DoH endpoint"; + }; + + port = lib.mkOption { + type = lib.types.port; + default = 3000; + description = "Internal port for AdGuard HTTP/DoH listener"; + }; + + upstreamDoh = lib.mkOption { + type = lib.types.str; + default = "https://dns.mullvad.net/dns-query"; + description = "Upstream DoH server URL"; + }; + + bootstrapDns = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ "194.242.2.2" "2a07:e340::2" ]; + description = "Bootstrap DNS servers for resolving DoH upstream"; + }; + + clients = lib.mkOption { + type = lib.types.listOf (lib.types.submodule { + options = { + name = lib.mkOption { + type = lib.types.str; + description = "Friendly name for the client device"; + }; + idSecret = lib.mkOption { + type = lib.types.str; + description = "SOPS secret name containing the ClientID"; + }; + }; + }); + default = [ ]; + description = "List of clients with their ClientID secrets"; + }; +}; +``` + +## Architecture + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ VPS Host │ +│ │ +│ ┌─────────────┐ ┌─────────────────────────────────────┐ │ +│ │ nginx │ │ adguard container │ │ +│ │ (host) │ │ ┌─────────────────────────────┐ │ │ +│ │ │ │ │ AdGuard Home │ │ │ +│ │ dns.ashis │─────▶│ │ │ │ │ +│ │ .great.xyz │:3000 │ │ - DoH server on :3000 │ │ │ +│ │ │ │ │ - Validates ClientIDs │ │ │ +│ │ /dns-query/ │ │ │ - Proxies to Mullvad DoH │ │ │ +│ │ {clientId} │ │ └─────────────────────────────┘ │ │ +│ └─────────────┘ │ ▲ │ │ +│ │ │ │ read-only │ │ +│ │ │ ┌──────┴──────┐ │ │ +│ │ │ │AdGuardHome. │ ← generated by │ │ +│ │ │ │ yaml │ Nix from SOPS │ │ +│ │ │ └─────────────┘ │ │ +│ │ └─────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌─────────────┐ │ +│ │ Firewall │ │ +│ │ │ │ +│ │ Block 53/udp│ ← External port 53 blocked │ +│ │ Block 53/tcp│ │ +│ └─────────────┘ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### Request Flow + +1. External client sends DoH request to `https://dns.ashisgreat.xyz/dns-query/{clientId}` +2. Nginx handles TLS termination and proxies to AdGuard container on `127.0.0.1:3000` +3. AdGuard validates the ClientID in the request path +4. Unknown ID → Connection dropped +5. Valid ID → Query resolved via Mullvad DoH upstream + +## ClientID Configuration + +### SOPS Secrets + +ClientIDs are stored encrypted in `secrets/secrets.yaml`: + +```yaml +adguard_client_phone: "xK9mP2vL7nQ4" +adguard_client_laptop: "jH3fR8wT5yB1" +``` + +### Module Configuration + +In `configuration.nix`: + +```nix +myModules.adguard = { + enable = true; + domain = "dns.ashisgreat.xyz"; + clients = [ + { name = "phone"; idSecret = "adguard_client_phone"; } + { name = "laptop"; idSecret = "adguard_client_laptop"; } + ]; +}; + +# SOPS secret declarations +sops.secrets.adguard_client_phone = { }; +sops.secrets.adguard_client_laptop = { }; +``` + +### Generated AdGuardHome.yaml + +The module generates the config via SOPS template: + +```yaml +dns: + upstream_dns: + - https://dns.mullvad.net/dns-query + bootstrap_dns: + - 194.242.2.2 + - 2a07:e340::2 + +http: + address: 0.0.0.0:3000 + +clients: + persistent: + - name: phone + ids: + - xK9mP2vL7nQ4 + - name: laptop + ids: + - jH3fR8wT5yB1 +``` + +## Nginx Configuration + +The module configures nginx directly (not via the nginx module): + +```nix +services.nginx.virtualHosts."dns.ashisgreat.xyz" = { + enableACME = true; + forceSSL = true; + + locations."/dns-query" = { + proxyPass = "http://127.0.0.1:3000"; + extraConfig = '' + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_method POST; + proxy_pass_request_body on; + proxy_buffers 8 16k; + proxy_buffer_size 32k; + ''; + }; + + locations."/" = { + return = "404"; + }; +}; +``` + +**Notes:** +- Admin UI not exposed externally +- All config changes via git + `nixos-rebuild switch` +- ACME certificate auto-provisioned + +## Firewall + +No changes needed. Port 53 is already blocked by the existing firewall configuration since it's not in `allowedTCPPorts`. Only ports 22 (SSH), 80, and 443 (via nginx) are open. + +## Files to Create/Modify + +| File | Action | +|------|--------| +| `modules/adguard.nix` | Create - new module | +| `modules/default.nix` | Modify - add import | +| `configuration.nix` | Modify - enable module with clients | +| `secrets/secrets.yaml` | Modify - add ClientID secrets | + +## Dependencies + +- `myModules.podman` - container runtime +- `sops-nix` - secrets management +- `services.nginx` - reverse proxy (shared with other modules)