Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Specify list of ExtraSANs for the kube-apiserver certs #263

Merged
merged 23 commits into from
Mar 26, 2024
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 8 additions & 7 deletions src/k8s/api/v1/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,14 @@ type BootstrapConfig struct {
// ServiceCIDR is the CIDR of the cluster services.
ServiceCIDR string `yaml:"service-cidr"`
// EnableRBAC determines if RBAC will be enabled; *bool to know true/false/unset.
EnableRBAC *bool `yaml:"enable-rbac"`
K8sDqlitePort int `yaml:"k8s-dqlite-port"`
Datastore string `yaml:"datastore"`
DatastoreURL string `yaml:"datastore-url,omitempty"`
DatastoreCACert string `yaml:"datastore-ca-crt,omitempty"`
DatastoreClientCert string `yaml:"datastore-client-crt,omitempty"`
DatastoreClientKey string `yaml:"datastore-client-key,omitempty"`
EnableRBAC *bool `yaml:"enable-rbac"`
K8sDqlitePort int `yaml:"k8s-dqlite-port"`
Datastore string `yaml:"datastore"`
DatastoreURL string `yaml:"datastore-url,omitempty"`
DatastoreCACert string `yaml:"datastore-ca-crt,omitempty"`
DatastoreClientCert string `yaml:"datastore-client-crt,omitempty"`
DatastoreClientKey string `yaml:"datastore-client-key,omitempty"`
ExtraSANs []string `yaml:"extrasans"`
}

// SetDefaults sets the fields to default values.
Expand Down
2 changes: 2 additions & 0 deletions src/k8s/api/v1/types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ func TestSetDefaults(t *testing.T) {
EnableRBAC: vals.Pointer(true),
K8sDqlitePort: 9000,
Datastore: "k8s-dqlite",
ExtraSANs: nil,
}

g.Expect(b).To(Equal(expected))
Expand All @@ -34,6 +35,7 @@ func TestBootstrapConfigFromMap(t *testing.T) {
Components: []string{"dns", "network", "storage"},
EnableRBAC: vals.Pointer(true),
K8sDqlitePort: 9000,
ExtraSANs: []string{},
}

// Convert the BootstrapConfig to a map
Expand Down
9 changes: 8 additions & 1 deletion src/k8s/pkg/k8sd/app/hooks_bootstrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -212,10 +212,16 @@ func onBootstrapControlPlane(s *state.State, initConfig map[string]string) error
return fmt.Errorf("unsupported datastore %s, must be one of %v", cfg.APIServer.Datastore, setup.SupportedDatastores)
}

userDefinedIPSANs, userDefinedDNSSANs := utils.SplitIPAndDNSSANs(bootstrapConfig.ExtraSANs)

IPSANs := append([]net.IP{nodeIP}, serviceIPs...)
IPSANs = append(IPSANs, userDefinedIPSANs...)

// Certificates
certificates := pki.NewControlPlanePKI(pki.ControlPlanePKIOpts{
Hostname: s.Name(),
IPSANs: append([]net.IP{nodeIP}, serviceIPs...),
IPSANs: IPSANs,
DNSSANs: userDefinedDNSSANs,
Years: 20,
AllowSelfSignedCA: true,
IncludeMachineAddressSANs: true,
Expand All @@ -235,6 +241,7 @@ func onBootstrapControlPlane(s *state.State, initConfig map[string]string) error
cfg.Certificates.APIServerKubeletClientCert = certificates.APIServerKubeletClientCert
cfg.Certificates.APIServerKubeletClientKey = certificates.APIServerKubeletClientKey
cfg.APIServer.ServiceAccountKey = certificates.ServiceAccountKey
cfg.Certificates.ExtraSANs = bootstrapConfig.ExtraSANs

// Generate kubeconfigs
for _, kubeconfig := range []struct {
Expand Down
8 changes: 7 additions & 1 deletion src/k8s/pkg/k8sd/app/hooks_join.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,16 @@ func onPostJoin(s *state.State, initConfig map[string]string) error {
return fmt.Errorf("unsupported datastore %s, must be one of %v", cfg.APIServer.Datastore, setup.SupportedDatastores)
}

userDefinedIPSANs, userDefinedDNSSANs := utils.SplitIPAndDNSSANs(cfg.Certificates.ExtraSANs)

IPSANs := append([]net.IP{nodeIP}, serviceIPs...)
IPSANs = append(IPSANs, userDefinedIPSANs...)

// Certificates
certificates := pki.NewControlPlanePKI(pki.ControlPlanePKIOpts{
Hostname: s.Name(),
IPSANs: append([]net.IP{nodeIP}, serviceIPs...),
IPSANs: IPSANs,
DNSSANs: userDefinedDNSSANs,
Years: 20,
IncludeMachineAddressSANs: true,
})
Expand Down
137 changes: 114 additions & 23 deletions src/k8s/pkg/k8sd/pki/control_plane_test.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
package pki_test

import (
"crypto/x509"
"embed"
"encoding/pem"
"io/fs"
"net"
"testing"

"github.com/canonical/k8s/pkg/k8sd/pki"
. "github.com/onsi/gomega"
)

//go:embed data
var testCertificates embed.FS

func TestControlPlaneCertificates(t *testing.T) {
c := pki.NewControlPlanePKI(pki.ControlPlanePKIOpts{
Hostname: "h1",
Expand All @@ -24,31 +32,114 @@ func TestControlPlaneCertificates(t *testing.T) {
Hostname: "h1",
Years: 10,
})
c.CACert = `
-----BEGIN CERTIFICATE-----
MIIDtTCCAp2gAwIBAgIQOPOTOjxvIVlC5ev8EzrnITANBgkqhkiG9w0BAQsFADAY
MRYwFAYDVQQDEw1rdWJlcm5ldGVzLWNhMB4XDTI0MDIwODAyNDYyOVoXDTM0MDIw
ODAyNDYyOVowGTEXMBUGA1UEAxMOa3ViZS1hcGlzZXJ2ZXIwggEiMA0GCSqGSIb3
DQEBAQUAA4IBDwAwggEKAoIBAQCum3KkohfK+E4KCpauilnlxm0e6y+jzyOaRCHx
P/3iLqN5zN+s2SV+GJNNcT3vSVZ1YhcJKWNrs7QxK2qcq9OhHncmp9Vqu5BV9O+e
ys4bBlf08lHH0//wrAwXy71ueWXN2uWyFg4i2VSirbRxpXGIR751i4qVtutbSOPy
3Jjf07upq3zAMyvTx1YTZcwduwW2vrU1f48IZOTueS1eOz0YjCkWLueD2uhLLgRA
mcxq33pwTM9P0MaZGrrM2GeA+1Hyss5WtoEMkR6TPUWQmYcKFEZui9/JpLfbM8yu
6h6Ta7GeSccjtclHSGp9fge0IXErhYSmLNoQ7JP8fQeg0DpTAgMBAAGjgfkwgfYw
DgYDVR0PAQH/BAQDAgSwMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcDATAM
BgNVHRMBAf8EAjAAMB8GA1UdIwQYMBaAFJjD6HMwGRJQMOzNm919/ZaqdcUwMIGV
BgNVHREEgY0wgYqCCmt1YmVybmV0ZXOCEmt1YmVybmV0ZXMuZGVmYXVsdIIWa3Vi
ZXJuZXRlcy5kZWZhdWx0LnN2Y4Iea3ViZXJuZXRlcy5kZWZhdWx0LnN2Yy5jbHVz
dGVygiRrdWJlcm5ldGVzLmRlZmF1bHQuc3ZjLmNsdXN0ZXIubG9jYWyHBAqYtwGH
BH8AAAEwDQYJKoZIhvcNAQELBQADggEBADPWn//rPb0SmZ49WhIa6wc39Ryl7eGo
Q2H+RY9BMah/xge6fLgeTvFe+H6Vol9BVqm5XgD0BuD5yzKYI2aDq8Ikm4EMOxPl
7Gs9cqWMMF7Iiw+rYJY4vwzm+5kSCg6oxBx8GLYYkDpbFe8UAWKf/9QTghtoBEEw
JVBDECnQwJU4tb9ANmPbgxmCYLZjx2vmXQRlXpe6QS9nPmMSS9KkJMyLEEpgzIIA
aSprnA8WIeSaO/5wLMYS1lUWWzegz2LnKuJ5C5Q+XYkwIY/vFH7OSTnmvt+rHwhh
4Oj+ScJ0RKnGGcXQnctSvMogDoucw7Y2RjxKcJV8fEKV5ZIeTz0U+nE=
-----END CERTIFICATE-----`

cert, err := fs.ReadFile(testCertificates, "data/ca.pem")
g.Expect(err).To(BeNil())
c.CACert = string(cert)

g := NewWithT(t)
g.Expect(c.CompleteCertificates()).ToNot(BeNil())
})

t.Run("ApiServerCertSANs", func(t *testing.T) {
c := pki.NewControlPlanePKI(pki.ControlPlanePKIOpts{
Hostname: "h1",
Years: 10,
AllowSelfSignedCA: true,
IPSANs: []net.IP{net.ParseIP("192.168.2.123")},
DNSSANs: []string{"cluster.local"},
})

g := NewWithT(t)
g.Expect(c.CompleteCertificates()).To(BeNil())

block, _ := pem.Decode([]byte(c.APIServerCert))
g.Expect(block).ToNot(BeNil())

cert, _ := x509.ParseCertificate(block.Bytes)
g.Expect(cert).ToNot(BeNil())

t.Run("IPAddresses", func(t *testing.T) {
g := NewWithT(t)
expectedIPs := []string{"192.168.2.123", "127.0.0.1", "::1"}

// Convert cert.IPAddresses to a slice of string representations
actualIPs := make([]string, len(cert.IPAddresses))
for i, ip := range cert.IPAddresses {
actualIPs[i] = ip.String()
}

for _, expectedIP := range expectedIPs {
t.Run(expectedIP, func(t *testing.T) {
g.Expect(actualIPs).To(ContainElement(expectedIP), "IP should be present: "+expectedIP)
})
}

g.Expect(cert.IPAddresses).To(HaveLen(len(expectedIPs)))
})

t.Run("DNSNames", func(t *testing.T) {
g := NewWithT(t)
expectedDNSNames := []string{"cluster.local", "kubernetes", "kubernetes.default", "kubernetes.default.svc", "kubernetes.default.svc.cluster", "kubernetes.default.svc.cluster.local"}

for _, expectedDNS := range expectedDNSNames {
t.Run(expectedDNS, func(t *testing.T) {
g.Expect(cert.DNSNames).To(ContainElement(expectedDNS), "DNS should be present: "+expectedDNS)
})
}

g.Expect(cert.DNSNames).To(HaveLen(len(expectedDNSNames)))
})
})

t.Run("KubeletCertSANs", func(t *testing.T) {
c := pki.NewControlPlanePKI(pki.ControlPlanePKIOpts{
Hostname: "h1",
Years: 10,
AllowSelfSignedCA: true,
IPSANs: []net.IP{net.ParseIP("192.168.2.123")},
DNSSANs: []string{"cluster.local"},
})

g := NewWithT(t)
g.Expect(c.CompleteCertificates()).To(BeNil())

block, _ := pem.Decode([]byte(c.KubeletCert))
g.Expect(block).ToNot(BeNil())

cert, _ := x509.ParseCertificate(block.Bytes)
g.Expect(cert).ToNot(BeNil())

t.Run("IPAddresses", func(t *testing.T) {
g := NewWithT(t)
expectedIPs := []string{"192.168.2.123", "127.0.0.1", "::1"}

// Convert cert.IPAddresses to a slice of string representations
actualIPs := make([]string, len(cert.IPAddresses))
for i, ip := range cert.IPAddresses {
actualIPs[i] = ip.String()
}

for _, expectedIP := range expectedIPs {
t.Run(expectedIP, func(t *testing.T) {
g.Expect(actualIPs).To(ContainElement(expectedIP), "IP should be present: "+expectedIP)
})
}

g.Expect(cert.IPAddresses).To(HaveLen(len(expectedIPs)))
})

t.Run("DNSNames", func(t *testing.T) {
g := NewWithT(t)
expectedDNSNames := []string{"h1", "cluster.local"}

for _, expectedDNS := range expectedDNSNames {
t.Run(expectedDNS, func(t *testing.T) {
g.Expect(cert.DNSNames).To(ContainElement(expectedDNS), "DNS should be present: "+expectedDNS)
})
}

g.Expect(cert.DNSNames).To(HaveLen(len(expectedDNSNames)))
})
})
}
22 changes: 22 additions & 0 deletions src/k8s/pkg/k8sd/pki/data/ca.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
-----BEGIN CERTIFICATE-----
MIIDkzCCAnugAwIBAgIUHjmfFK9cfwsJ9wVeay77DUxGfsQwDQYJKoZIhvcNAQEL
BQAwWTELMAkGA1UEBhMCVVMxDjAMBgNVBAgMBVN0YXRlMQ0wCwYDVQQHDARDaXR5
MRUwEwYDVQQKDAxPcmdhbml6YXRpb24xFDASBgNVBAMMC2V4YW1wbGUuY29tMB4X
DTI0MDMyMTE5MTE1NVoXDTM0MDMxOTE5MTE1NVowWTELMAkGA1UEBhMCVVMxDjAM
BgNVBAgMBVN0YXRlMQ0wCwYDVQQHDARDaXR5MRUwEwYDVQQKDAxPcmdhbml6YXRp
b24xFDASBgNVBAMMC2V4YW1wbGUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
MIIBCgKCAQEAqiI/f/IWAk+2I3uxoTxB20RxrwUvPglAmsvXpkT40PbCHZ9pqI4I
GQoY5mR4bQMx4s3TQNIMGIIha9IvLXVgQb6WxZNc7lLWOg/VHAw+0tUkGnO2o89v
loRNJj2+ZcFu9UZQDLa/cr5pKGnFI4O3rR8DcQxt9rPtSY62ICLFwqU2Hw3fjyHI
FITKmTrZNccmcWKBuOfj4DkFaFT9+jZ72W8DHBXMjAm7qZC3ar9ZlzhHT8mI942i
LuNd0r47yrzga/kLCtjHDYXjBGBareIsfAZDJ+1WV9wVShL42brTwchZhBVcxY66
by8PZJPD97c22zvVyCKIUGGcFKxvWb2fBQIDAQABo1MwUTAdBgNVHQ4EFgQU3LTT
fZ/8wUZhUj856yEniIkE6xwwHwYDVR0jBBgwFoAU3LTTfZ/8wUZhUj856yEniIkE
6xwwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEABRNgwMKqA5Y8
7wfa+X3RsoG0BVOF/+GYCtyXBwH3lXzlOrkTbkL4e9rYGmPx67VCpsnCEAhipta3
FqjGyZFhMsaaIDlhJjm+K7MTGA7aSfo6NIBmpPRKjIQFL2rhmqs1r7riafwvvDrU
CzhIi7rODCf7NAzoISU1EzowzKdKNgGYMNvIpv1pMd7p7WHQNK+W+gvQJZ93UpDY
o9fgMdo44Am9bsiiPi7LAWU5qzbdUErrgFslI+inwD3dOxIwBGfEfD0ngz2nF+Jh
S63GKldmH7KYVE4sdB2BvfgiraDTTHRIDNre930YIhVI+XLHIhtJ+BSpFO4w/idC
xjvgVUetag==
-----END CERTIFICATE-----
13 changes: 7 additions & 6 deletions src/k8s/pkg/k8sd/types/cluster_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,13 @@ type Network struct {
}

type Certificates struct {
CACert string `yaml:"ca-crt,omitempty"`
CAKey string `yaml:"ca-key,omitempty"`
APIServerKubeletClientCert string `yaml:"apiserver-kubelet-client-crt,omitempty"`
APIServerKubeletClientKey string `yaml:"apiserver-kubelet-client-key,omitempty"`
K8sDqliteCert string `yaml:"k8s-dqlite-crt,omitempty"`
K8sDqliteKey string `yaml:"k8s-dqlite-key,omitempty"`
CACert string `yaml:"ca-crt,omitempty"`
CAKey string `yaml:"ca-key,omitempty"`
APIServerKubeletClientCert string `yaml:"apiserver-kubelet-client-crt,omitempty"`
APIServerKubeletClientKey string `yaml:"apiserver-kubelet-client-key,omitempty"`
K8sDqliteCert string `yaml:"k8s-dqlite-crt,omitempty"`
K8sDqliteKey string `yaml:"k8s-dqlite-key,omitempty"`
ExtraSANs []string `yaml:"extrasans,omitempty"`

DatastoreCACert string `yaml:"datastore-ca-crt,omitempty"`
DatastoreClientCert string `yaml:"datastore-client-crt,omitempty"`
Expand Down
24 changes: 24 additions & 0 deletions src/k8s/pkg/utils/certificate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package utils

import (
"net"
)

func SplitIPAndDNSSANs(extraSANs []string) ([]net.IP, []string) {
var ipSANs []net.IP
var dnsSANs []string

for _, san := range extraSANs {
if san == "" {
continue
}

if ip := net.ParseIP(san); ip != nil {
ipSANs = append(ipSANs, ip)
} else {
dnsSANs = append(dnsSANs, san)
}
}

return ipSANs, dnsSANs
}
31 changes: 31 additions & 0 deletions src/k8s/pkg/utils/certificate_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package utils_test

import (
"testing"

"github.com/canonical/k8s/pkg/utils"

. "github.com/onsi/gomega"
)

func TestSplitIPAndDNSSANs(t *testing.T) {
tests := []string{"192.168.0.1", "::1", "cluster.local", "kubernetes.svc.local", "", "2001:db8:0:1:1:1:1:1"}

g := NewWithT(t)
gotIPs, gotDNSs := utils.SplitIPAndDNSSANs(tests)

// Convert cert.IPAddresses to a slice of string representations
ips := make([]string, len(gotIPs))
for i, ip := range gotIPs {
ips[i] = ip.String()
}

g.Expect(len(ips)).To(Equal(3))
g.Expect(ips).To(ContainElement("192.168.0.1"))
g.Expect(ips).To(ContainElement("::1"))
g.Expect(ips).To(ContainElement("2001:db8:0:1:1:1:1:1"))

g.Expect(len(gotDNSs)).To(Equal(2))
g.Expect(gotDNSs).To(ContainElement("cluster.local"))
g.Expect(gotDNSs).To(ContainElement("kubernetes.svc.local"))
}
Loading