{ "module": "fail2ban", "version": "1", "capabilities": [ "firewall" ], "claims": [ { "name": "node-intrusion-prevention", "scope": "node", "serves": [ "status", "banned", "ban", "unban" ] } ], "tools": [ "fail2ban_settings" ], "own-secrets": { "broker": "${dir:mesh-state}/broker" }, "jailing": { "into": "/etc/fail2ban/jail.d/mesh.conf", "filter-into": "/etc/fail2ban/filter.d" }, "resources": [ { "id": "package", "type": "package", "package": "fail2ban" }, { "id": "jail-d", "type": "directory", "path": "/etc/fail2ban/jail.d", "mode": "0755" }, { "id": "action-d", "type": "directory", "path": "/etc/fail2ban/action.d", "mode": "0755" }, { "id": "filter-d", "type": "directory", "path": "/etc/fail2ban/filter.d", "mode": "0755" }, { "id": "run-dir", "type": "directory", "path": "/var/run/fail2ban", "mode": "0755" }, { "id": "mesh-state", "type": "directory", "mode": "0700", "place": "mesh" }, { "id": "jail-local", "type": "file", "path": "/etc/fail2ban/jail.local", "mode": "0644", "content": "[INCLUDES]\n\nbefore = paths-arch.conf\n\n[DEFAULT]\n\n# Never act on the machine itself or on a tunnel peer: the mesh's private range is\n# ${machine:mesh-range}, named here rather than written as a value the module cannot\n# know (novox/hq ADR 0112). Without this, fail2ban could ban the mesh's own nodes.\nignoreip = 127.0.0.1/8 ::1 ${machine:mesh-range}\n\n# Three failures in a day ban for a day (novox/hq ADR 0179). The attackers this mesh sees pace\n# themselves at one try every ten minutes, under any ten-minute window; a day's window counts\n# them, and a day's ban costs a person who mistyped three times once, from one address, while\n# the mesh's own range is never banned at all.\nbantime = 1d\nfindtime = 1d\nmaxretry = 3\n\n# Ban through iptables, not through a firewall front-end the machine may not have. ufw is\n# installed on two of this mesh's machines and absent on the other two, and fail2ban finds out\n# only at ban time: the service reports healthy, the jail counts the attempt, the ban command\n# exits 127, and nothing is blocked. Proven on 2026-09-28 -- 'ufw: command not found' on a\n# machine the mesh reported as protected.\n#\n# The action below is this module's own, already used by the recidive jail on every machine\n# here, and it bans in DOCKER-USER as well as INPUT, so a container's published port is\n# covered too.\nbanaction = iptables-allports-dualchain\nbanaction_allports = iptables-allports-dualchain\n\n[sshd]\nenabled = true\nport = ssh\nlogpath = %(sshd_log)s\nbackend = %(sshd_backend)s\n" }, { "id": "jail-sshd", "type": "file", "path": "/etc/fail2ban/jail.d/sshd.conf", "mode": "0644", "content": "[sshd]\nenabled = true\nport = ssh\nlogpath = %(sshd_log)s\nbackend = %(sshd_backend)s\nmaxretry = 3\nfindtime = 1d\nbantime = 1d\n" }, { "id": "log", "type": "file", "path": "/var/log/fail2ban.log", "mode": "0640", "create-once": true, "content": "" }, { "id": "jail-recidive", "type": "file", "path": "/etc/fail2ban/jail.d/recidive.conf", "mode": "0644", "content": "[recidive]\nenabled = true\nlogpath = /var/log/fail2ban.log\n# Ban in both INPUT (host services like SSH) and DOCKER-USER (container services)\nbanaction = iptables-allports-dualchain\n# Banned twice in two weeks, by any jail, is banned for four (novox/hq ADR 0179).\nbantime = 4w\nfindtime = 2w\nmaxretry = 2\n" }, { "id": "action-dualchain", "type": "file", "path": "/etc/fail2ban/action.d/iptables-allports-dualchain.conf", "mode": "0644", "content": "# Fail2Ban action: ban in both INPUT and DOCKER-USER chains\n# Used by recidive to block repeat offenders from both host and Docker services\n\n[INCLUDES]\n\nbefore = iptables.conf\n\n[Definition]\n\ntype = allports\n\nactionstart = { -C f2b- -j >/dev/null 2>&1; } || { -N f2b- || true; -A f2b- -j ; }\n { -C INPUT -p -j f2b- >/dev/null 2>&1; } || { -I INPUT -p -j f2b-; }\n { -C DOCKER-USER -p -j f2b- >/dev/null 2>&1; } || { -I DOCKER-USER -p -j f2b-; }\n\nactionstop = -D INPUT -p -j f2b- 2>/dev/null || true\n -D DOCKER-USER -p -j f2b- 2>/dev/null || true\n -F f2b-\n -X f2b-\n\nactioncheck = -n -L f2b- >/dev/null\n\nactionban = -I f2b- 1 -s -j \n\nactionunban = -D f2b- -s -j \n\n[Init]\n\nchain = INPUT\nname = default\nprotocol = tcp\nblocktype = REJECT --reject-with icmp-port-unreachable\nreturntype = RETURN\nlockingopt = -w\niptables = iptables \n\n[Init?family=inet6]\n\nblocktype = REJECT --reject-with icmp6-port-unreachable\niptables = ip6tables \n" }, { "id": "logrotate", "type": "file", "path": "/etc/logrotate.d/fail2ban", "mode": "0644", "content": "/var/log/fail2ban.log {\n missingok\n notifempty\n postrotate\n /usr/bin/fail2ban-client flushlogs >/dev/null || true\n endscript\n}\n" }, { "id": "run", "type": "service", "unit": "fail2ban.service", "state": "running", "boot": "enabled", "restart-on": [ "jail-local", "jail-sshd", "jail-recidive", "action-dualchain", "composed-jails" ] }, { "id": "runtime", "type": "container", "name": "mesh-fail2ban", "artifact": "runtime", "network": "host", "volumes": [ "${dir:mesh-state}/broker:/run/secrets/broker:ro", "/var/run/fail2ban:/var/run/fail2ban" ], "env": { "MESH_BROKER_FILE": "/run/secrets/broker" } } ], "build": { "on": [ { "arg": "BUILD_BASE", "module": "mesh-tools", "artifact": "build" }, { "arg": "RUNTIME_BASE", "module": "mesh-tools", "artifact": "runtime" } ], "artifacts": [ { "name": "runtime", "kind": "image", "from": "Dockerfile" } ] } }