Add Pi-hole DNS blocking + TV lockdown, new services, flake upgrade

- Enable pihole-ftl/pihole-web for network-wide DNS ad-blocking on the
  LAN/Tailscale side, keeping nixserve's own resolution untouched
- Seed a dedicated Pi-hole group that locks the smart TV (by MAC) to
  resolving only *.maybec.at, denying everything else
- Add pingvin-share, frigate, mosquitto, and homeassistant docker services
  with matching nginx vhosts and firewall port for Home Assistant
- Disable immich (unused for now), add proxySsl option to proxyVhost
- Bump flake.lock (nixpkgs/home-manager upgrade)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-29 07:16:27 +02:00
co-authored by Claude Sonnet 5
parent bd1883552e
commit 4045f25aee
3 changed files with 125 additions and 20 deletions
+111 -6
View File
@@ -3,7 +3,7 @@
let
rssdownloader = pkgs.callPackage ./rssdownloader.nix { src = rssdownloader-src; };
proxyVhost = { upstream, websockets ? true, maxBodySize ? "1g" }: {
proxyVhost = { upstream, websockets ? true, maxBodySize ? "1g", proxySsl ? false }: {
forceSSL = true;
enableACME = true;
locations."/" = {
@@ -18,7 +18,9 @@ let
proxy_send_timeout 3600;
proxy_buffering off;
client_max_body_size ${maxBodySize};
'';
'' + (if proxySsl then ''
proxy_ssl_verify off;
'' else "");
};
};
@@ -82,10 +84,10 @@ in
};
};
services.immich = {
enable = true;
mediaLocation = "/media/immich";
};
#services.immich = {
# enable = true;
# mediaLocation = "/media/immich";
#};
users.users.git = {
isSystemUser = true;
@@ -127,6 +129,82 @@ in
pools = [ "media" ];
};
# DNS ad-blocking for other LAN/Tailscale clients only.
# nixserve's own DNS resolution is untouched (Tailscale MagicDNS via resolv.conf).
services.pihole-ftl = {
enable = true;
openFirewallDNS = true;
openFirewallWebserver = true;
lists = [
{ url = "https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts"; description = "StevenBlack unified hosts"; }
];
settings = {
dns.upstreams = [ "1.1.1.1" "1.0.0.1" ];
webserver.api.cli_pw = true;
};
};
services.pihole-web = {
enable = true;
ports = [ "8081o" ];
};
# Lock the smart TV down to *.maybec.at only: everything else fails to resolve.
# This seeds Pi-hole's gravity.db (groups/clients/rules) declaratively on every
# boot/rebuild -- that data lives outside pihole.toml so it isn't covered by
# services.pihole-ftl.settings.
systemd.services.pihole-tv-lockdown = {
description = "Seed Pi-hole gravity.db with smart TV DNS lockdown rules";
# gravity.db's group/client/rule tables are queried live per DNS lookup
# (not cached at FTL startup), so this only needs to run after FTL has
# created the database -- no need to race it to run "before" FTL starts.
after = [ "pihole-ftl.service" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
User = config.services.pihole-ftl.user;
Group = config.services.pihole-ftl.group;
};
script = ''
set -euo pipefail
${pkgs.sqlite}/bin/sqlite3 "${config.services.pihole-ftl.stateDirectory}/gravity.db" <<'SQL'
INSERT OR IGNORE INTO "group" (name, enabled, description)
VALUES ('tv-only', 1, 'Smart TV: only *.maybec.at may resolve');
INSERT OR IGNORE INTO domainlist (type, domain, enabled, comment)
VALUES (2, '.*', 1, 'tv-only: deny everything');
INSERT OR IGNORE INTO domainlist (type, domain, enabled, comment)
VALUES (3, '(^|\.)maybec\.at$', 1, 'tv-only: allow maybec.at and subdomains');
INSERT OR IGNORE INTO domainlist_by_group (domainlist_id, group_id)
SELECT d.id, g.id FROM domainlist d, "group" g
WHERE d.domain IN ('.*', '(^|\.)maybec\.at$') AND g.name = 'tv-only';
INSERT OR IGNORE INTO client (ip, comment)
VALUES ('20:28:bc:15:1d:c0', 'Smart TV (locked to maybec.at only)');
INSERT OR IGNORE INTO client_by_group (client_id, group_id)
SELECT c.id, g.id FROM client c, "group" g
WHERE c.ip = '20:28:bc:15:1d:c0' AND g.name = 'tv-only';
-- Pi-hole auto-links every new domainlist/client row into the Default
-- group (tr_domainlist_add / tr_client_add triggers). Strip that back
-- out, otherwise the deny-all regex applies to every client on the
-- network instead of just the TV.
DELETE FROM domainlist_by_group
WHERE group_id = 0
AND domainlist_id IN (SELECT id FROM domainlist WHERE domain IN ('.*', '(^|\.)maybec\.at$'));
DELETE FROM client_by_group
WHERE group_id = 0
AND client_id IN (SELECT id FROM client WHERE ip = '20:28:bc:15:1d:c0');
SQL
'';
};
virtualisation.docker = {
enable = true;
enableOnBoot = true;
@@ -180,6 +258,30 @@ in
dir = "/home/ulve/code/colony-archive";
};
systemd.services.pingvin-share = dockerService {
name = "pingvin-share";
description = "Pingvin Share file sharing service";
dir = "/home/ulve/code/pingvin-share";
};
systemd.services.frigate = dockerService {
name = "frigate";
description = "Frigate NVR";
dir = "/home/ulve/code/frigate";
};
systemd.services.mosquitto = dockerService {
name = "mosquitto";
description = "Mosquitto MQTT broker";
dir = "/home/ulve/code/mosquitto";
};
systemd.services.homeassistant = dockerService {
name = "homeassistant";
description = "Home Assistant";
dir = "/home/ulve/code/homeassistant";
};
systemd.services.dozzle = {
description = "Dozzle Docker log viewer";
after = [ "docker.service" "network.target" ];
@@ -208,5 +310,8 @@ virtualHosts."aftersummer.maybec.at" = proxyVhost { upstream = "http://127.0.0.
virtualHosts."photos.maybec.at" = proxyVhost { upstream = "http://[::1]:2283"; maxBodySize = "50g"; };
virtualHosts."colony.maybec.at" = proxyVhost { upstream = "http://127.0.0.1:3333"; };
virtualHosts."git.maybec.at" = proxyVhost { upstream = "http://127.0.0.1:3001"; maxBodySize = "10g"; };
virtualHosts."share.maybec.at" = proxyVhost { upstream = "http://127.0.0.1:8090"; maxBodySize = "10g"; };
virtualHosts."cam.maybec.at" = proxyVhost { upstream = "https://127.0.0.1:8971"; proxySsl = true; };
virtualHosts."home.maybec.at" = proxyVhost { upstream = "http://127.0.0.1:8123"; };
};
}