diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..706dc1c --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,30 @@ +# FredOS NixOS Configuration + +This is a NixOS flake-based configuration for multiple hosts: +- **FredOS-Gaming** — gaming desktop +- **FredOS-Mediaserver** — home media server +- **FredOS-Macbook** — MacBook laptop + +## Structure + +- `flake.nix` — flake inputs/outputs; all hosts use `nixpkgs` unstable +- `common.nix` — shared configuration across all hosts +- `hosts/` — per-host NixOS configuration modules +- `hosts/hardware/` — hardware-specific configuration +- `home-manager/` — Home Manager configuration (via NixOS module) +- `services/` — modular service definitions imported by hosts +- `settings/` — shared settings/variables + +## Code Evaluation + +Always validate Nix expressions with `nix eval` before committing. For example: + +```bash +# Evaluate a specific attribute to check for syntax/type errors +nix eval .#nixosConfigurations.FredOS-Gaming.config.system.stateVersion + +# Evaluate the full flake outputs to catch top-level errors +nix eval .#nixosConfigurations --apply builtins.attrNames +``` + +Use `nix flake check` for a broader check of the flake. diff --git a/common.nix b/common.nix index 416895e..42d089d 100644 --- a/common.nix +++ b/common.nix @@ -29,7 +29,7 @@ ./services/jellyfin.nix ./services/bazarr.nix ./services/cloudflare-ddns.nix - ./services/crowdsec.nix + ./services/fail2ban.nix ]; ### Make build time quicker diff --git a/hosts/FredOS-Mediaserver.nix b/hosts/FredOS-Mediaserver.nix index a27c18a..72ed92a 100644 --- a/hosts/FredOS-Mediaserver.nix +++ b/hosts/FredOS-Mediaserver.nix @@ -18,8 +18,6 @@ yt-dlp ]; - services.fail2ban.enable = true; - # Enable Docker virtualisation.docker.enable = true; diff --git a/services/crowdsec.nix b/services/crowdsec.nix deleted file mode 100644 index 1feb160..0000000 --- a/services/crowdsec.nix +++ /dev/null @@ -1,43 +0,0 @@ -{ config, lib, pkgs, ... }: -{ - config = lib.mkIf (config.networking.hostName == "FredOS-Mediaserver") { - services.crowdsec = { - enable = true; - autoUpdateService = true; - - # Install detection collections on first boot - hub.collections = [ "crowdsecurity/linux" "crowdsecurity/sshd" ]; - - settings = { - # Enable the Local API server (required for bouncer registration) - general.api.server.enable = true; - # Where the LAPI client credentials will be written on first boot - lapi.credentialsFile = "/var/lib/crowdsec/state/lapi-credentials.yaml"; - }; - - localConfig.acquisitions = [ - # SSH brute-force detection - { - source = "journalctl"; - journalctl_filter = [ "-u" "sshd" ]; - labels.type = "syslog"; - } - ]; - }; - - # The bouncer-register service uses raw cscli (no -c flag), so it looks for - # config at /etc/crowdsec/config.yaml. Symlink the Nix-generated config there. - systemd.tmpfiles.rules = [ - "L+ /etc/crowdsec/config.yaml - - - - ${(pkgs.formats.yaml { }).generate "crowdsec.yaml" config.services.crowdsec.settings.general}" - ]; - - # Ensure /var/lib/crowdsec exists before crowdsec starts (race with tmpfiles-resetup) - systemd.services.crowdsec.after = [ "systemd-tmpfiles-resetup.service" ]; - - # Firewall bouncer — auto-registers to local CrowdSec LAPI - services.crowdsec-firewall-bouncer = { - enable = true; - settings.api_url = "http://127.0.0.1:8080"; - }; - }; -} diff --git a/services/fail2ban.nix b/services/fail2ban.nix new file mode 100644 index 0000000..56fc95f --- /dev/null +++ b/services/fail2ban.nix @@ -0,0 +1,152 @@ +{ config, lib, pkgs, ... }: +{ + config = lib.mkIf (config.networking.hostName == "FredOS-Mediaserver") { + + services.fail2ban = { + enable = true; + + # Default ban settings (overridable per jail) + maxretry = 5; + bantime = "1h"; + + # Progressively longer bans for repeat offenders, up to 1 week + bantime-increment = { + enable = true; + multiplier = "1 2 4 8 16 32 64"; + maxtime = "168h"; + overalljails = true; + }; + + # Never ban local network traffic + ignoreIP = [ + "127.0.0.1/8" + "::1" + "192.168.0.0/16" + "10.0.0.0/8" + ]; + + jails = { + + # SSH brute force — built-in sshd filter via journald + sshd = { + settings = { + enabled = true; + filter = "sshd"; + maxretry = 5; + bantime = "1h"; + }; + }; + + # Nginx Proxy Manager — watches Docker-mounted log files for 401/403s + nginx-proxy-manager = { + settings = { + enabled = true; + filter = "nginx-http-auth"; + logpath = "/home/fred/docker/nginx-proxy-manager/data/logs/*.log"; + maxretry = 10; + bantime = "1h"; + }; + }; + + # Jellyfin auth failures — journald + jellyfin = { + settings = { + enabled = true; + backend = "systemd"; + journalmatch = "_SYSTEMD_UNIT=jellyfin.service"; + maxretry = 5; + bantime = "2h"; + }; + }; + + # Sonarr — log files at dataDir/logs/ + sonarr = { + settings = { + enabled = true; + filter = "arr-apps"; + logpath = "/var/lib/sonarr/logs/*.txt"; + maxretry = 5; + bantime = "1h"; + }; + }; + + # Radarr — log files at dataDir/logs/ + radarr = { + settings = { + enabled = true; + filter = "arr-apps"; + logpath = "/var/lib/radarr/logs/*.txt"; + maxretry = 5; + bantime = "1h"; + }; + }; + + # Prowlarr — log files at dataDir/logs/ + prowlarr = { + settings = { + enabled = true; + filter = "arr-apps"; + logpath = "/var/lib/prowlarr/logs/*.txt"; + maxretry = 5; + bantime = "1h"; + }; + }; + + # Bazarr — log files at dataDir/log/ + bazarr = { + settings = { + enabled = true; + filter = "bazarr"; + logpath = "/var/lib/bazarr/log/*.txt"; + maxretry = 5; + bantime = "1h"; + }; + }; + + # qBittorrent-nox — watches journald for web UI login failures + qbittorrent = { + settings = { + enabled = true; + filter = "qbittorrent"; + backend = "systemd"; + journalmatch = "_SYSTEMD_UNIT=qbittorrent-nox.service"; + maxretry = 5; + bantime = "1h"; + }; + }; + + }; + }; + + # Shared filter for Sonarr, Radarr, Prowlarr — they all use the same *arr codebase + environment.etc."fail2ban/filter.d/arr-apps.conf".text = '' + [Definition] + failregex = .*Auth-Failure ip + ignoreregex = + ''; + + # Bazarr (Python/Flask) auth failure filter + environment.etc."fail2ban/filter.d/bazarr.conf".text = '' + [Definition] + failregex = .*login attempt.* + .*unauthorized.* + ignoreregex = + ''; + + # qBittorrent web UI login failure filter + environment.etc."fail2ban/filter.d/qbittorrent.conf".text = '' + [Definition] + failregex = .*WebAPI login failure.*remote IP: + ignoreregex = + ''; + + # Jellyfin filter + environment.etc."fail2ban/filter.d/jellyfin.conf".text = '' + [Definition] + failregex = ^.*Authentication request for .* has been denied \(IP: ""\).*$ + ^.*Error processing request from remote IP Address .*$ + ignoreregex = + ''; + + }; +}