# 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**