mirror of
https://github.com/mulgadc/spinifex.git
synced 2026-10-15 17:11:30 +00:00
198 lines
6.8 KiB
Go
198 lines
6.8 KiB
Go
//go:build e2e
|
|
|
|
package cert
|
|
|
|
import (
|
|
"crypto/x509"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/mulgadc/spinifex/tests/e2e/harness"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// TestCertIssuance is the Go port of run-cert-e2e.sh.
|
|
//
|
|
// Validates that server certificates contain the correct SANs and that TLS
|
|
// connections succeed without InsecureSkipVerify when the Spinifex CA is
|
|
// trusted. Assumes the cluster is bootstrapped (services up, certs generated,
|
|
// CA installed in system trust store during install).
|
|
func TestCertIssuance(t *testing.T) {
|
|
env := harness.LoadEnv(t)
|
|
artifacts := harness.ArtifactDir(t, env)
|
|
|
|
caPath, err := harness.ResolveCACert(env)
|
|
require.NoError(t, err, "Spinifex CA cert must exist")
|
|
t.Logf("CA cert: %s", caPath)
|
|
t.Logf("mode=%s nodeIPs=%v serviceIPs=%v", env.Mode, env.NodeIPs, env.ServiceIPs)
|
|
|
|
caCert, err := harness.ParseCertFile(caPath)
|
|
require.NoError(t, err)
|
|
|
|
t.Run("SANContainsNodeIPs", func(t *testing.T) {
|
|
for _, ip := range env.NodeIPs {
|
|
ip := ip
|
|
t.Run(ip, func(t *testing.T) {
|
|
t.Parallel()
|
|
cert := loadCert(t, env, ip, artifacts)
|
|
assert.True(t, harness.CertHasIPSAN(cert, ip),
|
|
"cert for %s missing IP SAN %s (got IPs=%v DNS=%v)",
|
|
ip, ip, cert.IPAddresses, cert.DNSNames)
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("HostnameInDNSSANs", func(t *testing.T) {
|
|
hostname, err := os.Hostname()
|
|
require.NoError(t, err)
|
|
if hostname == "" || hostname == "localhost" {
|
|
t.Skipf("hostname %q not testable", hostname)
|
|
}
|
|
if env.Mode == harness.ModeMultinode || env.Mode == harness.ModeBaremetal {
|
|
t.Skip("hostname SAN check requires local cert path")
|
|
}
|
|
referenceIP := env.NodeIPs[0]
|
|
cert := loadCert(t, env, referenceIP, artifacts)
|
|
assert.True(t, harness.CertHasDNSSAN(cert, hostname),
|
|
"cert missing DNS SAN for hostname %q (DNS=%v)", hostname, cert.DNSNames)
|
|
})
|
|
|
|
t.Run("TLSHandshakeWithCAFile", func(t *testing.T) {
|
|
pool, err := harness.LoadCAPool(caPath)
|
|
require.NoError(t, err)
|
|
for _, ip := range env.ServiceIPs {
|
|
ip := ip
|
|
t.Run(ip, func(t *testing.T) {
|
|
t.Parallel()
|
|
url := fmt.Sprintf("https://%s:%d/", ip, env.AWSGWPort)
|
|
code, _, err := harness.HTTPSGet(url, pool, env.DefaultTimeout)
|
|
require.NoError(t, err, "TLS handshake to %s", url)
|
|
assert.NotZero(t, code, "expected HTTP response from %s", url)
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("SystemTrustStore", func(t *testing.T) {
|
|
t.Run("CAInstalled", func(t *testing.T) {
|
|
_, err := os.Stat(harness.SystemCAPath)
|
|
require.NoError(t, err, "CA cert must be installed at %s", harness.SystemCAPath)
|
|
})
|
|
|
|
t.Run("FingerprintMatchesConfig", func(t *testing.T) {
|
|
sysCert, err := harness.ParseCertFile(harness.SystemCAPath)
|
|
require.NoError(t, err)
|
|
assert.True(t, harness.FingerprintMatches(sysCert, caCert),
|
|
"system CA fingerprint differs from %s — system trust store stale", caPath)
|
|
})
|
|
|
|
t.Run("CurlWithoutCAFlag", func(t *testing.T) {
|
|
testIP := env.ServiceIPs[0]
|
|
url := fmt.Sprintf("https://%s:%d/", testIP, env.AWSGWPort)
|
|
code, _, err := harness.HTTPSGet(url, nil, env.DefaultTimeout)
|
|
require.NoError(t, err, "system trust store: GET %s failed — CA not in system bundle?", url)
|
|
assert.NotZero(t, code)
|
|
})
|
|
})
|
|
|
|
t.Run("OpenSSLVerify", func(t *testing.T) {
|
|
for _, ip := range env.ServiceIPs {
|
|
ip := ip
|
|
t.Run(ip, func(t *testing.T) {
|
|
t.Parallel()
|
|
require.NoError(t, harness.OpenSSLVerify(t, caPath, ip, env.AWSGWPort))
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("CADownloadEndpoint", func(t *testing.T) {
|
|
uiIP := env.NodeIPs[0]
|
|
if env.Mode == harness.ModeSingle {
|
|
uiIP = "127.0.0.1"
|
|
}
|
|
pool, err := harness.LoadCAPool(caPath)
|
|
require.NoError(t, err)
|
|
url := fmt.Sprintf("https://%s:%d/api/ca.pem", uiIP, env.UIPort)
|
|
code, body, err := harness.HTTPSGet(url, pool, env.DefaultTimeout)
|
|
if err != nil {
|
|
t.Skipf("UI not reachable at %s: %v", url, err)
|
|
}
|
|
require.Equalf(t, 200, code, "GET %s returned %d", url, code)
|
|
downloaded, err := harness.ParseCertPEM(body)
|
|
require.NoError(t, err, "CA download must be a PEM cert")
|
|
assert.True(t, harness.FingerprintMatches(downloaded, caCert),
|
|
"downloaded CA fingerprint differs from local CA at %s", caPath)
|
|
})
|
|
|
|
t.Run("CertMetadata", func(t *testing.T) {
|
|
if env.Mode == harness.ModeMultinode || env.Mode == harness.ModeBaremetal {
|
|
t.Skip("metadata check requires local cert path")
|
|
}
|
|
referenceIP := env.NodeIPs[0]
|
|
cert := loadCert(t, env, referenceIP, artifacts)
|
|
|
|
assert.Contains(t, cert.Subject.CommonName, "Spinifex Server",
|
|
"server cert CN should be 'Spinifex Server' (got %q)", cert.Subject.CommonName)
|
|
assert.Contains(t, cert.Issuer.CommonName, "Spinifex Local CA",
|
|
"server cert issuer should be 'Spinifex Local CA' (got %q)", cert.Issuer.CommonName)
|
|
assert.False(t, cert.NotAfter.IsZero(), "cert must have an expiry")
|
|
t.Logf("cert expiry: %s", cert.NotAfter)
|
|
})
|
|
|
|
t.Run("CrossNodeIssuerSharedCA", func(t *testing.T) {
|
|
if env.Mode == harness.ModeSingle || len(env.ServiceIPs) < 2 {
|
|
t.Skip("requires multi-node deployment")
|
|
}
|
|
for _, ip := range env.ServiceIPs {
|
|
ip := ip
|
|
t.Run(ip, func(t *testing.T) {
|
|
t.Parallel()
|
|
served, err := harness.FetchServedCert(ip, env.AWSGWPort, env.DefaultTimeout)
|
|
require.NoError(t, err)
|
|
assert.Contains(t, served.Issuer.CommonName, "Spinifex Local CA",
|
|
"node %s cert has unexpected issuer %q", ip, served.Issuer.CommonName)
|
|
})
|
|
}
|
|
})
|
|
|
|
harness.OnFailure(t, func() {
|
|
harness.DumpCmd(t, artifacts, "ip-addr.txt", "ip", "-4", "addr", "show")
|
|
harness.DumpCmd(t, artifacts, "ca-text.txt", "openssl", "x509", "-in", caPath, "-text", "-noout")
|
|
if _, err := os.Stat(harness.SystemCAPath); err == nil {
|
|
harness.DumpCmd(t, artifacts, "system-ca-text.txt",
|
|
"openssl", "x509", "-in", harness.SystemCAPath, "-text", "-noout")
|
|
}
|
|
})
|
|
}
|
|
|
|
// loadCert resolves the cert for ip: local file when available, served cert
|
|
// otherwise. Dumps cert text to artifacts on failure.
|
|
func loadCert(t *testing.T, env *harness.Env, ip, artifacts string) *x509.Certificate {
|
|
t.Helper()
|
|
if path := harness.ServerCertPath(env); path != "" {
|
|
cert, err := harness.ParseCertFile(path)
|
|
require.NoErrorf(t, err, "parse local cert %s", path)
|
|
harness.OnFailure(t, func() {
|
|
harness.DumpCmd(t, artifacts, sanitize("cert-"+ip+".txt"),
|
|
"openssl", "x509", "-in", path, "-text", "-noout")
|
|
})
|
|
return cert
|
|
}
|
|
served, err := harness.FetchServedCert(ip, env.AWSGWPort, env.DefaultTimeout)
|
|
require.NoErrorf(t, err, "fetch served cert %s:%d", ip, env.AWSGWPort)
|
|
harness.OnFailure(t, func() {
|
|
out, err := exec.Command("openssl", "s_client", "-connect",
|
|
fmt.Sprintf("%s:%d", ip, env.AWSGWPort), "-servername", ip).CombinedOutput()
|
|
if err != nil {
|
|
out = append(out, []byte(fmt.Sprintf("\n(exit: %v)", err))...)
|
|
}
|
|
harness.DumpFile(t, artifacts, sanitize("served-"+ip+".txt"), out)
|
|
})
|
|
return served
|
|
}
|
|
|
|
func sanitize(s string) string { return strings.ReplaceAll(s, "/", "_") }
|