diff --git a/common.nix b/common.nix index 31b06c4..acc81e3 100644 --- a/common.nix +++ b/common.nix @@ -30,6 +30,7 @@ ./services/bazarr.nix ./services/cloudflare-ddns.nix ./services/fail2ban.nix + ./services/authelia.nix ./services/homepage.nix ./services/arr-interconnect.nix ]; diff --git a/services/authelia.nix b/services/authelia.nix new file mode 100644 index 0000000..2700b59 --- /dev/null +++ b/services/authelia.nix @@ -0,0 +1,103 @@ +# services/authelia.nix — Native Authelia SSO with auto-migration from Docker +{ config, lib, pkgs, ... }: +let + # Migrates secrets + user DB from the old Docker Authelia setup + setupScript = pkgs.writeShellScript "authelia-setup" '' + set -euo pipefail + YQ="${pkgs.yq-go}/bin/yq" + DOCKER_CONFIG="/home/fred/docker/authelia/configuration.yml" + SECRETS_DIR="/var/secrets/authelia" + STATE_DIR="/var/lib/authelia-main" + + mkdir -p "$SECRETS_DIR" + + # Migrate secrets from Docker config if they haven't been extracted yet + if [ -f "$DOCKER_CONFIG" ]; then + if [ ! -f "$SECRETS_DIR/jwt_secret" ]; then + $YQ '.identity_validation.reset_password.jwt_secret' "$DOCKER_CONFIG" \ + | tr -d '"' > "$SECRETS_DIR/jwt_secret" + echo "Migrated jwt_secret" + fi + if [ ! -f "$SECRETS_DIR/session_secret" ]; then + $YQ '.session.secret' "$DOCKER_CONFIG" \ + | tr -d '"' > "$SECRETS_DIR/session_secret" + echo "Migrated session_secret" + fi + if [ ! -f "$SECRETS_DIR/storage_encryption_key" ]; then + $YQ '.storage.encryption_key' "$DOCKER_CONFIG" \ + | tr -d '"' > "$SECRETS_DIR/storage_encryption_key" + echo "Migrated storage_encryption_key" + fi + fi + + chmod 644 "$SECRETS_DIR"/* + + # Migrate users database + if [ ! -f "$STATE_DIR/users_database.yml" ] && \ + [ -f "/home/fred/docker/authelia/users_database.yml" ]; then + cp /home/fred/docker/authelia/users_database.yml "$STATE_DIR/" + chown authelia-main:authelia-main "$STATE_DIR/users_database.yml" + echo "Migrated users_database.yml" + fi + + echo "Authelia setup complete." + ''; +in +{ + config = lib.mkIf (config.networking.hostName == "FredOS-Mediaserver") { + + services.authelia.instances.main = { + enable = true; + + secrets = { + jwtSecretFile = "/var/secrets/authelia/jwt_secret"; + storageEncryptionKeyFile = "/var/secrets/authelia/storage_encryption_key"; + sessionSecretFile = "/var/secrets/authelia/session_secret"; + }; + + settings = { + theme = "dark"; + server.address = "tcp://127.0.0.1:9091/"; + + log = { + level = "info"; + format = "text"; + }; + + authentication_backend.file.path = "/var/lib/authelia-main/users_database.yml"; + + access_control = { + default_policy = "deny"; + rules = [ + { domain = "camera.nordhammer.it"; policy = "one_factor"; } + { domain = "homepage.nordhammer.it"; policy = "one_factor"; } + ]; + }; + + session = { + cookies = [{ + domain = "nordhammer.it"; + authelia_url = "https://auth.nordhammer.it"; + }]; + expiration = "1h"; + inactivity = "5m"; + }; + + storage.local.path = "/var/lib/authelia-main/db.sqlite3"; + notifier.filesystem.filename = "/var/lib/authelia-main/notification.txt"; + }; + }; + + # Auto-migrate Docker Authelia data on first deploy + systemd.services.authelia-setup = { + description = "Migrate Authelia secrets and user database from Docker"; + before = [ "authelia-main.service" ]; + requiredBy = [ "authelia-main.service" ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + ExecStart = setupScript; + }; + }; + }; +} diff --git a/services/fail2ban.nix b/services/fail2ban.nix index 1812be2..e5657f3 100644 --- a/services/fail2ban.nix +++ b/services/fail2ban.nix @@ -37,17 +37,29 @@ }; }; - # Nginx Proxy Manager — watches Docker-mounted log files for 401/403s - nginx-proxy-manager = { + # Nginx — watches access log for HTTP auth failures + nginx = { settings = { enabled = true; filter = "nginx-http-auth"; - logpath = "/home/fred/docker/nginx-proxy-manager/data/logs/*.log"; + logpath = "/var/log/nginx/access.log"; maxretry = 10; bantime = "1h"; }; }; + # Authelia — failed login attempts via journald + authelia = { + settings = { + enabled = true; + backend = "systemd"; + journalmatch = "_SYSTEMD_UNIT=authelia-main.service"; + filter = "authelia"; + maxretry = 5; + bantime = "2h"; + }; + }; + # Jellyfin auth failures — journald jellyfin = { settings = { @@ -140,6 +152,13 @@ ignoreregex = ''; + # Authelia filter + environment.etc."fail2ban/filter.d/authelia.conf".text = '' + [Definition] + failregex = ^.*Unsuccessful .* authentication attempt by user .* from .*$ + ignoreregex = + ''; + # Jellyfin filter environment.etc."fail2ban/filter.d/jellyfin.conf".text = '' [Definition] diff --git a/services/go2rtc.nix b/services/go2rtc.nix index d93e146..6644b2d 100644 --- a/services/go2rtc.nix +++ b/services/go2rtc.nix @@ -1,38 +1,18 @@ -#/services/go2rtc.nix -{ config, pkgs, lib, ... }: - +# services/go2rtc.nix — Native go2rtc camera streaming +{ config, lib, ... }: { config = lib.mkIf (config.networking.hostName == "FredOS-Mediaserver") { - virtualisation.oci-containers = { - backend = "docker"; - - # --- Authelia --- - containers."authelia" = { - image = "authelia/authelia:latest"; - volumes = [ - "/home/fred/docker/authelia:/config" - "/home/fred/docker/authelia/users_database.yml:/config/users_database.yml" - "/home/fred/docker/authelia/secrets:/secrets" - ]; - ports = [ "9091:9091" ]; - }; - - # --- Go2RTC --- - containers."go2rtc" = { - image = "alexxit/go2rtc:latest"; - volumes = [ - "/home/fred/docker/go2rtc/config.yml:/config/go2rtc.yaml" - ]; - ports = [ "1984:1984" ]; + services.go2rtc = { + enable = true; + settings = { + # NOTE: RTSP credentials end up in the nix store — same exposure as + # the old Docker bind-mount config. Acceptable for a local LAN camera. + streams.kids_bedroom = "rtsp://fredrik:12345678@192.168.4.39:554/stream1"; + api.listen = ":1984"; + webrtc.listen = ":8555"; }; }; - # --- Create directories --- - systemd.tmpfiles.rules = [ - # Local secrets & configs - "d /home/fred/docker/authelia/secrets 0700 fred users -" - "d /home/fred/docker/go2rtc 0755 fred users -" - ]; }; } diff --git a/services/homepage.nix b/services/homepage.nix index 6da1911..2a62e53 100644 --- a/services/homepage.nix +++ b/services/homepage.nix @@ -87,7 +87,7 @@ in # Allow access from anywhere on the LAN # Add your domain here too if you expose it via Nginx Proxy Manager - allowedHosts = "localhost:8082,127.0.0.1:8082,192.168.4.74:8082"; + allowedHosts = "localhost:8082,127.0.0.1:8082,homepage.nordhammer.it"; # API keys auto-extracted by homepage-extract-secrets.service environmentFiles = [ "/etc/homepage-secrets" ]; @@ -136,12 +136,12 @@ in Media = [ { Jellyfin = { - href = "http://192.168.4.74:8096"; + href = "https://jellyfin.nordhammer.it"; description = "Media server"; icon = "jellyfin.png"; widget = { type = "jellyfin"; - url = "http://192.168.4.74:8096"; + url = "http://127.0.0.1:8096"; key = "{{HOMEPAGE_VAR_JELLYFIN_KEY}}"; enableBlocks = true; enableNowPlaying = true; @@ -150,24 +150,24 @@ in } { Bazarr = { - href = "http://192.168.4.74:6767"; + href = "https://bazarr.nordhammer.it"; description = "Subtitle management"; icon = "bazarr.png"; widget = { type = "bazarr"; - url = "http://192.168.4.74:6767"; + url = "http://127.0.0.1:6767"; key = "{{HOMEPAGE_VAR_BAZARR_KEY}}"; }; }; } { Sonarr = { - href = "http://192.168.4.74:8989"; + href = "https://sonarr.nordhammer.it"; description = "TV show management"; icon = "sonarr.png"; widget = { type = "sonarr"; - url = "http://192.168.4.74:8989"; + url = "http://127.0.0.1:8989"; key = "{{HOMEPAGE_VAR_SONARR_KEY}}"; enableQueue = true; }; @@ -175,12 +175,12 @@ in } { Radarr = { - href = "http://192.168.4.74:7878"; + href = "https://radarr.nordhammer.it"; description = "Movie management"; icon = "radarr.png"; widget = { type = "radarr"; - url = "http://192.168.4.74:7878"; + url = "http://127.0.0.1:7878"; key = "{{HOMEPAGE_VAR_RADARR_KEY}}"; enableQueue = true; }; @@ -192,7 +192,7 @@ in Downloads = [ { qBittorrent = { - href = "http://192.168.4.74:8080"; + href = "https://torrent.nordhammer.it"; description = "Torrent client"; icon = "qbittorrent.png"; widget = { @@ -203,12 +203,12 @@ in } { Prowlarr = { - href = "http://192.168.4.74:9696"; + href = "https://prowlarr.nordhammer.it"; description = "Indexer manager"; icon = "prowlarr.png"; widget = { type = "prowlarr"; - url = "http://192.168.4.74:9696"; + url = "http://127.0.0.1:9696"; key = "{{HOMEPAGE_VAR_PROWLARR_KEY}}"; }; }; @@ -217,23 +217,16 @@ in } { Infrastructure = [ - { - "Nginx Proxy Manager" = { - href = "http://192.168.4.74:81"; - description = "Reverse proxy"; - icon = "nginx-proxy-manager.png"; - }; - } { Authelia = { - href = "http://192.168.4.74:9091"; + href = "https://auth.nordhammer.it"; description = "SSO & 2FA"; icon = "authelia.png"; }; } { go2rtc = { - href = "http://192.168.4.74:1984"; + href = "https://camera.nordhammer.it"; description = "Camera streams"; icon = "go2rtc.png"; }; diff --git a/services/nginx.nix b/services/nginx.nix index 4dbce25..68e4fd6 100644 --- a/services/nginx.nix +++ b/services/nginx.nix @@ -1,34 +1,113 @@ -#nginx.nix -{ config, pkgs, lib, ... }: -{ - config = lib.mkIf (config.networking.hostName == "FredOS-Mediaserver") { +# services/nginx.nix — Native nginx reverse proxy with ACME wildcard cert +{ config, lib, ... }: +let + # Authelia forward-auth snippet injected into protected locations + autheliaAuthConfig = '' + auth_request /internal/authelia/authz; + auth_request_set $target_url $scheme://$http_host$request_uri; + auth_request_set $user $upstream_http_remote_user; + auth_request_set $groups $upstream_http_remote_groups; + error_page 401 =302 https://auth.nordhammer.it/?rd=$target_url; + ''; - # Nginx Proxy Manager - virtualisation.oci-containers = { - backend = "docker"; - - containers."nginx-proxy-manager" = { - image = "jc21/nginx-proxy-manager:latest"; - ports = [ - "80:80" - "81:81" - "443:443" - ]; - volumes = [ - "/home/fred/docker/nginx-proxy-manager/data:/data" - "/home/fred/docker/nginx-proxy-manager/letsencrypt:/etc/letsencrypt" - ]; - # Remove the extraOptions with --restart, it conflicts with --rm - }; - }; - - # Create directories - systemd.tmpfiles.rules = [ - "d /home/fred/docker/nginx-proxy-manager/data 0755 root root -" - "d /home/fred/docker/nginx-proxy-manager/letsencrypt 0755 root root -" - ]; - - # Open firewall - networking.firewall.allowedTCPPorts = [ 80 81 443 ]; - }; + # Internal location that queries Authelia's verification endpoint + autheliaLocation = { + "/internal/authelia/authz" = { + proxyPass = "http://127.0.0.1:9091/api/authz/forward-auth"; + extraConfig = '' + internal; + proxy_set_header X-Original-Method $request_method; + proxy_set_header X-Original-URL $scheme://$http_host$request_uri; + proxy_set_header X-Forwarded-Method $request_method; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $http_host; + proxy_set_header X-Forwarded-URI $request_uri; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header Content-Length ""; + proxy_set_header Connection ""; + ''; + }; + }; + + ssl = { + useACMEHost = "nordhammer.it"; + forceSSL = true; + }; + + # Simple reverse proxy vhost + proxy = port: ssl // { + locations."/" = { + proxyPass = "http://127.0.0.1:${toString port}"; + proxyWebsockets = true; + }; + }; + + # Reverse proxy protected by Authelia forward auth + protectedProxy = port: ssl // { + locations = autheliaLocation // { + "/" = { + proxyPass = "http://127.0.0.1:${toString port}"; + proxyWebsockets = true; + extraConfig = autheliaAuthConfig; + }; + }; + }; +in +{ + config = lib.mkIf (config.networking.hostName == "FredOS-Mediaserver") { + + # Wildcard TLS cert via Cloudflare DNS-01 challenge + security.acme = { + acceptTerms = true; + defaults.email = "fredrik@nordhammer.it"; + certs."nordhammer.it" = { + domain = "*.nordhammer.it"; + extraDomainNames = [ "nordhammer.it" ]; + dnsProvider = "cloudflare"; + credentialFiles = { + "CF_DNS_API_TOKEN_FILE" = "/var/secrets/cloudflare-token"; + }; + }; + }; + + users.users.nginx.extraGroups = [ "acme" ]; + + services.nginx = { + enable = true; + recommendedProxySettings = true; + recommendedTlsSettings = true; + recommendedOptimisation = true; + recommendedGzipSettings = true; + + # File-based access log for fail2ban + appendHttpConfig = '' + access_log /var/log/nginx/access.log; + ''; + + virtualHosts = { + # --- Authelia portal (not behind auth itself) --- + "auth.nordhammer.it" = proxy 9091; + + # --- Media --- + "jellyfin.nordhammer.it" = proxy 8096; + "bazarr.nordhammer.it" = proxy 6767; + "sonarr.nordhammer.it" = proxy 8989; + "radarr.nordhammer.it" = proxy 7878; + + # --- Downloads --- + "prowlarr.nordhammer.it" = proxy 9696; + "torrent.nordhammer.it" = proxy 8080; + + # --- Other --- + "games.nordhammer.it" = proxy 8787; + "search.nordhammer.it" = proxy 8087; + + # --- Protected by Authelia --- + "camera.nordhammer.it" = protectedProxy 1984; + "homepage.nordhammer.it" = protectedProxy 8082; + }; + }; + + networking.firewall.allowedTCPPorts = [ 80 443 ]; + }; }