docs: address spec review feedback for AdGuard module

- Add complete container definition with security options
- Add SOPS template code with ClientID interpolation
- Fix nginx location to use regex for /dns-query/{clientId}
- Add volume persistence for stats/logs
- Add proxy_http_version for DoH
- Document security considerations

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
ashisgreat22 2026-03-18 19:00:15 +01:00
parent 053198d013
commit 51e723ddad

View file

@ -87,6 +87,11 @@ myModules.adguard = {
│ │ │ │AdGuardHome. │ ← generated by │ │ │ │ │ │AdGuardHome. │ ← generated by │ │
│ │ │ │ yaml │ Nix from SOPS │ │ │ │ │ │ yaml │ Nix from SOPS │ │
│ │ │ └─────────────┘ │ │ │ │ │ └─────────────┘ │ │
│ │ │ │ │
│ │ │ ┌─────────────┐ │ │
│ │ │ │ data/ │ ← persistent vol │ │
│ │ │ │ (stats,logs)│ │ │
│ │ │ └─────────────┘ │ │
│ │ └─────────────────────────────────────┘ │ │ │ └─────────────────────────────────────┘ │
│ │ │ │ │ │
│ ▼ │ │ ▼ │
@ -101,7 +106,8 @@ myModules.adguard = {
### Request Flow ### Request Flow
1. External client sends DoH request to `https://dns.ashisgreat.xyz/dns-query/{clientId}` 1. External client sends DoH request to `https://dns.ashisgreat.xyz/dns-query/CLIENTID`
- Example: `https://dns.ashisgreat.xyz/dns-query/xK9mP2vL7nQ4`
2. Nginx handles TLS termination and proxies to AdGuard container on `127.0.0.1:3000` 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 3. AdGuard validates the ClientID in the request path
4. Unknown ID → Connection dropped 4. Unknown ID → Connection dropped
@ -132,69 +138,154 @@ myModules.adguard = {
]; ];
}; };
# SOPS secret declarations # SOPS secret declarations (auto-created by module or manual)
sops.secrets.adguard_client_phone = { }; sops.secrets.adguard_client_phone = { };
sops.secrets.adguard_client_laptop = { }; sops.secrets.adguard_client_laptop = { };
``` ```
### Generated AdGuardHome.yaml ## Implementation Details
The module generates the config via SOPS template: ### Container Definition
```yaml ```nix
dns: virtualisation.oci-containers.containers."adguard" = {
upstream_dns: image = "docker.io/adguard/adguardhome:latest";
- https://dns.mullvad.net/dns-query ports = [ "127.0.0.1:${toString cfg.port}:3000/tcp" ];
bootstrap_dns: extraOptions = [
- 194.242.2.2 "--cap-drop=ALL"
- 2a07:e340::2 "--read-only"
"--tmpfs=/tmp"
];
volumes = [
"${config.sops.templates."adguardhome.yaml".path}:/opt/adguardhome/conf/AdGuardHome.yaml:ro"
"adguard-data:/opt/adguardhome/work"
];
};
```
http: **Notes:**
- Container runs with minimal capabilities (`--cap-drop=ALL`)
- Config file is read-only (managed by Nix/SOPS)
- `adguard-data` volume persists stats and query logs
### Data Directory
```nix
systemd.tmpfiles.rules = [
"d /var/lib/adguard 0755 root root -"
];
```
### SOPS Template for AdGuardHome.yaml
```nix
sops.templates."adguardhome.yaml" = {
content = ''
http:
address: 0.0.0.0:3000 address: 0.0.0.0:3000
clients: 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: persistent:
- name: phone ${lib.concatStringsSep "\n " (
map (client: ''
- name: ${client.name}
ids: ids:
- xK9mP2vL7nQ4 - ${config.sops.placeholder.${client.idSecret}}
- name: laptop '') cfg.clients
ids: )}
- jH3fR8wT5yB1
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
'';
};
```
### SOPS Secret Declarations (Auto-generated)
The module automatically declares SOPS secrets for each client:
```nix
sops.secrets = lib.mkMerge (
map (client: {
${client.idSecret} = { };
}) cfg.clients
);
``` ```
## Nginx Configuration ## Nginx Configuration
The module configures nginx directly (not via the nginx module): The module configures nginx directly (not via the nginx module) because DoH requires special handling:
```nix ```nix
services.nginx.virtualHosts."dns.ashisgreat.xyz" = { services.nginx.virtualHosts."${cfg.domain}" = {
enableACME = true; enableACME = true;
forceSSL = true; forceSSL = true;
locations."/dns-query" = { # Regex location to match /dns-query and /dns-query/{clientId}
proxyPass = "http://127.0.0.1:3000"; locations."~ ^/dns-query" = {
proxyPass = "http://127.0.0.1:${toString cfg.port}";
extraConfig = '' extraConfig = ''
proxy_http_version 1.1;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Proto $scheme;
proxy_method POST;
# DoH uses POST with application/dns-message
proxy_pass_request_body on; proxy_pass_request_body on;
proxy_set_header Content-Type "application/dns-message";
# Buffer settings for DNS queries
proxy_buffers 8 16k; proxy_buffers 8 16k;
proxy_buffer_size 32k; proxy_buffer_size 32k;
''; '';
}; };
# Block all other paths including admin UI
locations."/" = { locations."/" = {
return = "404"; return = "404";
}; };
}; };
# Ensure nginx user can access ACME certs
users.users.nginx.extraGroups = [ "acme" ];
# Open HTTPS port
networking.firewall.allowedTCPPorts = [ 443 ];
``` ```
**Notes:** **Security Notes:**
- Admin UI not exposed externally - ClientIDs appear in URL paths and may be logged by nginx
- All config changes via git + `nixos-rebuild switch` - Consider filtering nginx logs to redact ClientIDs if needed
- ACME certificate auto-provisioned - Admin UI is completely blocked (returns 404)
- No rate limiting configured (can be added if abuse occurs)
## Firewall ## Firewall