# from @notthebee { lib, config, pkgs, ... }: let cfg = config.server.fail2ban; in { options.server.fail2ban = { enable = lib.mkEnableOption { description = "Enable cloudflare fail2ban"; }; apiKeyFile = lib.mkOption { description = "File containing your API key, scoped to Firewall Rules: Edit"; type = lib.types.str; example = lib.literalExpression '' Authorization: Bearer vH6-p0y=i4w3n7TjKqZ@x8D_lR!A9b2cOezXgUuJdE5F ''' ''; }; zoneId = lib.mkOption { type = lib.types.str; }; jails = lib.mkOption { type = lib.types.attrsOf ( lib.types.submodule { options = { serviceName = lib.mkOption { example = "vaultwarden"; type = lib.types.str; }; failRegex = lib.mkOption { type = lib.types.str; example = "Login failed from IP: "; }; ignoreRegex = lib.mkOption { type = lib.types.str; default = ""; }; maxRetry = lib.mkOption { type = lib.types.int; default = 3; }; }; } ); }; }; config = lib.mkIf cfg.enable { services.fail2ban = { enable = true; extraPackages = [ pkgs.curl pkgs.jq ]; jails = lib.attrsets.mapAttrs (name: value: { settings = { bantime = "168h"; findtime = "30m"; enabled = true; backend = "systemd"; journalmatch = "_SYSTEMD_UNIT=${value.serviceName}.service"; port = "http,https"; filter = "${name}"; maxretry = value.maxRetry or 5; action = "cloudflare-token-agenix"; }; }) cfg.jails; }; environment.etc = lib.attrsets.mergeAttrsList [ (lib.attrsets.mapAttrs' ( name: value: (lib.nameValuePair "fail2ban/filter.d/${name}.conf" { text = '' [Definition] failregex = ${value.failRegex} ignoreregex = ${value.ignoreRegex} ''; }) ) cfg.jails) { "fail2ban/action.d/cloudflare-token-agenix.conf".text = let notes = "Fail2Ban on ${config.networking.hostName}"; cfapi = "https://api.cloudflare.com/client/v4/zones/${cfg.zoneId}/firewall/access_rules/rules"; in '' [Definition] actionstart = actionstop = actioncheck = actionunban = id=$(curl -s -X GET "${cfapi}" \ -H @${cfg.apiKeyFile} -H "Content-Type: application/json" \ | jq -r '.result[] | select(.notes == "${notes}" and .configuration.target == "ip" and .configuration.value == "") | .id') if [ -z "$id" ]; then echo "id for cannot be found"; exit 0; fi; \ curl -s -X DELETE "${cfapi}/$id" \ -H @${cfg.apiKeyFile} -H "Content-Type: application/json" \ --data '{"cascade": "none"}' actionban = curl -X POST "${cfapi}" -H @${cfg.apiKeyFile} -H "Content-Type: application/json" --data '{"mode":"block","configuration":{"target":"ip","value":""},"notes":"${notes}"}' [Init] name = cloudflare-token-agenix ''; } ]; }; }