diff --git a/cmd/eks-konnectivity-cert/main.go b/cmd/eks-konnectivity-cert/main.go
index 4cb62b328..58ec68ce2 100644
--- a/cmd/eks-konnectivity-cert/main.go
+++ b/cmd/eks-konnectivity-cert/main.go
@@ -1,16 +1,6 @@
-// Command eks-konnectivity-cert mints, then reuses, an ECDSA-P256 serving
-// certificate for the on-node konnectivity-server, signed by the K3s cluster CA.
-//
-// The konnectivity-agent (DaemonSet on workers) verifies the server it dials
-// using the in-pod ServiceAccount CA (/var/run/secrets/.../ca.crt), which in
-// K3s is the same server-ca that signs the apiserver. Signing the konnectivity
-// serving cert with that CA is therefore what lets the agent validate the
-// reverse-tunnel endpoint with no separate CA distribution.
-//
-// The eks-node AMI carries no openssl, so this is the Go equivalent: load the
-// K3s server CA cert+key, issue a leaf (ServerAuth) cert carrying the cluster's
-// konnectivity SANs (the NLB private-endpoint IP, public endpoint IP, NLB DNS),
-// and cache it under -dir so the OpenRC service is byte-stable across restarts.
+// Command eks-konnectivity-cert mints, then reuses, a K3s-CA-signed serving
+// certificate for the on-node konnectivity-server; the implementation is in
+// agents/eks/konnectivitycert.
//
// Usage:
//
@@ -23,225 +13,27 @@
package main
import (
- "crypto/ecdsa"
- "crypto/elliptic"
- "crypto/rand"
- "crypto/x509"
- "crypto/x509/pkix"
- "encoding/pem"
+ "context"
"flag"
- "fmt"
"log/slog"
- "math/big"
- "net"
"os"
- "path/filepath"
- "strings"
- "time"
_ "github.com/mulgadc/bluebottle/pkg/fipsboot"
+
+ "github.com/mulgadc/spinifex/spinifex/agents/eks/konnectivitycert"
)
func main() {
- dir := flag.String("dir", "", "directory to cache tls.crt/tls.key in")
- caCert := flag.String("ca-cert", "", "PEM path of the signing (K3s server) CA cert")
- caKey := flag.String("ca-key", "", "PEM path of the signing CA private key")
- cn := flag.String("cn", "konnectivity-server", "certificate common name")
- sansCSV := flag.String("sans", "", "comma-separated SANs (IP or DNS)")
+ cfg := konnectivitycert.Config{Out: os.Stdout}
+ flag.StringVar(&cfg.Dir, "dir", "", "directory to cache tls.crt/tls.key in")
+ flag.StringVar(&cfg.CACert, "ca-cert", "", "PEM path of the signing (K3s server) CA cert")
+ flag.StringVar(&cfg.CAKey, "ca-key", "", "PEM path of the signing CA private key")
+ flag.StringVar(&cfg.CN, "cn", "konnectivity-server", "certificate common name")
+ flag.StringVar(&cfg.SANs, "sans", "", "comma-separated SANs (IP or DNS)")
flag.Parse()
- if *dir == "" || *caCert == "" || *caKey == "" || *sansCSV == "" {
- fatal("eks-konnectivity-cert: -dir, -ca-cert, -ca-key and -sans are required")
+ if err := konnectivitycert.Run(context.Background(), cfg); err != nil {
+ slog.Error("eks-konnectivity-cert: " + err.Error())
+ os.Exit(1)
}
- dns, ips := splitSANs(*sansCSV)
- if len(dns) == 0 && len(ips) == 0 {
- fatal("eks-konnectivity-cert: -sans lists no usable names")
- }
-
- certPath, keyPath, err := ensureCert(*dir, *caCert, *caKey, *cn, dns, ips)
- if err != nil {
- fatal("eks-konnectivity-cert: " + err.Error())
- }
- fmt.Printf("%s\t%s\n", certPath, keyPath)
-}
-
-// ensureCert returns the cached cert/key paths under dir, minting and persisting
-// a fresh CA-signed pair when none is present, readable, and still SAN-current.
-func ensureCert(dir, caCertPath, caKeyPath, cn string, dns []string, ips []net.IP) (string, string, error) {
- certPath := filepath.Join(dir, "tls.crt")
- keyPath := filepath.Join(dir, "tls.key")
-
- if certCoversSANs(certPath, keyPath, dns, ips) {
- return certPath, keyPath, nil
- }
-
- caCert, caKey, err := loadCA(caCertPath, caKeyPath)
- if err != nil {
- return "", "", err
- }
- certPEM, keyPEM, err := issueLeaf(caCert, caKey, cn, dns, ips)
- if err != nil {
- return "", "", err
- }
- if err = os.MkdirAll(dir, 0o700); err != nil {
- return "", "", fmt.Errorf("mkdir %s: %w", dir, err)
- }
- if err = os.WriteFile(certPath, certPEM, 0o644); err != nil {
- return "", "", fmt.Errorf("write %s: %w", certPath, err)
- }
- if err = os.WriteFile(keyPath, keyPEM, 0o600); err != nil {
- return "", "", fmt.Errorf("write %s: %w", keyPath, err)
- }
- return certPath, keyPath, nil
-}
-
-// certCoversSANs reports whether the cached cert exists, parses, has a key, and
-// already carries every requested SAN — so a cert minted before the endpoint
-// set changed is re-issued rather than served stale.
-func certCoversSANs(certPath, keyPath string, dns []string, ips []net.IP) bool {
- c, err := os.ReadFile(certPath)
- if err != nil {
- return false
- }
- if k, kerr := os.ReadFile(keyPath); kerr != nil || len(k) == 0 {
- return false
- }
- der := decodeFirst(c)
- if der == nil {
- return false
- }
- crt, err := x509.ParseCertificate(der)
- if err != nil {
- return false
- }
- have := make(map[string]struct{}, len(crt.DNSNames)+len(crt.IPAddresses))
- for _, n := range crt.DNSNames {
- have[n] = struct{}{}
- }
- for _, ip := range crt.IPAddresses {
- have[ip.String()] = struct{}{}
- }
- for _, n := range dns {
- if _, ok := have[n]; !ok {
- return false
- }
- }
- for _, ip := range ips {
- if _, ok := have[ip.String()]; !ok {
- return false
- }
- }
- return true
-}
-
-// loadCA parses the PEM CA cert + EC/PKCS8 private key used to sign the leaf.
-func loadCA(certPath, keyPath string) (*x509.Certificate, *ecdsa.PrivateKey, error) {
- certPEM, err := os.ReadFile(certPath)
- if err != nil {
- return nil, nil, fmt.Errorf("read ca cert %s: %w", certPath, err)
- }
- der := decodeFirst(certPEM)
- if der == nil {
- return nil, nil, fmt.Errorf("ca cert %s: no PEM block", certPath)
- }
- caCert, err := x509.ParseCertificate(der)
- if err != nil {
- return nil, nil, fmt.Errorf("parse ca cert: %w", err)
- }
-
- keyPEM, err := os.ReadFile(keyPath)
- if err != nil {
- return nil, nil, fmt.Errorf("read ca key %s: %w", keyPath, err)
- }
- keyDER := decodeFirst(keyPEM)
- if keyDER == nil {
- return nil, nil, fmt.Errorf("ca key %s: no PEM block", keyPath)
- }
- caKey, err := parseECKey(keyDER)
- if err != nil {
- return nil, nil, err
- }
- return caCert, caKey, nil
-}
-
-// parseECKey accepts the EC SEC1 or PKCS8 encodings K3s may write for the CA key.
-func parseECKey(der []byte) (*ecdsa.PrivateKey, error) {
- if k, err := x509.ParseECPrivateKey(der); err == nil {
- return k, nil
- }
- k8, err := x509.ParsePKCS8PrivateKey(der)
- if err != nil {
- return nil, fmt.Errorf("parse ca key (sec1 and pkcs8 failed): %w", err)
- }
- ec, ok := k8.(*ecdsa.PrivateKey)
- if !ok {
- return nil, fmt.Errorf("ca key is %T, want ecdsa", k8)
- }
- return ec, nil
-}
-
-// issueLeaf mints an ECDSA-P256 ServerAuth leaf for cn + SANs, signed by caCert/caKey.
-func issueLeaf(caCert *x509.Certificate, caKey *ecdsa.PrivateKey, cn string, dns []string, ips []net.IP) (certPEM, keyPEM []byte, err error) {
- priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
- if err != nil {
- return nil, nil, fmt.Errorf("generate key: %w", err)
- }
- serial, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
- if err != nil {
- return nil, nil, fmt.Errorf("serial: %w", err)
- }
- now := time.Now().UTC()
- tmpl := x509.Certificate{
- SerialNumber: serial,
- Subject: pkix.Name{CommonName: cn},
- NotBefore: now.Add(-time.Hour),
- NotAfter: now.AddDate(10, 0, 0),
- KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
- ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
- BasicConstraintsValid: true,
- DNSNames: dns,
- IPAddresses: ips,
- }
- der, err := x509.CreateCertificate(rand.Reader, &tmpl, caCert, &priv.PublicKey, caKey)
- if err != nil {
- return nil, nil, fmt.Errorf("create cert: %w", err)
- }
- keyDER, err := x509.MarshalECPrivateKey(priv)
- if err != nil {
- return nil, nil, fmt.Errorf("marshal key: %w", err)
- }
- certPEM = pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})
- keyPEM = pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: keyDER})
- return certPEM, keyPEM, nil
-}
-
-// splitSANs partitions the CSV into DNS names and IPs (an entry that parses as an
-// IP goes to IPAddresses, else DNSNames), matching how SAN matching is typed.
-func splitSANs(s string) (dns []string, ips []net.IP) {
- for p := range strings.SplitSeq(s, ",") {
- p = strings.TrimSpace(p)
- if p == "" {
- continue
- }
- if ip := net.ParseIP(p); ip != nil {
- ips = append(ips, ip)
- continue
- }
- dns = append(dns, p)
- }
- return dns, ips
-}
-
-// decodeFirst returns the DER bytes of the first PEM block, or nil.
-func decodeFirst(pemBytes []byte) []byte {
- blk, _ := pem.Decode(pemBytes)
- if blk == nil {
- return nil
- }
- return blk.Bytes
-}
-
-func fatal(msg string) {
- slog.Error(msg)
- os.Exit(1)
}
diff --git a/spinifex/agents/eks/konnectivitycert/cert.go b/spinifex/agents/eks/konnectivitycert/cert.go
new file mode 100644
index 000000000..8b7ace8fa
--- /dev/null
+++ b/spinifex/agents/eks/konnectivitycert/cert.go
@@ -0,0 +1,243 @@
+// Package konnectivitycert implements eks-konnectivity-cert, which mints, then
+// reuses, an ECDSA-P256 serving certificate for the on-node konnectivity-server,
+// signed by the K3s cluster CA.
+//
+// The konnectivity-agent (DaemonSet on workers) verifies the server it dials
+// using the in-pod ServiceAccount CA (/var/run/secrets/.../ca.crt), which in
+// K3s is the same server-ca that signs the apiserver. Signing the konnectivity
+// serving cert with that CA is therefore what lets the agent validate the
+// reverse-tunnel endpoint with no separate CA distribution.
+//
+// The eks-node AMI carries no openssl, so this is the Go equivalent: load the
+// K3s server CA cert+key, issue a leaf (ServerAuth) cert carrying the cluster's
+// konnectivity SANs (the NLB private-endpoint IP, public endpoint IP, NLB DNS),
+// and cache it under -dir so the OpenRC service is byte-stable across restarts.
+//
+// Writes
/tls.crt and /tls.key; prints the two paths (tab-separated).
+package konnectivitycert
+
+import (
+ "context"
+ "crypto/ecdsa"
+ "crypto/elliptic"
+ "crypto/rand"
+ "crypto/x509"
+ "crypto/x509/pkix"
+ "encoding/pem"
+ "errors"
+ "fmt"
+ "io"
+ "math/big"
+ "net"
+ "os"
+ "path/filepath"
+ "strings"
+ "time"
+)
+
+// Config names the signing CA, the leaf's identity and where it is cached.
+type Config struct {
+ Dir string
+ CACert string
+ CAKey string
+ CN string
+ // SANs is the comma-separated SAN list (IP or DNS).
+ SANs string
+ // Out receives the tab-separated cert and key paths.
+ Out io.Writer
+}
+
+// Run mints or reuses the CA-signed certificate cached under cfg.Dir and writes
+// its cert and key paths to cfg.Out.
+func Run(_ context.Context, cfg Config) error {
+ if cfg.Dir == "" || cfg.CACert == "" || cfg.CAKey == "" || cfg.SANs == "" {
+ return errors.New("-dir, -ca-cert, -ca-key and -sans are required")
+ }
+ dns, ips := splitSANs(cfg.SANs)
+ if len(dns) == 0 && len(ips) == 0 {
+ return errors.New("-sans lists no usable names")
+ }
+
+ certPath, keyPath, err := ensureCert(cfg.Dir, cfg.CACert, cfg.CAKey, cfg.CN, dns, ips)
+ if err != nil {
+ return err
+ }
+ fmt.Fprintf(cfg.Out, "%s\t%s\n", certPath, keyPath)
+ return nil
+}
+
+// ensureCert returns the cached cert/key paths under dir, minting and persisting
+// a fresh CA-signed pair when none is present, readable, and still SAN-current.
+func ensureCert(dir, caCertPath, caKeyPath, cn string, dns []string, ips []net.IP) (string, string, error) {
+ certPath := filepath.Join(dir, "tls.crt")
+ keyPath := filepath.Join(dir, "tls.key")
+
+ if certCoversSANs(certPath, keyPath, dns, ips) {
+ return certPath, keyPath, nil
+ }
+
+ caCert, caKey, err := loadCA(caCertPath, caKeyPath)
+ if err != nil {
+ return "", "", err
+ }
+ certPEM, keyPEM, err := issueLeaf(caCert, caKey, cn, dns, ips)
+ if err != nil {
+ return "", "", err
+ }
+ if err = os.MkdirAll(dir, 0o700); err != nil {
+ return "", "", fmt.Errorf("mkdir %s: %w", dir, err)
+ }
+ if err = os.WriteFile(certPath, certPEM, 0o644); err != nil { //nolint:gosec // G306: public certificate; the private key is written 0600 below.
+ return "", "", fmt.Errorf("write %s: %w", certPath, err)
+ }
+ if err = os.WriteFile(keyPath, keyPEM, 0o600); err != nil {
+ return "", "", fmt.Errorf("write %s: %w", keyPath, err)
+ }
+ return certPath, keyPath, nil
+}
+
+// certCoversSANs reports whether the cached cert exists, parses, has a key, and
+// already carries every requested SAN — so a cert minted before the endpoint
+// set changed is re-issued rather than served stale.
+func certCoversSANs(certPath, keyPath string, dns []string, ips []net.IP) bool {
+ c, err := os.ReadFile(certPath)
+ if err != nil {
+ return false
+ }
+ if k, kerr := os.ReadFile(keyPath); kerr != nil || len(k) == 0 {
+ return false
+ }
+ der := decodeFirst(c)
+ if der == nil {
+ return false
+ }
+ crt, err := x509.ParseCertificate(der)
+ if err != nil {
+ return false
+ }
+ have := make(map[string]struct{}, len(crt.DNSNames)+len(crt.IPAddresses))
+ for _, n := range crt.DNSNames {
+ have[n] = struct{}{}
+ }
+ for _, ip := range crt.IPAddresses {
+ have[ip.String()] = struct{}{}
+ }
+ for _, n := range dns {
+ if _, ok := have[n]; !ok {
+ return false
+ }
+ }
+ for _, ip := range ips {
+ if _, ok := have[ip.String()]; !ok {
+ return false
+ }
+ }
+ return true
+}
+
+// loadCA parses the PEM CA cert + EC/PKCS8 private key used to sign the leaf.
+func loadCA(certPath, keyPath string) (*x509.Certificate, *ecdsa.PrivateKey, error) {
+ certPEM, err := os.ReadFile(certPath)
+ if err != nil {
+ return nil, nil, fmt.Errorf("read ca cert %s: %w", certPath, err)
+ }
+ der := decodeFirst(certPEM)
+ if der == nil {
+ return nil, nil, fmt.Errorf("ca cert %s: no PEM block", certPath)
+ }
+ caCert, err := x509.ParseCertificate(der)
+ if err != nil {
+ return nil, nil, fmt.Errorf("parse ca cert: %w", err)
+ }
+
+ keyPEM, err := os.ReadFile(keyPath)
+ if err != nil {
+ return nil, nil, fmt.Errorf("read ca key %s: %w", keyPath, err)
+ }
+ keyDER := decodeFirst(keyPEM)
+ if keyDER == nil {
+ return nil, nil, fmt.Errorf("ca key %s: no PEM block", keyPath)
+ }
+ caKey, err := parseECKey(keyDER)
+ if err != nil {
+ return nil, nil, err
+ }
+ return caCert, caKey, nil
+}
+
+// parseECKey accepts the EC SEC1 or PKCS8 encodings K3s may write for the CA key.
+func parseECKey(der []byte) (*ecdsa.PrivateKey, error) {
+ if k, err := x509.ParseECPrivateKey(der); err == nil {
+ return k, nil
+ }
+ k8, err := x509.ParsePKCS8PrivateKey(der)
+ if err != nil {
+ return nil, fmt.Errorf("parse ca key (sec1 and pkcs8 failed): %w", err)
+ }
+ ec, ok := k8.(*ecdsa.PrivateKey)
+ if !ok {
+ return nil, fmt.Errorf("ca key is %T, want ecdsa", k8)
+ }
+ return ec, nil
+}
+
+// issueLeaf mints an ECDSA-P256 ServerAuth leaf for cn + SANs, signed by caCert/caKey.
+func issueLeaf(caCert *x509.Certificate, caKey *ecdsa.PrivateKey, cn string, dns []string, ips []net.IP) (certPEM, keyPEM []byte, err error) {
+ priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+ if err != nil {
+ return nil, nil, fmt.Errorf("generate key: %w", err)
+ }
+ serial, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
+ if err != nil {
+ return nil, nil, fmt.Errorf("serial: %w", err)
+ }
+ now := time.Now().UTC()
+ tmpl := x509.Certificate{
+ SerialNumber: serial,
+ Subject: pkix.Name{CommonName: cn},
+ NotBefore: now.Add(-time.Hour),
+ NotAfter: now.AddDate(10, 0, 0),
+ KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
+ ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
+ BasicConstraintsValid: true,
+ DNSNames: dns,
+ IPAddresses: ips,
+ }
+ der, err := x509.CreateCertificate(rand.Reader, &tmpl, caCert, &priv.PublicKey, caKey)
+ if err != nil {
+ return nil, nil, fmt.Errorf("create cert: %w", err)
+ }
+ keyDER, err := x509.MarshalECPrivateKey(priv)
+ if err != nil {
+ return nil, nil, fmt.Errorf("marshal key: %w", err)
+ }
+ certPEM = pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})
+ keyPEM = pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: keyDER})
+ return certPEM, keyPEM, nil
+}
+
+// splitSANs partitions the CSV into DNS names and IPs (an entry that parses as an
+// IP goes to IPAddresses, else DNSNames), matching how SAN matching is typed.
+func splitSANs(s string) (dns []string, ips []net.IP) {
+ for p := range strings.SplitSeq(s, ",") {
+ p = strings.TrimSpace(p)
+ if p == "" {
+ continue
+ }
+ if ip := net.ParseIP(p); ip != nil {
+ ips = append(ips, ip)
+ continue
+ }
+ dns = append(dns, p)
+ }
+ return dns, ips
+}
+
+// decodeFirst returns the DER bytes of the first PEM block, or nil.
+func decodeFirst(pemBytes []byte) []byte {
+ blk, _ := pem.Decode(pemBytes)
+ if blk == nil {
+ return nil
+ }
+ return blk.Bytes
+}
diff --git a/cmd/eks-konnectivity-cert/main_test.go b/spinifex/agents/eks/konnectivitycert/cert_test.go
similarity index 99%
rename from cmd/eks-konnectivity-cert/main_test.go
rename to spinifex/agents/eks/konnectivitycert/cert_test.go
index 27a185130..8e32ff44f 100644
--- a/cmd/eks-konnectivity-cert/main_test.go
+++ b/spinifex/agents/eks/konnectivitycert/cert_test.go
@@ -1,4 +1,4 @@
-package main
+package konnectivitycert
import (
"crypto/ecdsa"
diff --git a/spinifex/agents/eks/konnectivitycert/run_test.go b/spinifex/agents/eks/konnectivitycert/run_test.go
new file mode 100644
index 000000000..d9f1dcc45
--- /dev/null
+++ b/spinifex/agents/eks/konnectivitycert/run_test.go
@@ -0,0 +1,78 @@
+package konnectivitycert
+
+import (
+ "bytes"
+ "context"
+ "path/filepath"
+ "strings"
+ "testing"
+)
+
+func TestRunRejectsIncompleteConfig(t *testing.T) {
+ dir := t.TempDir()
+ full := Config{Dir: dir, CACert: "ca.crt", CAKey: "ca.key", CN: "konnectivity-server", SANs: "10.0.0.1"}
+ cases := []struct {
+ name string
+ mutate func(*Config)
+ want string
+ }{
+ {"no dir", func(c *Config) { c.Dir = "" }, "-dir, -ca-cert, -ca-key and -sans are required"},
+ {"no ca cert", func(c *Config) { c.CACert = "" }, "-dir, -ca-cert, -ca-key and -sans are required"},
+ {"no ca key", func(c *Config) { c.CAKey = "" }, "-dir, -ca-cert, -ca-key and -sans are required"},
+ {"no sans", func(c *Config) { c.SANs = "" }, "-dir, -ca-cert, -ca-key and -sans are required"},
+ {"blank sans list", func(c *Config) { c.SANs = " , ," }, "-sans lists no usable names"},
+ }
+ for _, tc := range cases {
+ t.Run(tc.name, func(t *testing.T) {
+ cfg := full
+ tc.mutate(&cfg)
+ var out bytes.Buffer
+ cfg.Out = &out
+ err := Run(context.Background(), cfg)
+ if err == nil || err.Error() != tc.want {
+ t.Fatalf("Run error = %v, want %q", err, tc.want)
+ }
+ if out.Len() != 0 {
+ t.Errorf("Run wrote %q on failure", out.String())
+ }
+ })
+ }
+}
+
+func TestRunReportsUnreadableCA(t *testing.T) {
+ dir := t.TempDir()
+ missing := filepath.Join(dir, "absent.crt")
+ var out bytes.Buffer
+ err := Run(context.Background(), Config{
+ Dir: filepath.Join(dir, "leaf"), CACert: missing, CAKey: missing, SANs: "10.0.0.1", Out: &out,
+ })
+ if err == nil || !strings.HasPrefix(err.Error(), "read ca cert "+missing+": ") {
+ t.Fatalf("Run error = %v, want read ca cert failure", err)
+ }
+ if out.Len() != 0 {
+ t.Errorf("Run wrote %q on failure", out.String())
+ }
+}
+
+func TestRunPrintsCachedCertAndKeyPaths(t *testing.T) {
+ caCert, caKey := writeTestCA(t, t.TempDir())
+ dir := filepath.Join(t.TempDir(), "leaf")
+ var out bytes.Buffer
+ err := Run(context.Background(), Config{
+ Dir: dir, CACert: caCert, CAKey: caKey, CN: "konnectivity-server", SANs: "10.32.100.4,eks.example", Out: &out,
+ })
+ if err != nil {
+ t.Fatalf("Run: %v", err)
+ }
+ want := filepath.Join(dir, "tls.crt") + "\t" + filepath.Join(dir, "tls.key") + "\n"
+ if out.String() != want {
+ t.Fatalf("output = %q, want %q", out.String(), want)
+ }
+ leaf := leafFrom(t, filepath.Join(dir, "tls.crt"))
+ if err := leaf.VerifyHostname("10.32.100.4"); err != nil {
+ t.Errorf("IP SAN from -sans missing: %v", err)
+ }
+ if err := leaf.VerifyHostname("eks.example"); err != nil {
+ t.Errorf("DNS SAN from -sans missing: %v", err)
+ }
+}