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.
This commit is contained in:
joshsiv-mulga
2026-05-25 13:23:39 +10:00
parent f47844619d
commit 253334a5f5
4 changed files with 25 additions and 20 deletions
+8 -2
View File
@@ -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 ""
+1
View File
@@ -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
+10 -10
View File
@@ -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)
}
+6 -8
View File
@@ -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) {