diff --git a/internal/apply/apply.go b/internal/apply/apply.go index f7e5d62..8bd30a2 100644 --- a/internal/apply/apply.go +++ b/internal/apply/apply.go @@ -318,7 +318,7 @@ func ApplyKeeping( // A converged node whose found firewall was in force retires it only now, once everything — // the mesh's derived filter among it — applied cleanly (novox/hq ADR 0100). if len(failures) == 0 { - if err := retireFirewall(ctx, d, &known, run, log); err != nil { + if err := retireFirewall(ctx, d, origin, &known, run, log); err != nil { return report, known, &Error{Resource: "the firewall found on this machine", Err: err, Done: report} } } diff --git a/internal/apply/opening.go b/internal/apply/opening.go index 3adef33..ff6ee1c 100644 --- a/internal/apply/opening.go +++ b/internal/apply/opening.go @@ -57,10 +57,14 @@ func foundFirewall(ctx context.Context, d *declaration.Declaration, known *store // which is when the mesh's derived filter has taken its place. Disabled, never flushed: its // configuration stays on disk for a return to adopted, and the container runtime's rules are not // its to take. -func retireFirewall(ctx context.Context, d *declaration.Declaration, known *store.State, run Runner, - log func(string)) error { +// +// Only a declaration from the mesh converges a node. A carried bundle never says a node is adopted +// — it cannot — so its silence is not the controller's word that the node was converged, and an +// adopted node re-applying its bundle keeps the firewall it was found with. +func retireFirewall(ctx context.Context, d *declaration.Declaration, origin string, known *store.State, + run Runner, log func(string)) error { rec := known.Firewall - if d.Adoption != nil || rec == nil || rec.Kind != string(firewall.UFW) || !rec.WasActive || + if origin != store.OriginDeclared || d.Adoption != nil || rec == nil || rec.Kind != string(firewall.UFW) || !rec.WasActive || rec.DisabledByMesh { return nil } diff --git a/internal/apply/opening_test.go b/internal/apply/opening_test.go index d51bda5..7192925 100644 --- a/internal/apply/opening_test.go +++ b/internal/apply/opening_test.go @@ -202,3 +202,25 @@ func TestAnOpeningOnAConvergedNodeIsRefused(t *testing.T) { t.Error("an opening was accepted on a node the declaration does not say is adopted") } } + +func TestACarriedApplyOnAnAdoptedNodeLeavesItsFirewallInForce(t *testing.T) { + // The bundle, re-applied by the installer or the one-shot CLI, never says a node is adopted. + // That is not the controller converging it, so ufw must stay enabled (novox/hq ADR 0100). + dir := t.TempDir() + u := &ufwMachine{installed: true, active: true, rules: []string{"allow 22/tcp"}} + _, state, err := applyWith(t, adopted(t, `{"taken":[]}`, busOpening+","+withConf(dir)), store.State{}, u.run) + if err != nil { + t.Fatal(err) + } + u.asked = nil + carried := parse(t, `{"declaration":1,"resources":[`+withConf(filepath.Join(dir, "bundle"))+`]}`) + _, state, err = ApplyKeeping(context.Background(), archHost(t), carried, state, store.OriginCarried, + u.run, nil, nil, nil) + if err != nil { + t.Fatal(err) + } + if !u.active || state.Firewall.DisabledByMesh || u.index("ufw disable") >= 0 { + t.Fatalf("a carried apply retired the found firewall: active %v, record %+v, asked %v", + u.active, state.Firewall, u.asked) + } +}