mirror of
https://github.com/mulgadc/spinifex.git
synced 2026-10-15 09:01:31 +00:00
feat(admin): wire IPsec peer cert into admin init + join paths
Add --ipsec bool flag (default true) on adminInitCmd. Single-node init and the multi-node leader path both emit an IPsec peer cert after the existing CA+server-cert generation. The multi-node formation server propagates the IPSecEnabled flag to joining nodes via NetworkConfig; admin join honours it and emits its own peer cert during the same cert-issuance step that already produces the server cert. spinifex.toml template gains an unconditional [network] block carrying ipsec_enabled so the value is visible to operators on every install regardless of whether external networking is configured. The daemon reads it via cluster config (NetworkConfig.IPSecEnabled). Bead mulga-siv-125.3.5.
This commit is contained in:
+51
-19
@@ -300,6 +300,7 @@ func init() {
|
||||
adminInitCmd.Flags().Int("external-prefix-len", 24, "External pool subnet prefix length (auto-detected)")
|
||||
adminInitCmd.Flags().Bool("no-external", false, "Disable external networking (overlay-only, no internet for VMs)")
|
||||
adminInitCmd.Flags().Bool("gpu-passthrough", false, "Enable VFIO GPU passthrough (sets gpu_passthrough = true in daemon config)")
|
||||
adminInitCmd.Flags().Bool("ipsec", true, "Encrypt intra-AZ Geneve via OVN native IPsec (cluster-wide); disable only for trusted single-rack lab")
|
||||
|
||||
// Flags for admin join
|
||||
adminJoinCmd.Flags().String("region", "ap-southeast-2", "Region for this node")
|
||||
@@ -844,6 +845,7 @@ func runAdminInit(cmd *cobra.Command, args []string) {
|
||||
gatewayIP, _ := cmd.Flags().GetString("gateway-ip")
|
||||
noExternal, _ := cmd.Flags().GetBool("no-external")
|
||||
gpuPassthrough, _ := cmd.Flags().GetBool("gpu-passthrough")
|
||||
ipsecEnabled, _ := cmd.Flags().GetBool("ipsec")
|
||||
|
||||
// Fire telemetry in background (completes during init work, waited at end)
|
||||
noTelemetry, _ := cmd.Flags().GetBool("no-telemetry")
|
||||
@@ -1085,6 +1087,16 @@ func runAdminInit(cmd *cobra.Command, args []string) {
|
||||
// Generate SSL certificates (with bind IP in SANs for multi-node support)
|
||||
certPath := admin.GenerateCertificatesIfNeeded(configDir, force, bindIP)
|
||||
|
||||
// Generate per-node IPsec peer cert when cluster-wide IPsec is enabled
|
||||
// (default true). Reuses the cluster CA — no intermediate strongSwan PKI.
|
||||
if err := admin.GenerateIPSecPeerCertIfEnabled(ipsecEnabled, configDir, node, bindIP); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error generating IPsec peer certificate: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if ipsecEnabled {
|
||||
fmt.Println("🔐 IPsec peer certificate generated (intra-AZ Geneve encryption ON)")
|
||||
}
|
||||
|
||||
// Install CA certificate into system trust store
|
||||
installCACertificate(filepath.Join(configDir, "ca.pem"))
|
||||
|
||||
@@ -1106,29 +1118,31 @@ func runAdminInit(cmd *cobra.Command, args []string) {
|
||||
isMultiNode := nodes >= 2
|
||||
|
||||
if isMultiNode {
|
||||
// Build cluster-wide network config for propagation to joining nodes
|
||||
var networkConfig *formation.NetworkConfig
|
||||
// Build cluster-wide network config for propagation to joining nodes.
|
||||
// Always emit so the IPSecEnabled flag reaches joiners even when
|
||||
// external networking is disabled.
|
||||
networkConfig := &formation.NetworkConfig{
|
||||
IPSecEnabled: ipsecEnabled,
|
||||
}
|
||||
if externalMode != "" {
|
||||
bootstrapVpcId := utils.GenerateResourceID("vpc")
|
||||
bootstrapSubnetId := utils.GenerateResourceID("subnet")
|
||||
bootstrapIgwId := utils.GenerateResourceID("igw")
|
||||
networkConfig = &formation.NetworkConfig{
|
||||
ExternalMode: externalMode,
|
||||
PoolName: "wan",
|
||||
PoolSource: externalSource,
|
||||
PoolStart: poolStart,
|
||||
PoolEnd: poolEnd,
|
||||
PoolGateway: externalGateway,
|
||||
PoolGatewayIP: gatewayIP,
|
||||
PoolPrefixLen: externalPrefixLen,
|
||||
PoolDNSServers: dnsServers,
|
||||
BootstrapAccountId: admin.DefaultAccountID(),
|
||||
BootstrapVpcId: bootstrapVpcId,
|
||||
BootstrapSubnetId: bootstrapSubnetId,
|
||||
BootstrapIgwId: bootstrapIgwId,
|
||||
BootstrapCidr: handlers_ec2_vpc.DefaultVPCCidr,
|
||||
BootstrapSubnetCidr: handlers_ec2_vpc.DefaultSubnetCidr,
|
||||
}
|
||||
networkConfig.ExternalMode = externalMode
|
||||
networkConfig.PoolName = "wan"
|
||||
networkConfig.PoolSource = externalSource
|
||||
networkConfig.PoolStart = poolStart
|
||||
networkConfig.PoolEnd = poolEnd
|
||||
networkConfig.PoolGateway = externalGateway
|
||||
networkConfig.PoolGatewayIP = gatewayIP
|
||||
networkConfig.PoolPrefixLen = externalPrefixLen
|
||||
networkConfig.PoolDNSServers = dnsServers
|
||||
networkConfig.BootstrapAccountId = admin.DefaultAccountID()
|
||||
networkConfig.BootstrapVpcId = bootstrapVpcId
|
||||
networkConfig.BootstrapSubnetId = bootstrapSubnetId
|
||||
networkConfig.BootstrapIgwId = bootstrapIgwId
|
||||
networkConfig.BootstrapCidr = handlers_ec2_vpc.DefaultVPCCidr
|
||||
networkConfig.BootstrapSubnetCidr = handlers_ec2_vpc.DefaultSubnetCidr
|
||||
}
|
||||
|
||||
runAdminInitMultiNode(cmd, accessKey, secretKey, accountID, natsToken, clusterName,
|
||||
@@ -1246,6 +1260,7 @@ func runAdminInit(cmd *cobra.Command, args []string) {
|
||||
BootstrapSubnetCidr: handlers_ec2_vpc.DefaultSubnetCidr,
|
||||
|
||||
GPUPassthrough: gpuPassthrough,
|
||||
IPSecEnabled: ipsecEnabled,
|
||||
}
|
||||
|
||||
// Print external networking summary
|
||||
@@ -1837,6 +1852,22 @@ func runAdminJoin(cmd *cobra.Command, args []string) {
|
||||
}
|
||||
fmt.Printf("✅ Server certificate generated with bind IP: %s\n\n", bindIP)
|
||||
|
||||
// Match the leader's intra-AZ IPsec posture: NetworkConfig.IPSecEnabled is
|
||||
// authoritative. When the formation response omits NetworkConfig entirely
|
||||
// (no current code path; defensive guard against future regressions) fall
|
||||
// back to the AWS-parity default of ON.
|
||||
ipsecEnabled := true
|
||||
if statusResp.NetworkConfig != nil {
|
||||
ipsecEnabled = statusResp.NetworkConfig.IPSecEnabled
|
||||
}
|
||||
if err := admin.GenerateIPSecPeerCertIfEnabled(ipsecEnabled, configDir, node, bindIP); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error generating IPsec peer certificate: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if ipsecEnabled {
|
||||
fmt.Println("🔐 IPsec peer certificate generated (intra-AZ Geneve encryption ON)")
|
||||
}
|
||||
|
||||
// Build cluster topology from formation data
|
||||
clusterRoutes := formation.BuildClusterRoutes(statusResp.Nodes)
|
||||
predastoreNodes := formation.BuildPredastoreNodes(statusResp.Nodes)
|
||||
@@ -2246,6 +2277,7 @@ func finalizeNodeSetup(dataDir, certPath, adminAccessKey, adminSecretKey, region
|
||||
// applyNetworkConfig copies cluster-wide network settings from a formation
|
||||
// NetworkConfig into ConfigSettings and auto-detects the local WAN interface.
|
||||
func applyNetworkConfig(settings *admin.ConfigSettings, nc *formation.NetworkConfig) {
|
||||
settings.IPSecEnabled = nc.IPSecEnabled
|
||||
settings.ExternalMode = nc.ExternalMode
|
||||
settings.PoolName = nc.PoolName
|
||||
settings.PoolSource = nc.PoolSource
|
||||
|
||||
@@ -7,9 +7,10 @@ node = "{{.Node}}"
|
||||
[operator]
|
||||
email = "{{.OperatorEmail}}"
|
||||
{{- end}}
|
||||
{{- if .ExternalMode}}
|
||||
|
||||
[network]
|
||||
ipsec_enabled = {{.IPSecEnabled}}
|
||||
{{- if .ExternalMode}}
|
||||
external_mode = "{{.ExternalMode}}"
|
||||
|
||||
[[network.external_pools]]
|
||||
|
||||
@@ -102,6 +102,11 @@ type ConfigSettings struct {
|
||||
// GPUPassthrough enables VFIO GPU passthrough in the daemon config.
|
||||
// Sets gpu_passthrough = true under [nodes.<node>.daemon].
|
||||
GPUPassthrough bool
|
||||
|
||||
// IPSecEnabled toggles cluster-wide OVN native IPsec on intra-AZ Geneve
|
||||
// (default true). Surfaced under [network] in spinifex.toml so the value
|
||||
// is visible/auditable; the daemon reads it via cluster config.
|
||||
IPSecEnabled bool
|
||||
}
|
||||
|
||||
// PredastoreNodeConfig describes a single Predastore node for multi-node config generation.
|
||||
|
||||
@@ -48,6 +48,18 @@ func IPSecCertPaths(configDir string) (certPath, keyPath string) {
|
||||
// SANs discovered from the local machine's interfaces are not added here —
|
||||
// IPSec peer identity should be the explicit cluster identity only, not
|
||||
// every interface address, to keep peer-identity matching strict.
|
||||
// GenerateIPSecPeerCertIfEnabled is a no-op when enabled is false. Used by
|
||||
// admin init and admin join so the call site reads as a single guarded line
|
||||
// keyed off the cluster's network.ipsec_enabled flag.
|
||||
func GenerateIPSecPeerCertIfEnabled(enabled bool, configDir, hostname, nodeIP string) error {
|
||||
if !enabled {
|
||||
return nil
|
||||
}
|
||||
caCertPath := filepath.Join(configDir, "ca.pem")
|
||||
caKeyPath := filepath.Join(configDir, "ca.key")
|
||||
return GenerateIPSecPeerCert(configDir, caCertPath, caKeyPath, hostname, nodeIP)
|
||||
}
|
||||
|
||||
func GenerateIPSecPeerCert(configDir, caCertPath, caKeyPath, hostname, nodeIP string) error {
|
||||
if hostname == "" {
|
||||
return fmt.Errorf("ipsec peer cert: hostname required")
|
||||
|
||||
@@ -109,6 +109,39 @@ func TestGenerateIPSecPeerCert(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestGenerateIPSecPeerCertIfEnabled(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
caDir := t.TempDir()
|
||||
caCertPath := filepath.Join(caDir, "ca.pem")
|
||||
caKeyPath := filepath.Join(caDir, "ca.key")
|
||||
require.NoError(t, GenerateCACert(caCertPath, caKeyPath))
|
||||
|
||||
t.Run("disabled is a no-op", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
dir := t.TempDir()
|
||||
require.NoError(t, GenerateIPSecPeerCertIfEnabled(false, dir, "node-x", "10.0.0.9"))
|
||||
certPath, _ := IPSecCertPaths(dir)
|
||||
_, err := os.Stat(certPath)
|
||||
assert.True(t, os.IsNotExist(err), "no cert must be written when ipsec disabled")
|
||||
})
|
||||
|
||||
t.Run("enabled generates cert using configDir CA", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
// Use a configDir that already has ca.pem + ca.key beside it (the
|
||||
// canonical layout under /etc/spinifex/).
|
||||
dir := t.TempDir()
|
||||
require.NoError(t, GenerateCACert(filepath.Join(dir, "ca.pem"), filepath.Join(dir, "ca.key")))
|
||||
|
||||
require.NoError(t, GenerateIPSecPeerCertIfEnabled(true, dir, "node-y", "10.0.0.10"))
|
||||
certPath, keyPath := IPSecCertPaths(dir)
|
||||
_, err := os.Stat(certPath)
|
||||
require.NoError(t, err)
|
||||
_, err = os.Stat(keyPath)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestIPSecCertPaths(t *testing.T) {
|
||||
t.Parallel()
|
||||
certPath, keyPath := IPSecCertPaths("/etc/spinifex")
|
||||
|
||||
Reference in New Issue
Block a user