nixos-vps/docs/superpowers/plans/2026-03-18-adguard-home.md
ashisgreat22 30d5ce8134 docs: add AdGuard Home implementation plan
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-18 19:06:42 +01:00

11 KiB

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:

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

  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):

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

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

    # 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

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 <noreply@anthropic.com>"

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:

{
  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
git add modules/default.nix
git commit -m "feat(modules): register adguard module in default.nix

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>"

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):

  # === 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
git add configuration.nix
git commit -m "feat(config): enable AdGuard Home module with two clients

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>"

Task 4: Add ClientID Secrets

Files:

  • Modify: secrets/secrets.yaml

  • Step 1: Add ClientID secrets to SOPS

Edit secrets/secrets.yaml using SOPS:

nix-shell -p sops --run "sops secrets/secrets.yaml"

Add these entries (replace with your actual secret ClientIDs):

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
# 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
git add secrets/secrets.yaml
git commit -m "chore(secrets): add AdGuard ClientID secrets

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>"

Task 5: Build and Deploy

  • Step 1: Dry-run to verify configuration
nixos-rebuild build --flake .#nixos

Expected: Build succeeds without errors

  • Step 2: Deploy to system
sudo nixos-rebuild switch --flake .#nixos

Expected: System switches to new configuration, AdGuard container starts

  • Step 3: Verify container is running
sudo podman ps | grep adguard

Expected: Shows adguard container running, port 127.0.0.1:3000->3000/tcp

  • Step 4: Verify nginx configuration
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:

# 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