{ config, pkgs, lib, ... }: let choose = paths: builtins.head (builtins.filter (p: builtins.pathExists p) paths); in { options.local = { hostname = lib.mkOption { type = lib.types.str; default = "nixos-default"; description = "System hostname"; }; username = lib.mkOption { type = lib.types.str; default = "user"; description = "Primary user username"; }; userDescription = lib.mkOption { type = lib.types.str; default = "NixOS User"; description = "Primary user description"; }; address = lib.mkOption { type = lib.types.str; default = "10.1.1.100"; description = "Static IP address"; }; }; imports = [ (choose [./modules/desktop-manager/sway_greetd_homemanager.nix ../desktop-manager/sway_greetd_homemanager.nix]) (choose [./modules/local/hostname_username.nix ../local/hostname_username.nix]) (choose [./modules/local/networking_local.nix ../local/networking_local.nix]) (choose [./modules/bootloader/seabios-assigned-iso-at-birth.nix ../bootloader/seabios-assigned-iso-at-birth.nix]) (choose [./modules/lix-default.nix ../lix-default.nix]) # Optionally: (choose [ ./modules/toolsets/remote_building.nix ../toolsets/remote_building.nix ]) ]; config = { local.hostname = "nixos-traefik"; local.username = "traefikprg"; local.userDescription = "NixOS PRG Traefik Service"; local.address = "10.1.1.250"; # Configure Anubis service services.anubis = { # Use a single shared Anubis instance (redirect mode) so cookie + challenge # state is consistent across all protected services. instances = { shared = { enable = true; settings = { BIND_NETWORK = "tcp"; BIND = "127.0.0.1:8090"; # Redirect mode (Anubis will issue challenges & redirects) TARGET = " "; # Which domains are allowed to be redirected back to REDIRECT_DOMAINS = "prg-radio.org,git.prg-radio.org,wavelog.prg-radio.org,partdb.prg-radio.org,anubis.prg-radio.org"; # Public URL for this Anubis instance PUBLIC_URL = "https://anubis.prg-radio.org"; # Use bare domain for cookie scoping (modern browsers prefer no leading dot) COOKIE_DOMAIN = "prg-radio.org"; # Difficulty level DIFFICULTY = 7; #ALGOTIHM = "slow"; # Optional: serve robots.txt SERVE_ROBOTS_TXT = true; # Optional: webmaster email for error pages WEBMASTER_EMAIL = "dtu.prg@gmail.com"; # Metrics on separate port METRICS_BIND_NETWORK = "tcp"; METRICS_BIND = "127.0.0.1:8091"; }; }; }; }; services.traefik = { enable = true; group = "acme"; staticConfigOptions = { entryPoints = { web = { # Bind on IPv6 as well as IPv4 by using [::]:80 address = "[::]:80"; asDefault = true; http.redirections.entrypoint = { to = "websecure"; scheme = "https"; }; }; websecure = { # Bind on IPv6 as well as IPv4 by using [::]:443 address = "[::]:443"; asDefault = true; http.tls = { domains = [ { main = "prg-radio.org"; sans = ["*.prg-radio.org"]; } ]; }; }; # Federation entrypoint: external TLS on 8448 federation = { # Bind on IPv6 as well as IPv4 by using [::]:8448 address = "[::]:8448"; }; # TeamSpeak entry points teamspeak-voice = { # UDP entrypoint bind to IPv6 as well address = "[::]:9987/udp"; }; teamspeak-squery = { address = "[::]:10022/tcp"; }; teamspeak-data = { address = "[::]:30033/tcp"; }; }; log = { level = "INFO"; filePath = "${config.services.traefik.dataDir}/traefik.log"; format = "json"; }; api.dashboard = true; api.insecure = true; }; dynamicConfigOptions = { tls.certificates = [ { certFile = "/var/lib/acme/prg-radio.org/cert.pem"; keyFile = "/var/lib/acme/prg-radio.org/key.pem"; } ]; # ForwardAuth middleware so a single Anubis instance can protect many services http.middlewares = lib.mkForce (lib.mkMerge [ (lib.optionalAttrs true { anubis = { forwardAuth = { address = "http://127.0.0.1:8090/.within.website/x/cmd/anubis/api/check"; trustForwardHeader = true; # Ensure Traefik forwards Set-Cookie from Anubis back to the client authResponseHeaders = [ "Set-Cookie" ]; # Forward original host and proto so Anubis computes redirects correctly authRequestHeaders = [ "X-Forwarded-Host" "X-Forwarded-Proto" ]; }; }; }) ]); http.routers = { #anubis-api = { # rule = "Host(`anubis.prg-radio.org`) && PathPrefix(`/.within.website/x/cmd/anubis/api`)"; # service = "anubis"; # entryPoints = ["websecure"]; # priority = 200; # tls = {}; # }; # Anubis router (for challenge page) anubis = { rule = "Host(`anubis.prg-radio.org`)"; service = "anubis"; entryPoints = ["websecure"]; tls = {}; }; # Protected service example: Forgejo forgejo = { rule = "Host(`git.prg-radio.org`)"; service = "forgejo"; entryPoints = ["websecure"]; # Protect via shared Anubis using forwardAuth middlewares = ["anubis"]; tls = {}; }; # Matrix HTTP router for client requests (Element etc.) matrix = { rule = "Host(`lgbtq.prg-radio.org`)"; service = "matrix"; entryPoints = ["websecure"]; tls = {}; }; # Protected service: Wavelog wavelog = { rule = "Host(`wavelog.prg-radio.org`)"; service = "wavelog"; entryPoints = ["websecure"]; tls = {}; }; # Protected service: PartDB partdb = { rule = "Host(`partdb.prg-radio.org`)"; service = "partdb"; entryPoints = ["websecure"]; # Protect via shared Anubis using forwardAuth middlewares = ["anubis"]; tls = {}; }; }; http.services = { # Anubis service (challenge UI / redirect endpoint) anubis.loadBalancer = { servers = [ {url = "http://127.0.0.1:8090";} ]; #passHostHeader = true; }; forgejo.loadBalancer = { servers = [ {url = "http://127.0.0.1:8092";} ]; }; matrix.loadBalancer = { servers = [ {url = "http://10.1.1.248:12244";} ]; }; wavelog.loadBalancer = { servers = [ {url = "http://10.1.1.249:8086";} ]; }; partdb.loadBalancer = { servers = [ {url = "http://127.0.0.1:8094";} ]; }; }; # TCP routing for TeamSpeak tcp.routers = { teamspeak-squery = { rule = "HostSNI(`*`)"; service = "teamspeak-squery"; entryPoints = ["teamspeak-squery"]; }; teamspeak-data = { rule = "HostSNI(`*`)"; service = "teamspeak-data"; entryPoints = ["teamspeak-data"]; }; # Federation TCP router: incoming on :8448 -> Conduit backend matrix-federation = { rule = "HostSNI(`*`)"; service = "matrix-federation"; entryPoints = ["federation"]; # Pass TLS through to the backend (Conduit handles TLS on port 12244) tls = { passthrough = true; }; }; # Also accept TLS passthrough on port 443 for the Matrix host so # other servers that contact :443/_matrix/* # will be forwarded to Conduit as well. # I guess I had to declare the rule explicitly here again? matrix-tls443 = { # match by SNI to avoid interfering with other HTTPS sites rule = "HostSNI(`lgbtq.prg-radio.org`)"; service = "matrix-federation"; entryPoints = ["websecure"]; tls = { passthrough = true; }; }; }; tcp.services = { teamspeak-squery.loadBalancer = { servers = [ {address = "10.1.1.248:10022";} ]; }; teamspeak-data.loadBalancer = { servers = [ {address = "10.1.1.248:30033";} ]; }; matrix-federation.loadBalancer = { servers = [ {address = "10.1.1.248:12244";} ]; }; }; # UDP routing for TeamSpeak voice udp.routers = { teamspeak-voice = { entryPoints = ["teamspeak-voice"]; service = "teamspeak-voice"; }; }; udp.services = { teamspeak-voice.loadBalancer = { servers = [ {address = "10.1.1.248:9987";} ]; }; }; }; }; security.acme = { acceptTerms = true; defaults.email = "dtu.prg@gmail.com"; certs."prg-radio.org" = { domain = "*.prg-radio.org"; group = "acme"; dnsProvider = "cloudflare"; environmentFile = "/home/traefikprg/cloudflare/cloudflare.env"; reloadServices = ["traefik.service"]; }; }; # Enable Tailscale for remote access to Traefik dashboard and configuration services.tailscale.enable = true; systemd.services.traefik = { after = ["acme-finished-prg-radio.org.target"]; wants = ["acme-finished-prg-radio.org.target"]; }; networking.firewall.allowedTCPPorts = [80 443 10022 30033 8448]; networking.firewall.allowedUDPPorts = [80 443 9987]; system.stateVersion = "25.11"; }; }