Take the catalogue's composition rules into the node-engine

ADR 0593 §7: the composer with its refusals leaves mesh-sdk, so mesh-sdk
carries no composition rule. The catalogue functions only the
node-engine's composition reaches (Resolve, Resolution's Compose,
Declaration, DataItems, LeftOut, Rules and the rest, the settings,
placeholder and template refusals, the rendering helpers) are copied
with their tests into internal/compose/resolution; a method of a
mesh-sdk shape becomes a function taking it first. The composition
calls its own copies, and the captured catalogue its tests read lives
here now, shared by mesh-compose's tests.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
This commit is contained in:
2026-11-02 19:15:55 +01:00
parent be0226b624
commit d68e241724
686 changed files with 35703 additions and 80 deletions
@@ -0,0 +1,7 @@
/var/log/fail2ban.log {
missingok
notifempty
postrotate
/usr/bin/fail2ban-client flushlogs >/dev/null || true
endscript
}
@@ -0,0 +1,40 @@
# Fail2Ban action: ban in both INPUT and DOCKER-USER chains
# Used by recidive to block repeat offenders from both host and Docker services
[INCLUDES]
before = iptables.conf
[Definition]
type = allports
actionstart = { <iptables> -C f2b-<name> -j <returntype> >/dev/null 2>&1; } || { <iptables> -N f2b-<name> || true; <iptables> -A f2b-<name> -j <returntype>; }
{ <iptables> -C INPUT -p <protocol> -j f2b-<name> >/dev/null 2>&1; } || { <iptables> -I INPUT -p <protocol> -j f2b-<name>; }
{ <iptables> -C DOCKER-USER -p <protocol> -j f2b-<name> >/dev/null 2>&1; } || { <iptables> -I DOCKER-USER -p <protocol> -j f2b-<name>; }
actionstop = <iptables> -D INPUT -p <protocol> -j f2b-<name> 2>/dev/null || true
<iptables> -D DOCKER-USER -p <protocol> -j f2b-<name> 2>/dev/null || true
<iptables> -F f2b-<name>
<iptables> -X f2b-<name>
actioncheck = <iptables> -n -L f2b-<name> >/dev/null
actionban = <iptables> -I f2b-<name> 1 -s <ip> -j <blocktype>
actionunban = <iptables> -D f2b-<name> -s <ip> -j <blocktype>
[Init]
chain = INPUT
name = default
protocol = tcp
blocktype = REJECT --reject-with icmp-port-unreachable
returntype = RETURN
lockingopt = -w
iptables = iptables <lockingopt>
[Init?family=inet6]
blocktype = REJECT --reject-with icmp6-port-unreachable
iptables = ip6tables <lockingopt>
@@ -0,0 +1,48 @@
[INCLUDES]
before = paths-arch.conf
[DEFAULT]
# Never act on the machine itself or on a tunnel peer: the mesh's private range is
# ${machine:mesh-range}, named here rather than written as a value the module cannot
# know (novox/hq ADR 0112). Without this, fail2ban could ban the mesh's own nodes.
# **A ban list never holds a neighbour.** The mesh's own range is named rather than written
# (novox/hq ADR 0112), and every private range beside it: a source on one is somebody's own
# network, not the internet. On a machine behind a router that reflects local traffic, every
# client in the house arrives as the gateway's address — so one mistyped local request banned
# 192.168.1.1 on the home server and would have cut the whole house off from it (ADR 0186).
#
# **Nor the operator's own addresses** (novox/hq ADR 0339, issue 238): the public addresses the operator names
# as theirs, in the mesh-wide setting own-addresses. An agent polling the forge from the operator's home banned
# that home for four weeks on 2026-10-04, and nothing in the house reached the control-node until it was found
# among 400 bans. The file is written by this module's watch from the setting, one entry per line; fail2ban
# reads it again within a second of a change, so a new value needs no restart. Empty until the operator
# fills the setting. A jail that sets its own ignoreip leaves all of this out; the watch says so.
ignoreip = 127.0.0.1/8 ::1 ${machine:mesh-range} 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 169.254.0.0/16 fc00::/7 fe80::/10 file:/var/lib/mesh-fail2ban/own-addresses
# Three failures in a day ban for a day (novox/hq ADR 0179). The attackers this mesh sees pace
# themselves at one try every ten minutes, under any ten-minute window; a day's window counts
# them, and a day's ban costs a person who mistyped three times once, from one address, while
# the mesh's own range is never banned at all.
bantime = 1d
findtime = 1d
maxretry = 3
# Ban through iptables, not through a firewall front-end the machine may not have. ufw is
# installed on two of this mesh's machines and absent on the other two, and fail2ban finds out
# only at ban time: the service reports healthy, the jail counts the attempt, the ban command
# exits 127, and nothing is blocked. Proven on 2026-09-28 -- 'ufw: command not found' on a
# machine the mesh reported as protected.
#
# The action below is this module's own, already used by the recidive jail on every machine
# here, and it bans in DOCKER-USER as well as INPUT, so a container's published port is
# covered too.
banaction = iptables-allports-dualchain
banaction_allports = iptables-allports-dualchain
[sshd]
enabled = true
port = ssh
logpath = %(sshd_log)s
backend = %(sshd_backend)s
@@ -0,0 +1,3 @@
{
"own-addresses": []
}
@@ -0,0 +1,9 @@
[recidive]
enabled = true
logpath = /var/log/fail2ban.log
# Ban in both INPUT (host services like SSH) and DOCKER-USER (container services)
banaction = iptables-allports-dualchain
# Banned twice in two weeks, by any jail, is banned for four (novox/hq ADR 0179).
bantime = 4w
findtime = 2w
maxretry = 2
@@ -0,0 +1,8 @@
[sshd]
enabled = true
port = ssh
logpath = %(sshd_log)s
backend = %(sshd_backend)s
maxretry = 3
findtime = 1d
bantime = 1d
@@ -0,0 +1,195 @@
{
"module": "fail2ban",
"version": "1",
"capabilities": [
"firewall"
],
"claims": [
{
"name": "node-intrusion-prevention",
"scope": "node",
"serves": [
"status",
"banned",
"ban",
"unban"
]
}
],
"tools": [
"fail2ban_settings",
"fail2ban_health"
],
"jailing": {
"into": "/etc/fail2ban/jail.d/mesh.conf",
"filter-into": "/etc/fail2ban/filter.d"
},
"config": {
"own-addresses": {
"type": "list",
"description": "The addresses and networks outside the mesh that are the operator's own, each an IP address or a CIDR network: every jail ignores them beside the private ranges, the ban verb refuses them, and a failure a jail counted from one is said as a warning; empty is none.",
"trusted": true,
"default": []
}
},
"resources": [
{
"id": "package",
"type": "package",
"package": "fail2ban"
},
{
"id": "state-dir",
"type": "directory",
"path": "/var/lib/mesh-fail2ban",
"mode": "0755",
"unknown": "program"
},
{
"id": "own-addresses-list",
"type": "file",
"path": "/var/lib/mesh-fail2ban/own-addresses",
"mode": "0644",
"create-once": true,
"template": "files/own-addresses"
},
{
"id": "jail-d",
"type": "directory",
"path": "/etc/fail2ban/jail.d",
"mode": "0755",
"unknown": "program"
},
{
"id": "action-d",
"type": "directory",
"path": "/etc/fail2ban/action.d",
"mode": "0755",
"unknown": "program"
},
{
"id": "filter-d",
"type": "directory",
"path": "/etc/fail2ban/filter.d",
"mode": "0755",
"unknown": "program"
},
{
"id": "run-dir",
"type": "directory",
"path": "/var/run/fail2ban",
"mode": "0755",
"unknown": "program"
},
{
"id": "jail-local",
"type": "file",
"path": "/etc/fail2ban/jail.local",
"mode": "0644",
"template": "files/jail.local"
},
{
"id": "jail-sshd",
"type": "file",
"path": "/etc/fail2ban/jail.d/sshd.conf",
"mode": "0644",
"template": "files/sshd.conf"
},
{
"id": "own-addresses",
"type": "file",
"path": "/etc/fail2ban/mesh-own-addresses.json",
"mode": "0644",
"merge": "json",
"takes-config": [
"own-addresses"
],
"trusted": true,
"template": "files/mesh-own-addresses.json"
},
{
"id": "watch",
"type": "process",
"name": "fail2ban-watch",
"artifact": "tools",
"run": [
"./fail2ban-tools",
"watch"
],
"health": {
"kind": "tool",
"tool": "fail2ban_health",
"interval": "60s",
"timeout": "10s",
"looks": 2,
"grace": "90s"
}
},
{
"id": "log",
"type": "file",
"path": "/var/log/fail2ban.log",
"mode": "0640",
"create-once": true,
"template": "files/own-addresses"
},
{
"id": "jail-recidive",
"type": "file",
"path": "/etc/fail2ban/jail.d/recidive.conf",
"mode": "0644",
"template": "files/recidive.conf"
},
{
"id": "action-dualchain",
"type": "file",
"path": "/etc/fail2ban/action.d/iptables-allports-dualchain.conf",
"mode": "0644",
"template": "files/iptables-allports-dualchain.conf"
},
{
"id": "logrotate",
"type": "file",
"path": "/etc/logrotate.d/fail2ban",
"mode": "0644",
"template": "files/fail2ban"
},
{
"id": "run",
"type": "service",
"unit": "fail2ban.service",
"state": "running",
"boot": "enabled",
"restart-on": [
"jail-local",
"jail-sshd",
"jail-recidive",
"action-dualchain",
"composed-jails"
],
"health": {
"kind": "unit"
}
}
],
"build": {
"artifacts": [
{
"name": "tools",
"kind": "bundle",
"language": "go",
"system": "arch",
"from": "cmd/fail2ban-tools",
"binary": "fail2ban-tools",
"loads": [
"fail2ban-tools"
]
},
{
"name": "files",
"kind": "archive",
"from": "files"
}
]
}
}