From 253334a5f57cdfb39217eff37bcad815f3fbc17a Mon Sep 17 00:00:00 2001 From: joshsiv-mulga Date: Mon, 25 May 2026 13:23:39 +1000 Subject: [PATCH] fix(ipsec): enable openvswitch-ipsec at provision time, drop daemon write Provisioning now runs `systemctl enable --now openvswitch-ipsec.service` alongside openvswitch-switch and ovn-controller. The spinifex-daemon process drops its `enable --now` shell-out and only polls is-active. Sudoers gains one read-only line: spinifex-daemon ALL=(root) NOPASSWD: /usr/bin/systemctl is-active openvswitch-ipsec.service This mirrors the existing vpcd ovn-controller is-active precedent and removes the daemon's need for systemd-write capability. Aligns with least-privilege service account pattern: provision enables services, daemon only reads status. If the unit is inactive at runtime, the daemon refuses to flip ipsec_encapsulation=true (silent-drop guard from mulga-siv-136) and surfaces a clear error pointing at scripts/setup-ovn.sh. --- scripts/setup-ovn.sh | 10 ++++++++-- scripts/setup.sh | 1 + spinifex/daemon/ipsec.go | 20 ++++++++++---------- spinifex/daemon/ipsec_test.go | 14 ++++++-------- 4 files changed, 25 insertions(+), 20 deletions(-) diff --git a/scripts/setup-ovn.sh b/scripts/setup-ovn.sh index be9bd789b..e355f9c8d 100755 --- a/scripts/setup-ovn.sh +++ b/scripts/setup-ovn.sh @@ -721,8 +721,14 @@ echo "" echo "Step 10: Enabling OVN auto-start on boot..." sudo systemctl enable openvswitch-switch 2>/dev/null || true sudo systemctl enable ovn-controller 2>/dev/null || true -echo " openvswitch-switch: enabled on boot" -echo " ovn-controller: enabled on boot" +# ovs-monitor-ipsec drives strongSwan from OVS DB cert pointers. The daemon's +# enableOVNIPSec() flips ipsec_encapsulation=true at runtime and silently drops +# tunnel traffic if this unit isn't already up — enable at provision time so +# daemon never needs systemd-write capability (only is-active read). +sudo systemctl enable --now openvswitch-ipsec.service 2>/dev/null || true +echo " openvswitch-switch: enabled on boot" +echo " ovn-controller: enabled on boot" +echo " openvswitch-ipsec: enabled on boot" # --- Step 11: Health check --- echo "" diff --git a/scripts/setup.sh b/scripts/setup.sh index 0457f7497..5e3dc0d9a 100755 --- a/scripts/setup.sh +++ b/scripts/setup.sh @@ -186,6 +186,7 @@ install_sudoers() { 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 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 ed23e8675..213654009 100644 --- a/spinifex/daemon/ipsec.go +++ b/spinifex/daemon/ipsec.go @@ -12,8 +12,10 @@ import ( ) // systemctlActiveTimeout bounds how long enableOVNIPSec waits for -// openvswitch-ipsec.service to report active after a `systemctl enable -// --now`. Overridden in tests. +// openvswitch-ipsec.service to report active. The unit is enabled at +// provision time (scripts/setup-ovn.sh) so the daemon only needs read +// capability (systemctl is-active); this poll absorbs boot-time races +// where the unit hasn't finished starting yet. Overridden in tests. var systemctlActiveTimeout = 5 * time.Second // enableOVNIPSec wires the per-node IPsec peer cert into the local OVS DB and @@ -79,14 +81,12 @@ func (d *Daemon) enableOVNIPSec() error { return nil } -// ensureOVSMonitorIPSecActive enables + starts openvswitch-ipsec.service and -// waits for it to report active. Without this guarantee, ipsec_encapsulation -// silently drops all tunnel traffic because no userspace daemon is reading -// the OVS DB cert pointers to drive strongSwan IKE. +// ensureOVSMonitorIPSecActive polls openvswitch-ipsec.service for "active". +// The unit is enabled at provision time (scripts/setup-ovn.sh); the daemon +// has read-only sudoers scope (systemctl is-active). If the unit is inactive +// here, operator intervention is required — daemon refuses to flip +// ipsec_encapsulation=true and silently drop tunnel traffic. func ensureOVSMonitorIPSecActive() error { - if out, err := sudoCommand("systemctl", "enable", "--now", "openvswitch-ipsec.service").CombinedOutput(); err != nil { - return fmt.Errorf("enable --now: %s: %w", strings.TrimSpace(string(out)), err) - } deadline := time.Now().Add(systemctlActiveTimeout) var lastOut string for time.Now().Before(deadline) { @@ -97,5 +97,5 @@ func ensureOVSMonitorIPSecActive() error { } time.Sleep(250 * time.Millisecond) } - return fmt.Errorf("not active after %s: %s", systemctlActiveTimeout, lastOut) + return fmt.Errorf("not active after %s: %s (provision via scripts/setup-ovn.sh)", systemctlActiveTimeout, lastOut) } diff --git a/spinifex/daemon/ipsec_test.go b/spinifex/daemon/ipsec_test.go index f5dc5c1e6..4f89c31a3 100644 --- a/spinifex/daemon/ipsec_test.go +++ b/spinifex/daemon/ipsec_test.go @@ -65,23 +65,21 @@ func TestEnableOVNIPSec(t *testing.T) { require.NoError(t, d.enableOVNIPSec()) // Expected sudo invocations in order: - // systemctl enable --now openvswitch-ipsec.service - // systemctl is-active openvswitch-ipsec.service + // systemctl is-active openvswitch-ipsec.service (unit enabled at provision time) // ovs-vsctl set Open_vSwitch . other_config:certificate=... private_key=... ca_cert=... // ovs-vsctl set Open_vSwitch . other_config:ipsec_encapsulation=true - require.Len(t, recorder.runs, 4) - assert.Equal(t, []string{"systemctl", "enable", "--now", "openvswitch-ipsec.service"}, recorder.runs[0]) - assert.Equal(t, []string{"systemctl", "is-active", "openvswitch-ipsec.service"}, recorder.runs[1]) - for _, run := range recorder.runs[2:] { + require.Len(t, recorder.runs, 3) + assert.Equal(t, []string{"systemctl", "is-active", "openvswitch-ipsec.service"}, recorder.runs[0]) + for _, run := range recorder.runs[1:] { assert.Equal(t, "ovs-vsctl", run[0]) assert.Equal(t, "set", run[1]) assert.Equal(t, "Open_vSwitch", run[2]) } - joined := strings.Join(recorder.runs[2], " ") + joined := strings.Join(recorder.runs[1], " ") assert.Contains(t, joined, "other_config:certificate="+filepath.Join(configDir, "ipsec", "peer.pem")) assert.Contains(t, joined, "other_config:private_key="+filepath.Join(configDir, "ipsec", "peer.key")) assert.Contains(t, joined, "other_config:ca_cert="+filepath.Join(configDir, "ca.pem")) - assert.Contains(t, strings.Join(recorder.runs[3], " "), "other_config:ipsec_encapsulation=true") + assert.Contains(t, strings.Join(recorder.runs[2], " "), "other_config:ipsec_encapsulation=true") } func TestEnableOVNIPSec_SingleNodeSkip(t *testing.T) {