Commit graph

194 commits

Author SHA1 Message Date
27a4e85693 runner: use forgejo-runner package (renamed in 25.11)
The forgejo-actions-runner attr was renamed to forgejo-runner upstream.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-01 16:00:00 +01:00
29e1185694 runner: add Forgejo Actions runner on the mediaserver
Adds services/forgejo-runner.nix as a host-gated module on the mediaserver
and switches the flake-update workflow from runs-on: ubuntu-latest to the
self-hosted fred-nix label, mapped to catthehacker/ubuntu:act-latest for
GitHub-action compatibility. Token lives at /var/secrets/forgejo-runner-token
so it stays out of the Nix store.

Also drops the stray result/ build symlink from the worktree.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-01 15:58:28 +01:00
c45811acf9 router: accept docker0 on input chain
Containers connecting to host services on 10.0.0.1 (e.g. Profilarr → Radarr
at 10.0.0.1:7878) hit the input chain, not forward, because the destination
is a local IP. The forward chain already trusts docker0 for outbound; this
adds the matching input rule so the return path stops getting dropped.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-30 20:47:46 +01:00
98ccee2221 profilarr: use Docker Hub image (santiagosayshey), not GHCR
The ghcr.io/dictionarry-hub/profilarr path mentioned in some docs isn't
publicly pullable — anonymous token requests get 403. Canonical image is
santiagosayshey/profilarr:latest on Docker Hub per the upstream README.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-30 20:05:30 +01:00
a9649be705 profilarr: swap recyclarr for Dictionarry's Profilarr
Profilarr replaces the recyclarr/TRaSH-Guides flow with a stateful web
service that owns *arr profiles end-to-end via its own UI. Runs as an
oci-container on 127.0.0.1:6868, fronted by nginx at
profilarr.nordhammer.it behind Authelia (one_factor).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-30 20:00:33 +01:00
728779daab 2026-04-29 20:40:54 +01:00
8fa1e4c112 recyclarr: prefer x265 on 1080p profiles for disk space
Override TRaSH's -10000 ban on x265 (HD) to +500 on Sonarr WEB-1080p
and Radarr HD Bluray + WEB. The Scene/No-RlsGroup/Retags/Obfuscated
custom formats (each at -10000) still filter the truly low-bitrate
x265 trash, so we get smaller files without inviting slop.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 20:30:42 +01:00
79d7d3f88e adguard: explicitly enable LAN rewrites (schema change on stable)
AdGuard's recent config schema added an enabled flag on each rewrite
that defaults to false. Without it, the *.nordhammer.it -> 10.0.0.1
rules were silently disabled, so LAN clients resolved their own
domains to the public DDNS IP and tripped over NAT loopback.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 18:56:11 +01:00
4c80e26431 recyclarr: fix Sonarr UHD template name (web-2160p, not uhd-bluray-web)
The Sonarr 4K profile is sonarr-v4-quality-profile-web-2160p in TRaSH's
recyclarr templates — uhd-bluray-web exists for German content only.
The English UHD profile is WEB-only and named "WEB-2160p", so update
the include list and the AV1-ban score assignment to match.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 18:48:19 +01:00
3819cb6820 locale + crowdsec: pin timezone, declare static crowdsec user
Two failing services after the channel switch.

automatic-timezoned has been polkit-blocked since well before the
switch — replace with a static Europe/London timezone. Hosts that
travel can override locally if needed.

The vendored crowdsec module's setup unit chowns its config dir to
the (DynamicUser-allocated) crowdsec user via an ExecStartPre+ hack.
On stable's systemd the dynamic user isn't visible to chown via NSS
at that point, so it fails with 'invalid user'. Declaring crowdsec
as a static system user makes systemd use it (DynamicUser becomes a
no-op) and the chown resolves cleanly.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 14:00:41 +01:00
34a45af357 flake: split mediaserver onto nixos-25.11, keep desktops on unstable
The mediaserver kept hard-freezing on local builds (gnupg, openldap,
deno/rusty-v8) whenever a fresh unstable revision outran Hydra's
binary cache. It doesn't need bleeding-edge packages — every service
it runs is mature enough that 6-month-old versions are fine — so move
it onto the stable channel where the cache is essentially always
warm. Gaming and Macbook stay on unstable for fresh GPU/kernel work.

Implementation: add nixpkgs-stable + home-manager-stable inputs,
parameterise mkHost to accept a (nixpkgs, home-manager) pair.

Drive-by:
- Switch homepage.nix from environmentFiles (plural, unstable-only)
  to environmentFile (singular, present on both channels).
- Gate the openldap-skip-tests overlay to non-mediaserver hosts so
  it doesn't force a local rebuild on stable, where openldap is
  always cached.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 13:26:07 +01:00
3f2c88da94 arr-interconnect: drop manual 1080p quality floors
Recyclarr now manages quality definitions via TRaSH templates, so the
hand-rolled minSize=10 floor is redundant — every sync would overwrite
it anyway.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 10:31:17 +01:00
e99bc7cc9b recyclarr: add weekly TRaSH-Guide profile sync for Sonarr & Radarr
Score-based release filtering replaces the brittle "minimum size" approach
— good HEVC encodes from reputable groups now win regardless of file
size, while obfuscated/no-group/lazy-x265 garbage gets banned.

Profiles installed:
  Sonarr: WEB-1080p (default), UHD Bluray + WEB (per-show opt-in)
  Radarr: HD Bluray + WEB (default), UHD Bluray + WEB (per-movie opt-in)

AV1 is banned across all four profiles since the GPU lacks hardware
decode. API keys are extracted at runtime from each *arr's config.xml,
matching the arr-interconnect pattern.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 10:31:09 +01:00
ec32b9b849 router: rename nat table to router-nat so Docker's chains survive rebuilds
NixOS's nftables module rebuilds the tables it owns on every activation,
which previously wiped Docker's DOCKER/PREROUTING chains in ip nat
(both Docker and the router were defining 'ip nat'). Renaming our
table sidesteps the collision — kernel hooks across separate tables
at the same priority all run, so functionality is unchanged.

Eliminates the need to run 'systemctl restart docker' after every
nixos-rebuild to restore container port-forwards.
2026-04-26 19:43:33 +01:00
c1750c8538 router: allow WAN port-forwards to any DNAT target, not just eth0
The forward rule only accepted iifname=eno1 oifname=eth0 ct status=dnat,
which worked when port-forwards always landed on a LAN host. Docker
DNAT routes to docker0, so external traffic to 26900 was being DNAT'd
correctly but then dropped at the forward filter. Drop the oifname
constraint — the prerouting DNAT rule already controls what gets
forwarded; the filter doesn't need to second-guess it.
2026-04-26 19:42:15 +01:00
525147aa61 fail2ban: remove — superseded by CrowdSec
CrowdSec covers the same surface (sshd, authelia, nginx, *arr apps,
qBit) with the addition of community-sourced threat intel and ntfy
push alerts. Keeping both was redundant. State at /var/lib/fail2ban
will sit unused until cleaned up by hand.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-26 19:26:24 +01:00
a0a1d67124 crowdsec: add systemd-journal group so journalctl acquisitions work
DynamicUser can only see its own journald entries by default, so the
sshd + authelia journalctl acquisitions were dying with "insufficient
permissions" and exit status 1 from the spawned journalctl process.
Adding systemd-journal grants the read access journald gates on group
membership, restoring the ssh-bf / authelia-bf detection chain.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-25 20:03:10 +01:00
c4f0e4920e 7dtd-coop: START_MODE=1 (Start), not 2 (Update+STOP)
The vinanrra image's mode numbers are: 0=Install+STOP, 1=Start,
2=Update+STOP, 3=Update+Start, 4=Backup+STOP. I picked 2 thinking
it meant "Only Start", which is why the container kept exiting
cleanly after each update check. Mode 1 just starts the server,
which matches what the main 7dtd container uses.
2026-04-25 19:51:32 +01:00
c1cfa613f7 7dtd-coop: switch to START_MODE=2, seed serverfiles from main install
SteamCMD anonymous install fails with "Missing configuration" on a
fresh coop dir. The main 7dtd works because its binaries were
installed long ago and LinuxGSM skips the SteamCMD step. Same trick
for coop: rsync the binaries over and start-only, no update path.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-25 19:24:01 +01:00
568b815d8d router: allow docker0 forward and expose 7dtd-coop ports
Container outbound (image pulls, LinuxGSM bootstrap fetches) was
dropped by the inet filter forward chain — only eth0 and DNAT'd
WAN traffic were whitelisted. Add iifname "docker0" accept so
containers can reach the internet.

Also add the coop server's 26910/26911-26912 forwards to ports.toml
so WAN players can connect.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-25 15:35:26 +01:00
4d84fe2df3 7dtd: add private coop server on ports 26910-26912
Second container 7dtd-coop with its own /var/lib/7dtd-coop state dir
and a configure unit that patches the server as unlisted, 2-player,
distinct world seed.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-25 15:26:55 +01:00
709b6944ad crowdsec: add nginx group so DynamicUser can read access.log
The agent runs as a systemd DynamicUser and was failing the nginx
acquisition with "No matching files for pattern /var/log/nginx/access.log"
because access.log is nginx:nginx 640 — readOnlyPaths handles sandbox
visibility but not Unix perms. extraGroups = [ "nginx" ] gets it past
the group bit.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-25 15:23:41 +01:00
0125a1deb2 crowdsec: build notification plugins via package override
Upstream nixpkgs builds only cmd/crowdsec and cmd/crowdsec-cli; the
PR #446307 module's setup script expects notification plugins at
\$package/libexec/crowdsec/plugins/notification-*, causing first-start
failure (cannot stat notification-dummy). Add the cmd/notification-*
subpackages and move the resulting binaries into the libexec layout the
module expects.

Drop this override along with the vendored modules once the PR lands —
nixpkgs will need a matching package update for the rewrite to work.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-25 15:16:03 +01:00
a8d163b4a8 crowdsec: vendor PR #446307 rewrite to fix bootstrap
The upstream NixOS crowdsec module fails on first deploy ("no API client
section in configuration") because it doesn't auto-register LAPI
credentials. The rewrite in NixOS/nixpkgs#446307 (TornaxO7's branch) adds
a setup oneshot that runs `cscli machines add --auto` if the credentials
file is missing, and handles DynamicUser StateDirectory permissions
explicitly. The bouncer rewrite gets matching auto-registration.

Vendor both module files locally and disable the upstream copies. Drop
modules/crowdsec/ and the disabledModules+imports lines once the PR
merges into nixpkgs unstable.

Config moves to the new unified `settings` API (no more separate
`localConfig`); LAPI moved to 127.0.0.1:8081 to dodge the qBit collision.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-25 15:00:32 +01:00
7ec6146917 crowdsec: add community IDS/IPS with ntfy push alerts
Enables the CrowdSec agent with sshd/nginx/http-cve hub collections,
acquires logs from nginx, sshd, and Authelia journald, and wires the
firewall bouncer to enforce bans via nftables. Alerts are POSTed to a
self-chosen ntfy.sh topic (URL read from /var/secrets/ntfy-url, falls
back to a placeholder so the repo stays eval-clean without the secret).

Module is self-contained — remove the file + import to uninstall; state
lives under /var/lib/crowdsec.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-24 22:30:16 +01:00
a44c149955 fail2ban: drop legacy 192.168.0.0/16 from ignoreIP
LAN is 10.0.0.0/24 since the router cutover; the 192.168 range was
a leftover from the eero-bridge era and no longer matches any host.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-24 20:41:53 +01:00
f83fd72a98 qbit: fix CSRF-loop behind Authelia + self-heal data-dir ownership
- nginx: strip Referer on torrent.nordhammer.it so qBit's origin check
  doesn't reject the post-Authelia redirect (Referer was auth.nordhammer.it,
  Host was torrent.nordhammer.it → 401 loop).
- tmpfiles: collapse the nested qbittorrent `d` rules into a single
  `d` + recursive `Z` so systemd re-enforces ownership/perms on every
  boot. Caught Docker-migration UID drift that silently broke state
  persistence and file logging.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-24 20:04:04 +01:00
0c7b6f1b58 nginx: strip cookies on qBit proxy so localhost-bypass always wins
qBittorrent's auth logic is "no SID cookie → bypass for localhost; SID
cookie present → validate it." If the browser has a stale SID from an
earlier session, qBit fails validation and returns 401 even though the
connection is from 127.0.0.1 and bypass is enabled.

Strip both directions: drop the client's Cookie header on the way in so
qBit never sees an SID, and hide Set-Cookie on the way back so the
browser never accumulates one in the first place.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-24 14:57:55 +01:00
88c1b8b2fe arr-interconnect: enforce Prowlarr local-auth bypass
Sonarr/Radarr/Bazarr default to DisabledForLocalAddresses so that requests
coming via the nginx reverse proxy (from 127.0.0.1) skip the app's own
login, leaving Authelia as the single gate. Prowlarr defaults to Enabled,
which produces a 401 behind Authelia.

Idempotent: only rewrites config.xml + restarts prowlarr when it finds
the "Enabled" value; logs a no-op otherwise. Added pkgs.systemd to PATH
so the restart call works.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-24 14:39:53 +01:00
081b12f945 2026-04-24 13:39:32 +01:00
bcaecc244d Put Servarr + qBit + games + search behind Authelia
Only Jellyfin and the Authelia portal itself stay unprotected externally
(Jellyfin because it's streamed to remote clients; Authelia because it
is the login gate). Everything else (sonarr, radarr, bazarr, prowlarr,
torrent/qBittorrent, games, search) now goes through Authelia forward auth.

Internal integrations (Homepage widgets, Prowlarr → Sonarr/Radarr,
Bazarr → Sonarr/Radarr, transcode-hevc qBit queries) use 127.0.0.1:PORT
directly, so they are unaffected.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-24 11:21:28 +01:00
0c937b8601 router: phase-2 cleanup + camera DHCP reservation
- trustedLegacyCidrs now empty; eno1 is strictly WAN
- AdGuard rewrite retargets nordhammer.it → 10.0.0.1 (the new router IP)
- dnsmasq pins the bedroom camera (f0:a7:31:6c:50:4b) to 10.0.0.39

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-24 10:52:11 +01:00
5426e3847b router: expose forwarded ports on eno1; AdGuard rewrite for LAN hostname
- Input chain now accepts WAN traffic for every port in ports.toml so
  external access (SSH, HTTP, HTTPS, game ports) works through the eero's
  upstream port forwards during phase 1, and via our own DNAT in phase 2.
- Add AdGuard DNS rewrite nordhammer.it → 192.168.4.25 so LAN clients
  hit the mediaserver directly instead of relying on eero hairpin NAT.
  Target changes to 10.0.0.1 at phase 2 cutover.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-24 10:22:56 +01:00
661ad14948 router: trust the legacy eero subnet on eno1 during phase 1
Without this, the default-drop input policy blocked SSH and AdGuard DNS
from existing 192.168.4.x clients because they arrive on eno1 (still
acting as a client on the eero network until phase 2 cutover).

The trustedLegacyCidrs list is meant to be emptied in phase 2 when
eno1 becomes the ISP-facing WAN.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-24 10:17:06 +01:00
77eafded92 Turn mediaserver into a home router
Adds services/router.nix with systemd-networkd (eno1=WAN via DHCP,
eth0=LAN 10.0.0.1/24), nftables (NAT + firewall, default drop on WAN
in), dnsmasq (DHCP only — AdGuard Home keeps :53 for DNS), and sysctl
IP forwarding. NetworkManager is forced off on this host.

Port forwards live in ports.toml at the repo root and are imported via
builtins.fromTOML. Supports single ports, ranges ("26901-26902"), and
"both" protocol. Initial forwards: 22, 80, 443, 26900, 26901-26902.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-24 09:48:38 +01:00
7ee5a37fc5 arr-interconnect: add gawk to PATH for idempotency check
The quality-floor helper uses awk to compare floats (since jq output
can be 10 vs 10.0 depending on type). Without gawk on PATH, the check
failed silently and every run issued PUTs even when values already
matched.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-22 21:44:02 +01:00
194d488942 arr-interconnect: floor 1080p quality at 10 MB/min
Sonarr/Radarr default minSize=0 let through tiny sub-bitrate releases
(e.g. 163 MiB for a 40-min episode = 0.8 Mbps, unwatchable). Set min to
10 MB/min (~1.3 Mbps) across HDTV/WEBDL/WEBRip/Bluray 1080p so anything
below that is rejected on grab. Idempotent: only PUTs when value differs.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-22 21:41:50 +01:00
a825e36e2e Make AdGuard settings authoritative; add busybox; drop fallback DNS
- services/adguard.nix: mutableSettings = false so Nix config overrides
  UI-made changes on rebuild (settings are the source of truth)
- common.nix: add busybox for its collection of handy utilities
- common.nix: remove networking.nameservers — DNS now comes purely from
  per-host NetworkManager config (AdGuard as the only resolver, no leaks)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-22 19:57:55 +01:00
b7aa8e20ef nginx: move adguard vhost behind Authelia forward auth
Pairs with the prior commit that added the ACL rule.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-22 14:16:52 +01:00
070efb961a Wire AdGuard Home into Authelia SSO and Homepage dashboard
- adguard.nordhammer.it now routes through Authelia forward auth
  (AdGuard Home itself has no login, so this becomes the single gate)
- Added Authelia ACL rule for the subdomain so default_policy=deny
  returns 401 for redirect instead of 403
- Added AdGuard Home widget to Homepage under Infrastructure

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-22 14:15:57 +01:00
8aeb8e2de7 adguard: parallel upstreams + plain UDP fallbacks for speed
DoH-only sequential upstreams made first-time lookups slow. Add plain
UDP 1.1.1.1/9.9.9.9 alongside DoH and set upstream_mode=parallel so
AdGuard queries all four simultaneously and uses the fastest response.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-22 14:04:24 +01:00
919c991e3d Add AdGuard Home for network-wide DNS ad blocking
New services/adguard.nix runs AdGuard Home on the mediaserver with DoH
upstreams (Cloudflare + Quad9) and three default blocklists. DNS listens
on :53; web UI on 127.0.0.1:3000, reverse-proxied at adguard.nordhammer.it.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-22 13:40:30 +01:00
032693ef39 Authorize 7dtd.nordhammer.it in Authelia ACL
Without this rule the subdomain falls under default_policy=deny,
which returns 403 instead of the 401 that nginx needs to redirect
to the Authelia login page.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-18 23:23:48 +01:00
dfbc727f5f Disable EAC on 7DTD server so Proton clients can connect
Proton-based clients (e.g. CachyOS native install hitting 7DTD via
the Proton runtime) fail EAC handshake against a Linux dedicated
server. Disabling server-side lets Proton clients join via the
"Play without EasyAntiCheat" splash option.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-18 23:05:08 +01:00
740fca4fcf Expose 7DTD WebDashboard behind Authelia at 7dtd.nordhammer.it
Publishes the container's web dashboard port only on host loopback
(127.0.0.1:8090) so nginx can reverse-proxy it with Authelia
forward-auth, matching the Homepage/camera vhost pattern. Also flips
WebDashboardEnabled to true in the XML patcher so the server actually
starts the web server.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-18 21:43:11 +01:00
c05f986e1c Add 7 Days to Die dedicated server container; drop V-Rising
Enables the previously-disabled game-servers module with a new 7DTD
container (vinanrra/7dtd-server) on ports 26900 TCP + 26900-26902 UDP.
A oneshot systemd service waits for LGSM's first install to drop
sdtdserver.xml, then patches in the server name, password, and
random-gen world before restarting the container. V-Rising is removed
— the module hadn't been imported, so this just drops dead code.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-17 22:28:49 +01:00
d80ccf4e6d Stop Sonarr/Radarr from nuking qBittorrent torrents after import
Sonarr was silently removing torrents from qBittorrent once imports
completed, killing seeding. Set removeCompletedDownloads to false for
both clients so torrents stick around and keep seeding post-import.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-17 21:23:28 +01:00
d450b8e021 Seed placeholder latest.json so Homepage widget doesn't 404 pre-update
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-16 21:19:23 +01:00
f57c6e99ec Add Last Update widget to Homepage via record-update script
record-update parses nvd diff after switch and writes latest.json;
Homepage polls a local-only nginx listener and renders date/changes/
closure/kernel via a customapi widget.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-16 20:58:19 +01:00
2e29d3dce5 Force UMask=0002 on Radarr, Sonarr, Bazarr
New nixpkgs defaults for the *arr services set UMask=0022, which
conflicts with the media-group-writable overrides. Wrap with
lib.mkForce alongside the existing Jellyfin fix.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-16 20:22:37 +01:00