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>
This commit is contained in:
ashisgreat22 2026-03-18 18:57:54 +01:00
parent 638d588d81
commit 053198d013

View file

@ -0,0 +1,216 @@
# 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
```nix
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`:
```yaml
adguard_client_phone: "xK9mP2vL7nQ4"
adguard_client_laptop: "jH3fR8wT5yB1"
```
### Module Configuration
In `configuration.nix`:
```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:
```yaml
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):
```nix
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)