1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
|
{ config, pkgs, lib, ... }:
# AdGuard Home - DNS resolver + optional network-wide ad blocker.
#
# Plain DNS listens on port 53 (udp/tcp) directly on the public IP.
# DNS-over-TLS (853/tcp) and DNS-over-HTTPS (8443/tcp, path /dns-query) are
# also served directly by AdGuard itself, terminating TLS with their own
# independent Let's Encrypt cert (modules/acme.nix) - NOT via Caddy, since
# Caddy already owns port 443 for its own reverse-proxied vhosts and
# AdGuard's HTTPS/DoH listener needs its own port.
#
# AdGuard's HTTPS/DoH listener binds to the same host as the plain `http`
# listener below (confirmed in AdGuardHome's source: both come from
# HTTPConfig.Address) - that's why `http.address` binds 0.0.0.0 rather than
# 127.0.0.1 even though the plain-HTTP web UI is still meant to be reached
# only via Caddy's reverse proxy: port 3000 itself is never opened in the
# firewall (modules/common.nix), so it's still unreachable directly.
# DNS-over-TLS reuses `dns.bind_hosts` below, so it needs no such change.
#
# Admin password is seeded from a sops secret containing a bcrypt hash.
# To generate the hash on your local machine:
# htpasswd -nB admin
# Copy the hash portion (everything after "admin:") into secrets.yaml.
{
services.adguardhome = {
enable = true;
mutableSettings = false; # declarative mode - config comes from Nix only
settings = {
http = {
address = "0.0.0.0:3000";
};
tls = {
enabled = true;
server_name = "dns.karanj.com";
port_https = 8443;
port_dns_over_tls = 853;
# DNS-over-QUIC and DNSCrypt were not requested - keep them off.
port_dns_over_quic = 0;
port_dnscrypt = 0;
certificate_path = "/var/lib/acme/dns.karanj.com/fullchain.pem";
private_key_path = "/var/lib/acme/dns.karanj.com/key.pem";
};
dns = {
bind_hosts = [ "0.0.0.0" ];
port = 53;
# Clients configure this server's public IPv4 address (not
# dns.karanj.com - that hostname only resolves to the HTTPS web UI
# via Caddy) as their plain DNS resolver, e.g. <server-ipv4>:53.
# For encrypted DNS, see the client setup notes in README.md.
# Upstream DNS resolvers (privacy-respecting)
upstream_dns = [
"https://dns.quad9.net/dns-query"
"https://cloudflare-dns.com/dns-query"
];
bootstrap_dns = [
"9.9.9.9"
"1.1.1.1"
];
enable_dnssec = true;
};
# Users block: username "admin", password from sops secret.
# The ExecStartPre below writes the hash into the config on every
# service start, because mutableSettings=false uses a static config
# file but the password hash must be injected at runtime (it contains
# a secret).
users = [
{
name = "admin";
# Placeholder - replaced at service start by the ExecStartPre below
password = "REPLACED_AT_ACTIVATION";
}
];
# Basic filtering
filtering = {
enabled = true;
update_interval = 24;
};
# Block lists
filters = [
{
enabled = true;
url = "https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt";
name = "AdGuard DNS filter";
id = 1;
}
{
enabled = true;
url = "https://adaway.org/hosts.txt";
name = "AdAway Default Blocklist";
id = 2;
}
];
};
};
# AdGuardHome needs a static system user rather than the module's default
# DynamicUser=true: sops-nix chowns the "adguard/password_hash" secret to
# this user *during activation*, which happens while the service is
# stopped (for a restart) - a DynamicUser only exists while its service is
# actually running, so that chown would fail to resolve the user otherwise.
users.users.adguardhome = {
isSystemUser = true;
group = "adguardhome";
};
users.groups.adguardhome = { };
# mutableSettings = false makes the module's own ExecStartPre unconditionally
# `cp --force` the store-generated config (with the literal placeholder)
# over $STATE_DIRECTORY/AdGuardHome.yaml on every service start. An
# activation script can't win that race, so inject the real hash as a
# second ExecStartPre, ordered after the module's via mkAfter, so it always
# runs right before ExecStart.
#
# Plain bash substring substitution + `>` redirect is used instead of
# `sed -i`: sed -i creates a temp file and chowns/renames it in place, and
# this unit's hardened SystemCallFilter (~@privileged) blocks the chown
# syscall, killing sed with SIGSYS ("Bad system call").
# Don't attempt the first start until the ACME cert referenced by
# tls.certificate_path/private_key_path above actually exists.
systemd.services.adguardhome.after = [ "acme-dns.karanj.com.service" ];
systemd.services.adguardhome.wants = [ "acme-dns.karanj.com.service" ];
systemd.services.adguardhome.serviceConfig = {
DynamicUser = lib.mkForce false;
User = "adguardhome";
Group = "adguardhome";
ExecStartPre = lib.mkAfter [
"${pkgs.writeShellScript "adguard-inject-password" ''
set -eu
CFG="$STATE_DIRECTORY/AdGuardHome.yaml"
HASH=$(cat "${config.sops.secrets."adguard/password_hash".path}")
CONTENT=$(cat "$CFG")
printf '%s\n' "''${CONTENT//REPLACED_AT_ACTIVATION/$HASH}" > "$CFG"
''}"
];
};
}
|