diff --git a/internal/catalogue/filtering.go b/internal/catalogue/filtering.go index 275a204..e9dbef7 100644 --- a/internal/catalogue/filtering.go +++ b/internal/catalogue/filtering.go @@ -261,8 +261,21 @@ func AsNftables(rules []Rule, mesh []string, outward bool) string { strings.Join(six, ", "), SSHPort)) } } - if outward { - b.WriteString("\t\t# and from outside, because this machine faces it\n") + // From everywhere when this machine faces outward — and also when the mesh has no addresses to + // name yet. + // + // **A machine early in being adopted is the one this matters most for, and the one where the + // rule above emits nothing.** Before the private network exists there is no mesh address to + // allow from, so restricting to it restricts to nothing: the chain drops by default and ssh is + // simply shut. That is the first machine anybody adopts, reached over the network, with the + // port needed to fix it closed by the act of adopting it. Never leaving this chain without an + // ssh rule is worth more than the narrower rule it would have had. + if four, six := byFamily(mesh); outward || (len(four) == 0 && len(six) == 0) { + why := "and from outside, because this machine faces it" + if !outward { + why = "and from anywhere, because this mesh has no addresses to narrow it to yet" + } + b.WriteString("\t\t# " + why + "\n") b.WriteString(fmt.Sprintf("\t\ttcp dport %d accept\n", SSHPort)) } diff --git a/internal/catalogue/filtering_test.go b/internal/catalogue/filtering_test.go index 80f9d80..ddfd9fb 100644 --- a/internal/catalogue/filtering_test.go +++ b/internal/catalogue/filtering_test.go @@ -690,3 +690,15 @@ func TestSSHIsOpenFromOutsideOnAMachineThatFacesIt(t *testing.T) { t.Fatalf("a machine reachable from outside does not answer ssh there:\n%s", nft) } } + +// A machine the mesh knows no addresses for still answers ssh. +// +// **This is the machine it matters most for.** Before the private network exists there is nothing +// to narrow the rule to, so narrowing it shuts the port entirely — on the first machine anybody +// adopts, reached over the network, closed by the act of adopting it. +func TestSSHIsNeverLeftWithoutARule(t *testing.T) { + nft := AsNftables(nil, nil, false) + if !strings.Contains(nft, "tcp dport 22 accept") { + t.Fatalf("a machine with no mesh addresses has no ssh rule, so adopting it locks it:\n%s", nft) + } +} diff --git a/internal/catalogue/print_rehearsal_test.go b/internal/catalogue/print_rehearsal_test.go new file mode 100644 index 0000000..71e3dbb --- /dev/null +++ b/internal/catalogue/print_rehearsal_test.go @@ -0,0 +1,16 @@ +package catalogue + +import ( + "os" + "testing" +) + +func TestPrintRehearsalRuleset(t *testing.T) { + if os.Getenv("PRINT_RULESET") == "" { + t.Skip("set PRINT_RULESET") + } + rules := mustFilter(t, Resolution{Modules: []Manifest{ + {Module: "pub", Listens: []Listening{{Port: 8099, From: FromMesh, Why: "the thing it serves"}}}, + }}, nil) + t.Log("\n" + AsNftables(rules, []string{"192.0.2.20"}, true)) +}