Design for private DoH server with ClientID-based access control. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
216 lines
7.4 KiB
Markdown
216 lines
7.4 KiB
Markdown
# 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)
|