- 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>
318 lines
10 KiB
Nix
318 lines
10 KiB
Nix
{ config, pkgs, rssdownloader-src, ... }:
|
|
|
|
let
|
|
rssdownloader = pkgs.callPackage ./rssdownloader.nix { src = rssdownloader-src; };
|
|
|
|
proxyVhost = { upstream, websockets ? true, maxBodySize ? "1g", proxySsl ? false }: {
|
|
forceSSL = true;
|
|
enableACME = true;
|
|
locations."/" = {
|
|
proxyPass = upstream;
|
|
proxyWebsockets = websockets;
|
|
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_read_timeout 3600;
|
|
proxy_send_timeout 3600;
|
|
proxy_buffering off;
|
|
client_max_body_size ${maxBodySize};
|
|
'' + (if proxySsl then ''
|
|
proxy_ssl_verify off;
|
|
'' else "");
|
|
};
|
|
};
|
|
|
|
dockerService = { name, description, dir }: {
|
|
inherit description;
|
|
after = [ "docker.service" "network.target" ];
|
|
requires = [ "docker.service" ];
|
|
wantedBy = [ "multi-user.target" ];
|
|
serviceConfig = {
|
|
Type = "oneshot";
|
|
RemainAfterExit = true;
|
|
WorkingDirectory = dir;
|
|
ExecStart = "${pkgs.docker-compose}/bin/docker-compose up -d";
|
|
ExecStop = "${pkgs.docker-compose}/bin/docker-compose down";
|
|
};
|
|
};
|
|
in
|
|
{
|
|
services.nfs.server = {
|
|
enable = true;
|
|
statdPort = 4000;
|
|
lockdPort = 4001;
|
|
mountdPort = 4002;
|
|
exports = ''
|
|
/media 192.168.50.0/24(rw,sync,no_subtree_check,all_squash,anonuid=1000,anongid=1000,fsid=0,insecure)
|
|
/media 100.64.0.0/10(rw,sync,no_subtree_check,all_squash,anonuid=1000,anongid=1000,fsid=0,insecure)
|
|
'';
|
|
};
|
|
|
|
services.jellyfin.enable = true;
|
|
|
|
services.tailscale = {
|
|
enable = true;
|
|
openFirewall = true;
|
|
useRoutingFeatures = "server";
|
|
};
|
|
|
|
services.syncthing = {
|
|
enable = true;
|
|
user = "ulve";
|
|
dataDir = "/home/ulve";
|
|
configDir = "/home/ulve/.config/syncthing";
|
|
openDefaultPorts = true;
|
|
settings.devices."macbook" = {
|
|
id = "U4YVRUT-JAAXHQ4-TA6FAGH-SO6Z7CI-KDFPRNG-UZYNXDK-6OQY7ZA-UBTZRQW";
|
|
};
|
|
settings.devices."phone" = {
|
|
id = "WXTP2PT-GW3C3EJ-JYYYXUT-VEKCAQV-NQTPQLI-TOMDAI3-CSUUHF5-6DH72AM";
|
|
};
|
|
settings.folders."Downloads" = {
|
|
path = "/home/ulve/Downloads";
|
|
devices = [ "macbook" ];
|
|
};
|
|
settings.folders."yrw8u-ut036" = {
|
|
path = "/home/ulve/Phone";
|
|
devices = [ "phone" ];
|
|
};
|
|
settings.folders."xdnt7-iaqtd" = {
|
|
path = "/home/ulve/Vaults";
|
|
devices = [ "macbook" ];
|
|
};
|
|
};
|
|
|
|
#services.immich = {
|
|
# enable = true;
|
|
# mediaLocation = "/media/immich";
|
|
#};
|
|
|
|
users.users.git = {
|
|
isSystemUser = true;
|
|
group = "git";
|
|
home = "/media/git";
|
|
shell = pkgs.bash;
|
|
};
|
|
users.groups.git = {};
|
|
|
|
services.gitea = {
|
|
enable = true;
|
|
user = "git";
|
|
group = "git";
|
|
stateDir = "/media/git";
|
|
settings = {
|
|
server = {
|
|
DOMAIN = "git.maybec.at";
|
|
ROOT_URL = "https://git.maybec.at/";
|
|
HTTP_ADDR = "127.0.0.1";
|
|
HTTP_PORT = 3001;
|
|
START_SSH_SERVER = false;
|
|
SSH_DOMAIN = "git.maybec.at";
|
|
SSH_PORT = 22;
|
|
SSH_USER = "git";
|
|
};
|
|
service.DISABLE_REGISTRATION = true;
|
|
};
|
|
};
|
|
|
|
services.openssh.extraConfig = ''
|
|
Match User git
|
|
AuthorizedKeysCommand ${config.services.gitea.package}/bin/gitea keys -c ${config.services.gitea.customDir}/conf/app.ini -e git -u %u -t %t -k %k
|
|
AuthorizedKeysCommandUser git
|
|
'';
|
|
|
|
services.zfs.autoScrub = {
|
|
enable = true;
|
|
interval = "monthly";
|
|
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;
|
|
autoPrune = {
|
|
enable = true;
|
|
dates = "weekly";
|
|
flags = [ "--all" ];
|
|
};
|
|
daemon.settings = {
|
|
log-driver = "json-file";
|
|
log-opts = {
|
|
max-size = "10m";
|
|
max-file = "3";
|
|
};
|
|
};
|
|
};
|
|
|
|
systemd.services.transmission = dockerService {
|
|
name = "transmission";
|
|
description = "Transmission via Mullvad VPN";
|
|
dir = "/home/ulve/code/transmission";
|
|
};
|
|
|
|
systemd.services.aftersummer = dockerService {
|
|
name = "aftersummer";
|
|
description = "Aftersummer";
|
|
dir = "/home/ulve/code/aftersummer";
|
|
};
|
|
|
|
systemd.services.spine = dockerService {
|
|
name = "spine";
|
|
description = "Spine ebook server";
|
|
dir = "/home/ulve/code/wargaming";
|
|
};
|
|
|
|
systemd.services.bookthing = dockerService {
|
|
name = "bookthing";
|
|
description = "Bookthing audiobook server";
|
|
dir = "/home/ulve/code/bookthing";
|
|
};
|
|
|
|
systemd.services.tailscale-exit = dockerService {
|
|
name = "tailscale-exit";
|
|
description = "Tailscale exit node via Mullvad VPN";
|
|
dir = "/home/ulve/code/tailscale";
|
|
};
|
|
|
|
systemd.services.colony-archive = dockerService {
|
|
name = "colony-archive";
|
|
description = "Colony Archive tabletop RPG campaign manager";
|
|
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" ];
|
|
requires = [ "docker.service" ];
|
|
wantedBy = [ "multi-user.target" ];
|
|
serviceConfig = {
|
|
Type = "simple";
|
|
ExecStartPre = "${pkgs.docker}/bin/docker rm -f dozzle || true";
|
|
ExecStart = "${pkgs.docker}/bin/docker run --rm --name dozzle -p 8080:8080 -v /var/run/docker.sock:/var/run/docker.sock amir20/dozzle";
|
|
ExecStop = "${pkgs.docker}/bin/docker stop dozzle";
|
|
Restart = "on-failure";
|
|
};
|
|
};
|
|
|
|
security.acme = {
|
|
acceptTerms = true;
|
|
defaults.email = "johansson.olov@gmail.com";
|
|
};
|
|
|
|
services.nginx = {
|
|
enable = true;
|
|
virtualHosts."aftersummer.maybec.at" = proxyVhost { upstream = "http://127.0.0.1:5588"; };
|
|
virtualHosts."spine.maybec.at" = proxyVhost { upstream = "http://localhost:3000"; };
|
|
virtualHosts."bookthing.maybec.at" = proxyVhost { upstream = "http://127.0.0.1:8000"; };
|
|
virtualHosts."jelly.maybec.at" = proxyVhost { upstream = "http://127.0.0.1:8096"; };
|
|
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"; };
|
|
};
|
|
}
|