diff --git a/scripts/setup.sh b/scripts/setup.sh index 7220ed1c6..612ba007b 100755 --- a/scripts/setup.sh +++ b/scripts/setup.sh @@ -187,6 +187,7 @@ spinifex-daemon ALL=(root) NOPASSWD: /sbin/ip, /usr/sbin/ip spinifex-daemon ALL=(root) NOPASSWD: /usr/bin/ovs-vsctl, /usr/bin/ovs-appctl spinifex-daemon ALL=(root) NOPASSWD: /usr/sbin/dhcpcd spinifex-daemon ALL=(root) NOPASSWD: /usr/bin/systemctl is-active openvswitch-ipsec.service +spinifex-daemon ALL=(root) NOPASSWD: /usr/bin/ovn-nbctl set NB_Global . ipsec=true # Spinifex VPC daemon: OVN and OVS read/write, OVN controller status check and DHCP spinifex-vpcd ALL=(root) NOPASSWD: /usr/sbin/dhcpcd diff --git a/spinifex/daemon/ipsec.go b/spinifex/daemon/ipsec.go index 213654009..5d3963526 100644 --- a/spinifex/daemon/ipsec.go +++ b/spinifex/daemon/ipsec.go @@ -18,6 +18,11 @@ import ( // where the unit hasn't finished starting yet. Overridden in tests. var systemctlActiveTimeout = 5 * time.Second +// ovnNBSocketPath gates the NB_Global ipsec=true write to the management +// node — workers don't run ovn-central and have no local socket. +// Overridden in tests. +var ovnNBSocketPath = "/run/ovn/ovnnb_db.sock" + // enableOVNIPSec wires the per-node IPsec peer cert into the local OVS DB and // flips ipsec_encapsulation=true. ovs-monitor-ipsec (shipped by the // openvswitch-ipsec package) reads the cert/key/CA pointers and materialises @@ -73,6 +78,19 @@ func (d *Daemon) enableOVNIPSec() error { return fmt.Errorf("enable ipsec_encapsulation: %s: %w", strings.TrimSpace(string(out)), err) } + // NB_Global.ipsec=true triggers ovn-controller on every chassis to add + // options:remote_name to the Geneve tunnel ports, which is what + // ovs-monitor-ipsec keys off to materialise the per-peer strongSwan + // connections. Without it, xfrm stays empty and Geneve runs plaintext + // even with ipsec_encapsulation=true set locally. Only the management + // node has a local NB socket; on workers, skip silently — the flag is + // cluster-wide and one writer is enough. + if _, err := os.Stat(ovnNBSocketPath); err == nil { + if out, err := sudoCommand("ovn-nbctl", "set", "NB_Global", ".", "ipsec=true").CombinedOutput(); err != nil { + return fmt.Errorf("set NB_Global ipsec=true: %s: %w", strings.TrimSpace(string(out)), err) + } + } + slog.Info("OVN native IPsec enabled on intra-AZ Geneve", "cert", certPath, "key", keyPath, diff --git a/spinifex/daemon/ipsec_test.go b/spinifex/daemon/ipsec_test.go index 4f89c31a3..79390302b 100644 --- a/spinifex/daemon/ipsec_test.go +++ b/spinifex/daemon/ipsec_test.go @@ -61,6 +61,13 @@ func TestEnableOVNIPSec(t *testing.T) { require.NoError(t, os.WriteFile(full, []byte("x"), 0600)) } + // Point the NB-socket gate at a non-existent path so this test runs the + // worker path (no ovn-nbctl). TestEnableOVNIPSec_Management covers the + // management-node branch. + origNBSock := ovnNBSocketPath + ovnNBSocketPath = filepath.Join(configDir, "no-such-socket") + t.Cleanup(func() { ovnNBSocketPath = origNBSock }) + d := &Daemon{configPath: configPath, clusterConfig: multiNodeClusterConfig()} require.NoError(t, d.enableOVNIPSec()) @@ -82,6 +89,36 @@ func TestEnableOVNIPSec(t *testing.T) { assert.Contains(t, strings.Join(recorder.runs[2], " "), "other_config:ipsec_encapsulation=true") } +func TestEnableOVNIPSec_Management(t *testing.T) { + recorder := &recordingSudo{} + orig := sudoCommand + sudoCommand = recorder.stub + t.Cleanup(func() { sudoCommand = orig }) + + configDir := t.TempDir() + configPath := filepath.Join(configDir, "spinifex.toml") + require.NoError(t, os.WriteFile(configPath, []byte("placeholder"), 0600)) + for _, rel := range []string{"ca.pem", "ipsec/peer.pem", "ipsec/peer.key"} { + full := filepath.Join(configDir, rel) + require.NoError(t, os.MkdirAll(filepath.Dir(full), 0750)) + require.NoError(t, os.WriteFile(full, []byte("x"), 0600)) + } + + // Simulate a local NB socket so enableOVNIPSec runs the management + // branch and writes NB_Global.ipsec=true. + sockPath := filepath.Join(configDir, "ovnnb_db.sock") + require.NoError(t, os.WriteFile(sockPath, []byte{}, 0600)) + origNBSock := ovnNBSocketPath + ovnNBSocketPath = sockPath + t.Cleanup(func() { ovnNBSocketPath = origNBSock }) + + d := &Daemon{configPath: configPath, clusterConfig: multiNodeClusterConfig()} + require.NoError(t, d.enableOVNIPSec()) + + require.Len(t, recorder.runs, 4) + assert.Equal(t, []string{"ovn-nbctl", "set", "NB_Global", ".", "ipsec=true"}, recorder.runs[3]) +} + func TestEnableOVNIPSec_SingleNodeSkip(t *testing.T) { orig := sudoCommand sudoCommand = func(name string, args ...string) *exec.Cmd {