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:
parent
638d588d81
commit
053198d013
1 changed files with 216 additions and 0 deletions
216
docs/superpowers/specs/2026-03-18-adguard-home-design.md
Normal file
216
docs/superpowers/specs/2026-03-18-adguard-home-design.md
Normal 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)
|
||||
Loading…
Add table
Add a link
Reference in a new issue