{ config, pkgs, lib, ... }: let cfg = config.services.crowdsec-firewall-bouncer; format = pkgs.formats.yaml { }; in { options.services.crowdsec-firewall-bouncer = let inherit (lib) types mkOption mkEnableOption mkPackageOption ; in { enable = mkEnableOption "CrowdSec Firewall Bouncer"; package = mkPackageOption pkgs "crowdsec-firewall-bouncer" { }; createRulesets = mkOption { type = types.bool; description = '' Whether to have the module create the appropriate firewall configuration based on the bouncer settings. You may disable this option to manually configure it. ''; default = true; }; registerBouncer = { enable = mkOption { type = types.bool; description = '' Whether to automatically register the bouncer to the locally running `crowdsec` service. When authenticating to an external CrowdSec API, you may use the [](#opt-services.crowdsec-firewall-bouncer.secrets.apiKeyPath) option instead. ''; default = config.services.crowdsec.enable; defaultText = lib.literalExpression "config.services.crowdsec.enable"; }; bouncerName = mkOption { type = types.nonEmptyStr; description = "Name to register the bouncer as to the CrowdSec API"; default = "crowdsec-firewall-bouncer"; }; }; secrets = { apiKeyPath = mkOption { type = types.nullOr types.path; description = '' Path to the API key to authenticate with a local CrowdSec API. You need to call `cscli bouncers add ` to register the bouncer and get this API key. When authenticating to the locally running `crowdsec` service, you may use the [](#opt-services.crowdsec-firewall-bouncer.registerBouncer.enable) option instead. ''; default = null; }; }; settings = mkOption { description = '' Settings for the main CrowdSec Firewall Bouncer. Refer to the defaults at . ''; default = { }; type = types.submodule { freeformType = format.type; options = { mode = mkOption { type = types.str; description = "Firewall mode to use."; default = if config.networking.nftables.enable then "nftables" else "iptables"; defaultText = lib.literalExpression ''if config.networking.nftables.enable then "nftables" else "iptables"''; }; api_url = mkOption { type = types.str; description = "URL of the local API."; example = "http://127.0.0.1:8080"; default = "http://${config.services.crowdsec.settings.config.api.server.listen_uri}"; defaultText = lib.literalExpression ''http://$\{config.services.crowdsec.settings.config.api.server.listen_uri}''; }; api_key = mkOption { type = types.nullOr types.str; description = '' API key to authenticate with a local crowdsec API. You need to call `cscli bouncers add ` to register the bouncer and get this API key. Setting this option will store this secret in the Nix store. Instead, you should set the `services.crowdsec-firewall-bouncer.secrets.apiKeyPath` option, which will read the value at runtime. ''; default = null; }; }; }; }; }; config = lib.mkIf cfg.enable { assertions = [ { assertion = cfg.registerBouncer.enable || (cfg.secrets.apiKeyPath != null) || (cfg.settings.api_key != null); message = '' An API key must be set for the bouncer to be able to authenticate to a local crowdsec API. See the `registerBouncer.enable` and `secrets.apiKeyPath` options of `services.crowdsec-firewall-bouncer` for more information. ''; } { assertion = !(cfg.registerBouncer.enable && (cfg.secrets.apiKeyPath != null)); message = '' The `registerBouncer.enable` and `secrets.apiKeyPath` options of `services.crowdsec-firewall-bouncer` are mutually exclusive. ''; } { assertion = !(cfg.registerBouncer.enable && !config.services.crowdsec.enable); message = '' The `services.crowdsec-firewall-bouncer.registerBouncer.enable` option requires the `crowdsec` service to be enabled. ''; } { assertion = !(cfg.settings.mode == "ipset" && cfg.createRulesets); message = '' The crowdsec-firewall-bouncer module is currently not able to configure the firewall in "ipset" mode. Either set the `services.crowdsec-firewall-bouncer.settings.mode` to "iptables" to leave the bouncer manage the firewall configuration, or disable the `services.crowdsec-firewall-bouncer.createRulesets` option and manually configure your firewall. ''; } ]; # Default settings services.crowdsec-firewall-bouncer.settings = { update_frequency = lib.mkDefault "10s"; log_mode = lib.mkDefault "stdout"; log_level = lib.mkDefault "info"; log_dir = lib.mkDefault "/var/log/crowdsec-firewall-bouncer"; # iptables-specific config blacklists_ipv4 = lib.mkDefault "crowdsec-blacklists"; blacklists_ipv6 = lib.mkDefault "crowdsec6-blacklists"; iptables_chains = lib.mkDefault [ "INPUT" ]; # nftables-specific config nftables = { ipv4 = { enabled = lib.mkDefault true; set-only = lib.mkDefault true; table = lib.mkDefault "crowdsec"; chain = lib.mkDefault "crowdsec-chain"; }; ipv6 = { enabled = lib.mkDefault true; set-only = lib.mkDefault true; table = lib.mkDefault "crowdsec6"; chain = lib.mkDefault "crowdsec6-chain"; }; }; }; # Use a placeholder for the api_key if it is to be read from a file at runtime services.crowdsec-firewall-bouncer.settings.api_key = lib.mkIf ( cfg.registerBouncer.enable || (cfg.secrets.apiKeyPath != null) ) "@API_KEY_FILE@"; networking.nftables.tables = lib.mkIf (cfg.settings.mode == "nftables") { "${cfg.settings.nftables.ipv4.table}" = lib.mkIf (cfg.createRulesets && cfg.settings.nftables.ipv4.enabled && cfg.settings.nftables.ipv4.set-only) { family = "ip"; content = '' set crowdsec-blacklists { type ipv4_addr flags timeout } chain ${cfg.settings.nftables.ipv4.chain} { type filter hook input priority filter; policy accept; ip saddr @crowdsec-blacklists drop } ''; }; "${cfg.settings.nftables.ipv6.table}" = lib.mkIf (cfg.createRulesets && cfg.settings.nftables.ipv6.enabled && cfg.settings.nftables.ipv6.set-only) { family = "ip6"; content = '' set crowdsec6-blacklists { type ipv6_addr flags timeout } chain ${cfg.settings.nftables.ipv6.chain} { type filter hook input priority filter; policy accept; ip6 saddr @crowdsec6-blacklists drop } ''; }; }; systemd.services = let apiKeyFile = "/var/lib/crowdsec-firewall-bouncer-register/api-key.cred"; in { crowdsec-firewall-bouncer-register = lib.mkIf cfg.registerBouncer.enable rec { description = "Register the CrowdSec Firewall Bouncer to the local CrowdSec service"; wantedBy = [ "multi-user.target" ]; after = [ "crowdsec.service" ]; wants = after; path = [ config.services.crowdsec.package ]; script = '' # Ensure the directory exists mkdir -p "$(dirname ${apiKeyFile})" || true echo "Checking bouncer registration..." if cscli bouncers list --output json | ${lib.getExe pkgs.jq} -e -- ${lib.escapeShellArg "any(.[]; .name == \"${cfg.registerBouncer.bouncerName}\")"} >/dev/null; then echo "Bouncer already registered. Verify the API key is still present" if [ ! -f ${apiKeyFile} ]; then echo "Bouncer registered but API key is not present" echo "Unregistering bouncer..." cscli bouncers delete ${cfg.registerBouncer.bouncerName} || true else echo "API key file exists, nothing to do" exit 0 fi else echo "Bouncer not registered" echo "Remove any previously saved API key" rm -f '${apiKeyFile}' fi echo "Register the bouncer and save the new API key" if ! cscli bouncers add --output raw -- ${lib.escapeShellArg cfg.registerBouncer.bouncerName} > ${apiKeyFile} 2>&1; then echo "Failed to register the bouncer" cat ${apiKeyFile} || true # Show error message rm -f ${apiKeyFile} exit 1 fi chmod 0440 ${apiKeyFile} || true echo "Successfully registered bouncer and saved API key" cscli bouncers list ''; serviceConfig = { Type = "oneshot"; # Run as crowdsec user to be able to use cscli User = config.services.crowdsec.user; Group = config.services.crowdsec.group; StateDirectory = "crowdsec-firewall-bouncer-register crowdsec"; StateDirectoryMode = "0750"; DynamicUser = true; LockPersonality = true; PrivateDevices = true; ProcSubset = "pid"; ProtectClock = true; ProtectControlGroups = true; ProtectHome = true; ProtectHostname = true; ProtectKernelLogs = true; ProtectKernelModules = true; ProtectKernelTunables = true; ProtectProc = "invisible"; RestrictNamespaces = true; RestrictRealtime = true; SystemCallArchitectures = "native"; RestrictAddressFamilies = "none"; CapabilityBoundingSet = [ "" ]; SystemCallFilter = [ "@system-service" "~@privileged" "~@resources" ]; UMask = "0077"; ExecStartPost = "+systemctl --no-block try-restart crowdsec-firewall-bouncer.service"; }; }; crowdsec-firewall-bouncer = let runtime-dir-name = "crowdsec-firewall-bouncer"; final-config-file = "/run/${runtime-dir-name}/config.yaml"; generateConfig = pkgs.writeShellScript "crowdsec-firewall-bouncer-config" '' set -euo pipefail umask 077 # Copy the template to the final location cp ${format.generate "crowdsec-firewall-bouncer-config-template.yml" cfg.settings} ${final-config-file} chmod 0600 ${final-config-file} # Replace the api_key placeholder with the secret ${lib.getExe pkgs.replace-secret} '@API_KEY_FILE@' "$CREDENTIALS_DIRECTORY/API_KEY_FILE" ${final-config-file} ''; isIptables = (cfg.settings.mode == "iptables") || (cfg.settings.mode == "ipset"); isNftables = cfg.settings.mode == "nftables"; in rec { description = "CrowdSec Firewall Bouncer"; wantedBy = [ "multi-user.target" ]; partOf = lib.optional isNftables "nftables.service" ++ lib.optional isIptables "firewall.service"; after = lib.optional isNftables "nftables.service" ++ lib.optional isIptables "firewall.service" ++ lib.optional config.services.crowdsec.enable "crowdsec.service"; wants = after; requires = lib.optional cfg.registerBouncer.enable "crowdsec-firewall-bouncer-register.service"; # When using iptables/ipset modes, the bouncer calls external binaries so they must be added to the path. # For nftables mode, it does not depend on external binaries. path = lib.optionals isIptables [ pkgs.iptables pkgs.ipset ]; serviceConfig = rec { Type = "notify"; ExecStartPre = [ generateConfig "${lib.getExe cfg.package} -c ${final-config-file} -t" ]; ExecStart = [ "${lib.getExe cfg.package} -c ${final-config-file}" ]; # Same as upstream LimitNOFILE = 65536; KillMode = "mixed"; # Load the api_key secret to be able to use it when generating the final config LoadCredential = if (cfg.registerBouncer.enable) then "API_KEY_FILE:${apiKeyFile}" else if (cfg.secrets.apiKeyPath != null) then "API_KEY_FILE:${cfg.secrets.apiKeyPath}" else null; # Run as crowdsec user to be able to use cscli User = config.services.crowdsec.user; Group = config.services.crowdsec.group; DynamicUser = true; RuntimeDirectory = runtime-dir-name; StateDirectory = "crowdsec-firewall-bouncer-register crowdsec"; StateDirectoryMode = "0750"; LogsDirectory = "crowdsec-firewall-bouncer"; LockPersonality = true; PrivateDevices = true; ProcSubset = "pid"; ProtectClock = true; ProtectControlGroups = true; ProtectHome = true; ProtectHostname = true; ProtectKernelLogs = true; ProtectKernelModules = true; ProtectKernelTunables = true; ProtectProc = "invisible"; RestrictNamespaces = true; RestrictRealtime = true; SystemCallArchitectures = "native"; RestrictAddressFamilies = [ "AF_NETLINK" "AF_UNIX" "AF_INET" "AF_INET6" ]; AmbientCapabilities = [ # Needed to be able to manipulate the rulesets "CAP_NET_ADMIN" ] ++ lib.optional ((cfg.settings.mode == "iptables") || (cfg.settings.mode == "ipset")) "CAP_NET_RAW"; CapabilityBoundingSet = AmbientCapabilities; SystemCallFilter = [ "@system-service" "~@privileged" "~@resources" ]; UMask = "0077"; Restart = "always"; }; }; }; }; meta = { maintainers = with lib.maintainers; [ nicomem tornax ]; }; }