nixos-vps/docs/superpowers/specs/2026-03-18-adguard-home-design.md
ashisgreat22 053198d013 docs: add AdGuard Home module design spec
Design for private DoH server with ClientID-based access control.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-18 18:57:54 +01:00

7.4 KiB

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

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:

adguard_client_phone: "xK9mP2vL7nQ4"
adguard_client_laptop: "jH3fR8wT5yB1"

Module Configuration

In configuration.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:

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

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)