diff --git a/docs/superpowers/plans/2026-03-18-adguard-home.md b/docs/superpowers/plans/2026-03-18-adguard-home.md new file mode 100644 index 0000000..81e478a --- /dev/null +++ b/docs/superpowers/plans/2026-03-18-adguard-home.md @@ -0,0 +1,436 @@ +# AdGuard Home Module Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add a NixOS module for AdGuard Home providing private DoH with ClientID-based access control. + +**Architecture:** AdGuard Home runs in a Podman container with a generated config from SOPS secrets. Nginx proxies DoH requests at `dns.ashisgreat.xyz`. ClientIDs act as passwords - only recognized IDs get DNS resolution. + +**Tech Stack:** NixOS, Podman, AdGuard Home, nginx, SOPS-nix + +--- + +## File Structure + +| File | Action | Purpose | +|------|--------|---------| +| `modules/adguard.nix` | Create | Main module with options, container, nginx, SOPS config | +| `modules/default.nix` | Modify | Add import for adguard.nix | +| `configuration.nix` | Modify | Enable module with domain and clients | +| `secrets/secrets.yaml` | Modify | Add ClientID secrets (encrypted) | + +--- + +### Task 1: Create the AdGuard Module File + +**Files:** +- Create: `modules/adguard.nix` + +- [ ] **Step 1: Create module skeleton with options** + +Create `modules/adguard.nix` with the module header, options definition, and empty config block: + +```nix +# AdGuard Home Module +# Provides: Private DNS-over-HTTPS with ClientID-based access control +# +# Usage: +# myModules.adguard = { +# enable = true; +# domain = "dns.example.com"; +# clients = [ +# { name = "phone"; idSecret = "adguard_client_phone"; } +# ]; +# }; + +{ + config, + lib, + pkgs, + ... +}: + +let + cfg = config.myModules.adguard; +in +{ + options.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"; + }; + }; + + config = lib.mkIf cfg.enable { + # Implementation in next step + }; +} +``` + +- [ ] **Step 2: Add podman dependency and container definition** + +Add inside the `config = lib.mkIf cfg.enable { }` block: + +```nix + config = lib.mkIf cfg.enable { + # Ensure Podman is enabled + myModules.podman.enable = true; + + # AdGuard Home Container + 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" + ]; + }; +``` + +- [ ] **Step 3: Add SOPS template for AdGuardHome.yaml** + +Add after the container definition (still inside config block): + +```nix + # SOPS template for AdGuard configuration + 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 + ''; + }; +``` + +- [ ] **Step 4: Add SOPS secret declarations for clients** + +Add after the SOPS template: + +```nix + # Auto-declare SOPS secrets for each client + sops.secrets = lib.mkMerge ( + map (client: { + ${client.idSecret} = { }; + }) cfg.clients + ); +``` + +- [ ] **Step 5: Add nginx virtual host configuration** + +Add after SOPS secrets: + +```nix + # Nginx configuration for DoH endpoint + services.nginx.virtualHosts."${cfg.domain}" = { + enableACME = true; + forceSSL = true; + + # 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; + + # 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" ]; + }; +} +``` + +- [ ] **Step 6: Verify complete module file** + +The complete `modules/adguard.nix` should have: +- Module header comment with usage example +- All 6 options (enable, domain, port, upstreamDoh, bootstrapDns, clients) +- Container definition with security options +- SOPS template generating AdGuardHome.yaml +- SOPS secret declarations for clients +- Nginx virtual host with DoH proxy config + +- [ ] **Step 7: Commit the module** + +```bash +git add modules/adguard.nix +git commit -m "feat(modules): add AdGuard Home module with DoH and ClientID support + +Co-Authored-By: Claude Opus 4.6 " +``` + +--- + +### Task 2: Register Module in default.nix + +**Files:** +- Modify: `modules/default.nix` + +- [ ] **Step 1: Add adguard.nix import** + +Add `./adguard.nix` to the imports list in `modules/default.nix`: + +```nix +{ + imports = [ + ./system.nix + ./podman.nix + ./nginx.nix + ./searxng.nix + ./openclaw-podman.nix + ./vaultwarden.nix + ./crowdsec.nix + ./backup.nix + ./adguard.nix # Add this line + ]; +} +``` + +- [ ] **Step 2: Commit the change** + +```bash +git add modules/default.nix +git commit -m "feat(modules): register adguard module in default.nix + +Co-Authored-By: Claude Opus 4.6 " +``` + +--- + +### Task 3: Enable Module in configuration.nix + +**Files:** +- Modify: `configuration.nix` + +- [ ] **Step 1: Add AdGuard module configuration** + +Add after the existing module configurations (around line 88, after nginx config): + +```nix + # === AdGuard Home (DoH) === + myModules.adguard = { + enable = true; + domain = "dns.ashisgreat.xyz"; + clients = [ + { name = "phone"; idSecret = "adguard_client_phone"; } + { name = "laptop"; idSecret = "adguard_client_laptop"; } + ]; + }; +``` + +- [ ] **Step 2: Commit the change** + +```bash +git add configuration.nix +git commit -m "feat(config): enable AdGuard Home module with two clients + +Co-Authored-By: Claude Opus 4.6 " +``` + +--- + +### Task 4: Add ClientID Secrets + +**Files:** +- Modify: `secrets/secrets.yaml` + +- [ ] **Step 1: Add ClientID secrets to SOPS** + +Edit `secrets/secrets.yaml` using SOPS: + +```bash +nix-shell -p sops --run "sops secrets/secrets.yaml" +``` + +Add these entries (replace with your actual secret ClientIDs): + +```yaml +adguard_client_phone: "your-secret-phone-client-id" +adguard_client_laptop: "your-secret-laptop-client-id" +``` + +**Note:** Choose random strings for ClientIDs. They act as passwords. Example format: `xK9mP2vL7nQ4` + +- [ ] **Step 2: Verify secrets are encrypted** + +```bash +# Should show encrypted content, not plaintext +head -5 secrets/secrets.yaml | grep -c ENC +# Expected: output > 0 (shows encrypted entries exist) +``` + +- [ ] **Step 3: Commit the encrypted secrets** + +```bash +git add secrets/secrets.yaml +git commit -m "chore(secrets): add AdGuard ClientID secrets + +Co-Authored-By: Claude Opus 4.6 " +``` + +--- + +### Task 5: Build and Deploy + +- [ ] **Step 1: Dry-run to verify configuration** + +```bash +nixos-rebuild build --flake .#nixos +``` + +Expected: Build succeeds without errors + +- [ ] **Step 2: Deploy to system** + +```bash +sudo nixos-rebuild switch --flake .#nixos +``` + +Expected: System switches to new configuration, AdGuard container starts + +- [ ] **Step 3: Verify container is running** + +```bash +sudo podman ps | grep adguard +``` + +Expected: Shows adguard container running, port 127.0.0.1:3000->3000/tcp + +- [ ] **Step 4: Verify nginx configuration** + +```bash +sudo nginx -t +``` + +Expected: `syntax is ok` and `test is successful` + +- [ ] **Step 5: Test DoH endpoint (manual)** + +From a client device configured with the DoH URL: +``` +https://dns.ashisgreat.xyz/dns-query/your-secret-phone-client-id +``` + +Or test with curl: +```bash +# Create a simple DNS query for google.com (A record) +# This is a base64url-encoded DNS query +curl -H "content-type: application/dns-message" \ + --data-binary @- \ + "https://dns.ashisgreat.xyz/dns-query/YOUR_CLIENT_ID" \ + < /dev/null +# Note: Real DNS query needs proper wire format, this just tests connectivity +``` + +--- + +## Summary + +| Task | Description | Commits | +|------|-------------|---------| +| 1 | Create adguard.nix module | 1 | +| 2 | Register in default.nix | 1 | +| 3 | Enable in configuration.nix | 1 | +| 4 | Add secrets to SOPS | 1 | +| 5 | Build, deploy, verify | 0 (deployment) | + +**Total: 4 commits + deployment**