From 454defb0050da07b6e75c31a910574e3c1f07fe5 Mon Sep 17 00:00:00 2001 From: joshsiv-mulga Date: Tue, 6 Oct 2026 15:39:43 +1000 Subject: [PATCH] refactor: move IPsec certificate handling to network host --- cmd/spinifex/cmd/admin.go | 4 +- scripts/setup-ovn.sh | 2 +- .../ipsec.go => network/host/ipsec_cert.go} | 8 +++- .../host/ipsec_cert_test.go} | 38 +++++++++++++++++-- spinifex/network/host/ipsec_enable.go | 3 +- 5 files changed, 44 insertions(+), 11 deletions(-) rename spinifex/{admin/ipsec.go => network/host/ipsec_cert.go} (96%) rename spinifex/{admin/ipsec_test.go => network/host/ipsec_cert_test.go} (83%) diff --git a/cmd/spinifex/cmd/admin.go b/cmd/spinifex/cmd/admin.go index 2c41539b3..a100e3a28 100644 --- a/cmd/spinifex/cmd/admin.go +++ b/cmd/spinifex/cmd/admin.go @@ -1668,7 +1668,7 @@ func runAdminInit(cmd *cobra.Command, args []string) { if ipsecEnabled { caCertPath := filepath.Join(configDir, "ca.pem") caKeyPath := filepath.Join(configDir, "ca.key") - if err := admin.GenerateIPSecPeerCert(configDir, caCertPath, caKeyPath, node, bindIP); err != nil { + if err := host.GenerateIPSecPeerCert(configDir, caCertPath, caKeyPath, node, bindIP); err != nil { fmt.Fprintf(os.Stderr, "Error generating IPsec peer certificate: %v\n", err) os.Exit(1) } @@ -2655,7 +2655,7 @@ func runAdminJoin(cmd *cobra.Command, args []string) { if ipsecEnabled { caCertPath := filepath.Join(configDir, "ca.pem") caKeyPath := filepath.Join(configDir, "ca.key") - if err := admin.GenerateIPSecPeerCert(configDir, caCertPath, caKeyPath, node, bindIP); err != nil { + if err := host.GenerateIPSecPeerCert(configDir, caCertPath, caKeyPath, node, bindIP); err != nil { fmt.Fprintf(os.Stderr, "Error generating IPsec peer certificate: %v\n", err) os.Exit(1) } diff --git a/scripts/setup-ovn.sh b/scripts/setup-ovn.sh index 9b33f522d..3bd0735c2 100755 --- a/scripts/setup-ovn.sh +++ b/scripts/setup-ovn.sh @@ -1297,7 +1297,7 @@ fi # explicitly given. Reasons to pin: # 1. IPsec identity: ovs-monitor-ipsec uses chassis-id as the IKEv2 # `@` peer identity. Our per-node IPsec peer cert -# (admin.GenerateIPSecPeerCert) carries the cluster node name as CN +# (host.GenerateIPSecPeerCert) carries the cluster node name as CN # + dnsName SAN — see spx admin init/join, which take --node NAME and # bake NAME into the cert. Leaving chassis-id as the package-generated # UUID would cause `received AUTHENTICATION_FAILED` because charon diff --git a/spinifex/admin/ipsec.go b/spinifex/network/host/ipsec_cert.go similarity index 96% rename from spinifex/admin/ipsec.go rename to spinifex/network/host/ipsec_cert.go index dd8bf513c..752b3f5af 100644 --- a/spinifex/admin/ipsec.go +++ b/spinifex/network/host/ipsec_cert.go @@ -1,4 +1,4 @@ -package admin +package host import ( "crypto/rand" @@ -29,6 +29,10 @@ const ( ipsecCACertSymlink = "ca.pem" ) +// ipsecCertKeyBits is the RSA key size for IPsec peer certificates. It is a +// seam so tests can lower it for faster key generation. +var ipsecCertKeyBits = 4096 + // id-kp-ipsecIKE per RFC 4945 §5.1.3.12. strongSwan accepts certs carrying // this EKU as IKEv2 peer credentials; ovs-monitor-ipsec's auto-generated // strongSwan config consumes them as-is. @@ -61,7 +65,7 @@ func GenerateIPSecPeerCert(configDir, caCertPath, caKeyPath, hostname, nodeIP st return err } - peerKey, err := rsa.GenerateKey(rand.Reader, certKeyBits) + peerKey, err := rsa.GenerateKey(rand.Reader, ipsecCertKeyBits) if err != nil { return fmt.Errorf("ipsec peer cert: generate key: %w", err) } diff --git a/spinifex/admin/ipsec_test.go b/spinifex/network/host/ipsec_cert_test.go similarity index 83% rename from spinifex/admin/ipsec_test.go rename to spinifex/network/host/ipsec_cert_test.go index d0fbbd6f8..e80689089 100644 --- a/spinifex/admin/ipsec_test.go +++ b/spinifex/network/host/ipsec_cert_test.go @@ -1,19 +1,49 @@ -package admin +package host import ( + "crypto/rand" + "crypto/rsa" "crypto/x509" + "crypto/x509/pkix" "encoding/pem" + "math/big" "net" "os" "path/filepath" "testing" + "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -// TestGenerateIPSecPeerCert shares a CA across subtests so the 4096-bit -// CA-key generation runs once (~0.7s). +func init() { ipsecCertKeyBits = 2048 } + +// writeTestCA writes a throwaway self-signed RSA CA in the PEM/PKCS8 form the +// IPsec issuer loads. +func writeTestCA(t *testing.T, certPath, keyPath string) { + t.Helper() + key, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err) + tmpl := &x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{CommonName: "test CA"}, + NotBefore: time.Now().Add(-time.Hour), + NotAfter: time.Now().Add(24 * time.Hour), + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature, + BasicConstraintsValid: true, + IsCA: true, + } + der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &key.PublicKey, key) + require.NoError(t, err) + keyDER, err := x509.MarshalPKCS8PrivateKey(key) + require.NoError(t, err) + require.NoError(t, os.WriteFile(certPath, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der}), 0o644)) + require.NoError(t, os.WriteFile(keyPath, pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: keyDER}), 0o600)) +} + +// TestGenerateIPSecPeerCert shares a CA across subtests so the CA-key +// generation runs once. // //nolint:tparallel // the parent cannot be parallel: it overrides the charonCATrustDir/charonRereadCAs package vars for the duration of the test func TestGenerateIPSecPeerCert(t *testing.T) { @@ -33,7 +63,7 @@ func TestGenerateIPSecPeerCert(t *testing.T) { caDir := t.TempDir() caCertPath := filepath.Join(caDir, "ca.pem") caKeyPath := filepath.Join(caDir, "ca.key") - require.NoError(t, GenerateCACert(caCertPath, caKeyPath)) + writeTestCA(t, caCertPath, caKeyPath) t.Run("happy path", func(t *testing.T) { t.Parallel() diff --git a/spinifex/network/host/ipsec_enable.go b/spinifex/network/host/ipsec_enable.go index 906aaeafa..137b8abca 100644 --- a/spinifex/network/host/ipsec_enable.go +++ b/spinifex/network/host/ipsec_enable.go @@ -12,7 +12,6 @@ import ( "strings" "time" - "github.com/mulgadc/spinifex/spinifex/admin" "github.com/mulgadc/spinifex/spinifex/bootstrap/config" "github.com/mulgadc/spinifex/spinifex/foundation/telemetry" hostcommand "github.com/mulgadc/spinifex/spinifex/runtime/host/command" @@ -274,7 +273,7 @@ func EnableOVNIPSec(ctx context.Context, configPath string, clusterConfig *confi // turn on encapsulation, once ovs-monitor-ipsec is up to act on both. func configureLocalIPSec(configPath string) error { configDir := filepath.Dir(configPath) - certPath, keyPath := admin.IPSecCertPaths(configDir) + certPath, keyPath := IPSecCertPaths(configDir) caCertPath := filepath.Join(configDir, "ca.pem") for _, p := range []string{certPath, keyPath, caCertPath} {