mirror of
https://github.com/mulgadc/spinifex.git
synced 2026-10-20 11:21:49 +00:00
fix(daemon): flip NB_Global.ipsec=true on management node
Setting Open_vSwitch.other_config:ipsec_encapsulation=true on each chassis is necessary but not sufficient for OVN native IPsec — without NB_Global.ipsec=true, ovn-controller never adds options:remote_name to the Geneve tunnel ports, so ovs-monitor-ipsec sees no IPsec-enabled tunnels and the kernel xfrm tables stay empty. After enabling ipsec_encapsulation locally, also write NB_Global.ipsec=true when the local ovnnb_db socket is present (management node only); workers skip silently — one writer is enough for a cluster-wide flag. Adds a scoped sudoers entry pinning the daemon to that exact ovn-nbctl invocation so the grant can't be reused for other NB mutations.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user