The mechanism, mirroring Filtering: a module declares Jails (name, failregex, jail stanza) naming no node/path (ADR 0112); the intrusion-prevention holder declares Jailing (where composed jails go); the mesh gathers every assigned module's jails into one jail.d file (a fixed id the fail2ban service restarts on) plus a filter.d file per jail. A node not running a module has none of its jails. Tested. Behaviour-neutral until a service module declares a jail — the per-service content (postgres/mssql/mailu failregex+logpath) is authored next, against how each container actually logs.
63 lines
2.8 KiB
Go
63 lines
2.8 KiB
Go
package catalogue
|
|
|
|
import (
|
|
"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 {
|
|
fmt.Fprintf(&composed, "\n# from %s\n[%s]\nenabled = true\nfilter = %s\n%s\n",
|
|
d.module, 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...)
|
|
}
|