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:
parent
053198d013
commit
51e723ddad
1 changed files with 121 additions and 30 deletions
|
|
@ -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:**
|
||||||
address: 0.0.0.0:3000
|
- 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
|
||||||
|
|
||||||
clients:
|
### Data Directory
|
||||||
persistent:
|
|
||||||
- name: phone
|
```nix
|
||||||
ids:
|
systemd.tmpfiles.rules = [
|
||||||
- xK9mP2vL7nQ4
|
"d /var/lib/adguard 0755 root root -"
|
||||||
- name: laptop
|
];
|
||||||
ids:
|
```
|
||||||
- jH3fR8wT5yB1
|
|
||||||
|
### SOPS Template for AdGuardHome.yaml
|
||||||
|
|
||||||
|
```nix
|
||||||
|
sops.templates."adguardhome.yaml" = {
|
||||||
|
content = ''
|
||||||
|
http:
|
||||||
|
address: 0.0.0.0:3000
|
||||||
|
|
||||||
|
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:
|
||||||
|
${lib.concatStringsSep "\n " (
|
||||||
|
map (client: ''
|
||||||
|
- name: ${client.name}
|
||||||
|
ids:
|
||||||
|
- ${config.sops.placeholder.${client.idSecret}}
|
||||||
|
'') cfg.clients
|
||||||
|
)}
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue