fail2ban restarts when the composed jail file changes, and each filter is a file of its own, so a module that changed only its failregex left the running jail on the old pattern. The jail file now names each filter's digest.
73 lines
3.5 KiB
Go
73 lines
3.5 KiB
Go
package catalogue
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
// A node's fail2ban jails, composed from the modules it runs (novox/hq to-be 31).
|
|
//
|
|
// **The same shape as the firewall.** Every module's `listens` become the node's rule set; every
|
|
// module's `jails` become the node's fail2ban config. A module that runs an authenticating service
|
|
// declares what a break-in on it looks like and how to ban it, naming no node and no path (ADR
|
|
// 0112); the intrusion-prevention holder — the one module with `jailing` — gathers them and writes
|
|
// them where it owns. A node not running a module has none of its jails.
|
|
|
|
// jailsInto composes every jail declared by the modules on a node into the files the holder writes:
|
|
// one jail file (all stanzas, so the fail2ban service restarts on a single resource) and one filter
|
|
// file per jail (its failregex, which fail2ban references by the jail's name).
|
|
//
|
|
// Owned by the holder, because the directory is: two modules writing into one fail2ban is the
|
|
// collision the holder model exists to prevent. Empty when nothing declares a jail — then the file
|
|
// is written empty rather than absent, so removing the last jail is an ordinary change the service
|
|
// restarts on rather than a file that vanishes.
|
|
func jailsInto(modules []Manifest, j *Jailing) []map[string]any {
|
|
type declared struct {
|
|
module string
|
|
jail Jail
|
|
}
|
|
var jails []declared
|
|
for _, m := range modules {
|
|
for _, jail := range m.Jails {
|
|
jails = append(jails, declared{m.Module, jail})
|
|
}
|
|
}
|
|
// A stable order the host applies as given (ADR 0005), and so the same set composes byte for
|
|
// byte every time rather than differing by map iteration.
|
|
sort.Slice(jails, func(a, b int) bool { return jails[a].jail.Name < jails[b].jail.Name })
|
|
|
|
var composed strings.Builder
|
|
composed.WriteString("# The mesh's jails, composed from the modules this node runs. Do not edit —\n")
|
|
composed.WriteString("# replaced whenever the node's modules change (novox/hq to-be 31).\n")
|
|
|
|
out := make([]map[string]any, 0, len(jails)+1)
|
|
for _, d := range jails {
|
|
// **The filter's digest rides in the jail file.** fail2ban is restarted when this file
|
|
// changes, and the filter is a file of its own: a module that changed only what a failure
|
|
// looks like rewrote the filter on disk and left the running jail on the old pattern, with
|
|
// nothing said (novox/hq issue 191's rollout found it on gitea's sshd). Naming the filter's
|
|
// digest here makes a changed pattern a changed jail file, so the restart the service
|
|
// already takes on it covers the filter too.
|
|
sum := sha256.Sum256([]byte(d.jail.Failregex))
|
|
fmt.Fprintf(&composed, "\n# from %s, filter %s\n[%s]\nenabled = true\nfilter = %s\n%s\n",
|
|
d.module, hex.EncodeToString(sum[:])[:12], d.jail.Name, d.jail.Name,
|
|
strings.TrimRight(d.jail.Jail, "\n"))
|
|
// The filter is a file of its own, named as the jail's filter= references it.
|
|
out = append(out, map[string]any{
|
|
"id": "filter-" + d.jail.Name,
|
|
"type": "file", "path": strings.TrimRight(j.FilterInto, "/") + "/" + d.jail.Name + ".conf",
|
|
"mode": "0644",
|
|
"content": "# Generated by the mesh (from module " + d.module + "). Do not edit.\n" +
|
|
"[Definition]\nfailregex = " + d.jail.Failregex + "\n",
|
|
})
|
|
}
|
|
// The one jail file, first, with the fixed id the fail2ban service names in its restart-on.
|
|
return append([]map[string]any{{
|
|
"id": ComposedJailsID(), "type": "file", "path": j.Into, "mode": "0644",
|
|
"content": composed.String(),
|
|
}}, out...)
|
|
}
|