diff --git a/docs/superpowers/specs/2026-03-18-adguard-home-design.md b/docs/superpowers/specs/2026-03-18-adguard-home-design.md index 9c5c42f..945f806 100644 --- a/docs/superpowers/specs/2026-03-18-adguard-home-design.md +++ b/docs/superpowers/specs/2026-03-18-adguard-home-design.md @@ -87,6 +87,11 @@ myModules.adguard = { │ │ │ │AdGuardHome. │ ← generated by │ │ │ │ │ │ yaml │ Nix from SOPS │ │ │ │ │ └─────────────┘ │ │ +│ │ │ │ │ +│ │ │ ┌─────────────┐ │ │ +│ │ │ │ data/ │ ← persistent vol │ │ +│ │ │ │ (stats,logs)│ │ │ +│ │ │ └─────────────┘ │ │ │ │ └─────────────────────────────────────┘ │ │ │ │ │ ▼ │ @@ -101,7 +106,8 @@ myModules.adguard = { ### Request Flow -1. External client sends DoH request to `https://dns.ashisgreat.xyz/dns-query/{clientId}` +1. External client sends DoH request to `https://dns.ashisgreat.xyz/dns-query/CLIENTID` + - Example: `https://dns.ashisgreat.xyz/dns-query/xK9mP2vL7nQ4` 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 @@ -132,69 +138,154 @@ myModules.adguard = { ]; }; -# SOPS secret declarations +# SOPS secret declarations (auto-created by module or manual) sops.secrets.adguard_client_phone = { }; sops.secrets.adguard_client_laptop = { }; ``` -### Generated AdGuardHome.yaml +## Implementation Details -The module generates the config via SOPS template: +### Container Definition -```yaml -dns: - upstream_dns: - - https://dns.mullvad.net/dns-query - bootstrap_dns: - - 194.242.2.2 - - 2a07:e340::2 +```nix +virtualisation.oci-containers.containers."adguard" = { + image = "docker.io/adguard/adguardhome:latest"; + ports = [ "127.0.0.1:${toString cfg.port}:3000/tcp" ]; + extraOptions = [ + "--cap-drop=ALL" + "--read-only" + "--tmpfs=/tmp" + ]; + volumes = [ + "${config.sops.templates."adguardhome.yaml".path}:/opt/adguardhome/conf/AdGuardHome.yaml:ro" + "adguard-data:/opt/adguardhome/work" + ]; +}; +``` -http: - address: 0.0.0.0:3000 +**Notes:** +- Container runs with minimal capabilities (`--cap-drop=ALL`) +- Config file is read-only (managed by Nix/SOPS) +- `adguard-data` volume persists stats and query logs -clients: - persistent: - - name: phone - ids: - - xK9mP2vL7nQ4 - - name: laptop - ids: - - jH3fR8wT5yB1 +### Data Directory + +```nix +systemd.tmpfiles.rules = [ + "d /var/lib/adguard 0755 root root -" +]; +``` + +### SOPS Template for AdGuardHome.yaml + +```nix +sops.templates."adguardhome.yaml" = { + content = '' + http: + address: 0.0.0.0:3000 + + dns: + upstream_dns: + - ${cfg.upstreamDoh} + bootstrap_dns: + ${lib.concatStringsSep "\n " (map (d: "- ${d}") cfg.bootstrapDns)} + querylog_enabled: true + querylog_file_enabled: true + statistics_enabled: true + + clients: + persistent: + ${lib.concatStringsSep "\n " ( + map (client: '' + - name: ${client.name} + ids: + - ${config.sops.placeholder.${client.idSecret}} + '') cfg.clients + )} + + filtering: + protection_enabled: true + filtering_enabled: true + + safebrowsing: + enabled: false + + parental: + enabled: false + + safesearch: + enabled: false + + log: + file: "" + max_backups: 0 + max_size: 100 + compress: false + local_time: false + verbose: false + ''; +}; +``` + +### SOPS Secret Declarations (Auto-generated) + +The module automatically declares SOPS secrets for each client: + +```nix +sops.secrets = lib.mkMerge ( + map (client: { + ${client.idSecret} = { }; + }) cfg.clients +); ``` ## Nginx Configuration -The module configures nginx directly (not via the nginx module): +The module configures nginx directly (not via the nginx module) because DoH requires special handling: ```nix -services.nginx.virtualHosts."dns.ashisgreat.xyz" = { +services.nginx.virtualHosts."${cfg.domain}" = { enableACME = true; forceSSL = true; - locations."/dns-query" = { - proxyPass = "http://127.0.0.1:3000"; + # Regex location to match /dns-query and /dns-query/{clientId} + locations."~ ^/dns-query" = { + proxyPass = "http://127.0.0.1:${toString cfg.port}"; extraConfig = '' + proxy_http_version 1.1; 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; + + # DoH uses POST with application/dns-message proxy_pass_request_body on; + proxy_set_header Content-Type "application/dns-message"; + + # Buffer settings for DNS queries proxy_buffers 8 16k; proxy_buffer_size 32k; ''; }; + # Block all other paths including admin UI locations."/" = { return = "404"; }; }; + +# Ensure nginx user can access ACME certs +users.users.nginx.extraGroups = [ "acme" ]; + +# Open HTTPS port +networking.firewall.allowedTCPPorts = [ 443 ]; ``` -**Notes:** -- Admin UI not exposed externally -- All config changes via git + `nixos-rebuild switch` -- ACME certificate auto-provisioned +**Security Notes:** +- ClientIDs appear in URL paths and may be logged by nginx +- Consider filtering nginx logs to redact ClientIDs if needed +- Admin UI is completely blocked (returns 404) +- No rate limiting configured (can be added if abuse occurs) ## Firewall