Skip to content

Commit 58ff17b

Browse files
committed
When using external CA, look for common trust anchor within CA bundle.
1 parent 19d9e4f commit 58ff17b

File tree

3 files changed

+232
-16
lines changed

3 files changed

+232
-16
lines changed

cmd/kubeadm/app/phases/kubeconfig/kubeconfig.go

Lines changed: 44 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -264,10 +264,36 @@ func validateKubeConfig(outDir, filename string, config *clientcmdapi.Config) er
264264
}
265265
caExpected := bytes.TrimSpace(config.Clusters[expectedCluster].CertificateAuthorityData)
266266

267-
// If the current CA cert on disk doesn't match the expected CA cert, error out because we have a file, but it's stale
268-
if !bytes.Equal(caCurrent, caExpected) {
269-
return errors.Errorf("a kubeconfig file %q exists already but has got the wrong CA cert", kubeConfigFilePath)
267+
// Parse the current certificate authority data
268+
currentCACerts, err := certutil.ParseCertsPEM(caCurrent)
269+
if err != nil {
270+
return errors.Errorf("the kubeconfig file %q contains an invalid CA cert", kubeConfigFilePath)
271+
}
272+
273+
// Parse the expected certificate authority data
274+
expectedCACerts, err := certutil.ParseCertsPEM(caExpected)
275+
if err != nil {
276+
return errors.Errorf("the expected base64 encoded CA cert could not be parsed as a PEM:\n%s\n", caExpected)
277+
}
278+
279+
// Only use the first certificate in the current CA cert list
280+
currentCaCert := currentCACerts[0]
281+
282+
// Find a common trust anchor
283+
trustAnchorFound := false
284+
for _, expectedCaCert := range expectedCACerts {
285+
// Compare the current CA cert to the expected CA cert.
286+
// If the certificates match then a common trust anchor was found.
287+
if currentCaCert.Equal(expectedCaCert) {
288+
trustAnchorFound = true
289+
break
290+
}
291+
}
292+
if !trustAnchorFound {
293+
return errors.Errorf("a kubeconfig file %q exists but does not contain a trusted CA in its current context's "+
294+
"cluster. Total CA certificates found: %d", kubeConfigFilePath, len(currentCACerts))
270295
}
296+
271297
// If the current API Server location on disk doesn't match the expected API server, show a warning
272298
if currentConfig.Clusters[currentCluster].Server != config.Clusters[expectedCluster].Server {
273299
klog.Warningf("a kubeconfig file %q exists already but has an unexpected API Server URL: expected: %s, got: %s",
@@ -386,20 +412,31 @@ func writeKubeConfigFromSpec(out io.Writer, spec *kubeConfigSpec, clustername st
386412
func ValidateKubeconfigsForExternalCA(outDir string, cfg *kubeadmapi.InitConfiguration) error {
387413
// Creates a kubeconfig file with the target CA and server URL
388414
// to be used as a input for validating user provided kubeconfig files
389-
caCert, err := pkiutil.TryLoadCertFromDisk(cfg.CertificatesDir, kubeadmconstants.CACertAndKeyBaseName)
415+
caCert, intermediaries, err := pkiutil.TryLoadCertChainFromDisk(cfg.CertificatesDir, kubeadmconstants.CACertAndKeyBaseName)
390416
if err != nil {
391417
return errors.Wrapf(err, "the CA file couldn't be loaded")
392418
}
419+
420+
// Combine caCert and intermediaries into one array
421+
caCertChain := append([]*x509.Certificate{caCert}, intermediaries...)
422+
393423
// Validate period
394-
certsphase.CheckCertificatePeriodValidity(kubeadmconstants.CACertAndKeyBaseName, caCert)
424+
for _, cert := range caCertChain {
425+
certsphase.CheckCertificatePeriodValidity(kubeadmconstants.CACertAndKeyBaseName, cert)
426+
}
395427

396428
// validate user provided kubeconfig files for the scheduler and controller-manager
397429
localAPIEndpoint, err := kubeadmutil.GetLocalAPIEndpoint(&cfg.LocalAPIEndpoint)
398430
if err != nil {
399431
return err
400432
}
401433

402-
validationConfigLocal := kubeconfigutil.CreateBasic(localAPIEndpoint, "dummy", "dummy", pkiutil.EncodeCertPEM(caCert))
434+
caCertBytes, err := pkiutil.EncodeCertBundlePEM(caCertChain)
435+
if err != nil {
436+
return err
437+
}
438+
439+
validationConfigLocal := kubeconfigutil.CreateBasic(localAPIEndpoint, "dummy", "dummy", caCertBytes)
403440
kubeConfigFileNamesLocal := []string{
404441
kubeadmconstants.ControllerManagerKubeConfigFileName,
405442
kubeadmconstants.SchedulerKubeConfigFileName,
@@ -417,7 +454,7 @@ func ValidateKubeconfigsForExternalCA(outDir string, cfg *kubeadmapi.InitConfigu
417454
return err
418455
}
419456

420-
validationConfigCPE := kubeconfigutil.CreateBasic(controlPlaneEndpoint, "dummy", "dummy", pkiutil.EncodeCertPEM(caCert))
457+
validationConfigCPE := kubeconfigutil.CreateBasic(controlPlaneEndpoint, "dummy", "dummy", caCertBytes)
421458
kubeConfigFileNamesCPE := []string{
422459
kubeadmconstants.AdminKubeConfigFileName,
423460
kubeadmconstants.SuperAdminKubeConfigFileName,

cmd/kubeadm/app/phases/kubeconfig/kubeconfig_test.go

Lines changed: 174 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -608,7 +608,11 @@ func TestValidateKubeConfig(t *testing.T) {
608608

609609
func TestValidateKubeconfigsForExternalCA(t *testing.T) {
610610
tmpDir := testutil.SetupTempDir(t)
611-
defer os.RemoveAll(tmpDir)
611+
defer func() {
612+
if err := os.RemoveAll(tmpDir); err != nil {
613+
t.Error(err)
614+
}
615+
}()
612616
pkiDir := filepath.Join(tmpDir, "pki")
613617

614618
initConfig := &kubeadmapi.InitConfiguration{
@@ -623,11 +627,9 @@ func TestValidateKubeconfigsForExternalCA(t *testing.T) {
623627

624628
// creates CA, write to pkiDir and remove ca.key to get into external CA condition
625629
caCert, caKey := certstestutil.SetupCertificateAuthority(t)
626-
if err := pkiutil.WriteCertAndKey(pkiDir, kubeadmconstants.CACertAndKeyBaseName, caCert, caKey); err != nil {
627-
t.Fatalf("failure while saving CA certificate and key: %v", err)
628-
}
629-
if err := os.Remove(filepath.Join(pkiDir, kubeadmconstants.CAKeyName)); err != nil {
630-
t.Fatalf("failure while deleting ca.key: %v", err)
630+
631+
if err := pkiutil.WriteCertBundle(pkiDir, kubeadmconstants.CACertAndKeyBaseName, []*x509.Certificate{caCert}); err != nil {
632+
t.Fatalf("failure while saving CA certificate: %v", err)
631633
}
632634

633635
notAfter, _ := time.Parse(time.RFC3339, "2026-01-02T15:04:05Z")
@@ -697,7 +699,11 @@ func TestValidateKubeconfigsForExternalCA(t *testing.T) {
697699
for name, test := range tests {
698700
t.Run(name, func(t *testing.T) {
699701
tmpdir := testutil.SetupTempDir(t)
700-
defer os.RemoveAll(tmpdir)
702+
defer func() {
703+
if err := os.RemoveAll(tmpdir); err != nil {
704+
t.Error(err)
705+
}
706+
}()
701707

702708
for name, config := range test.filesToWrite {
703709
if err := createKubeConfigFileIfNotExists(tmpdir, name, config); err != nil {
@@ -719,6 +725,166 @@ func TestValidateKubeconfigsForExternalCA(t *testing.T) {
719725
}
720726
}
721727

728+
func TestValidateKubeconfigsForExternalCAMissingRoot(t *testing.T) {
729+
tmpDir := testutil.SetupTempDir(t)
730+
defer func() {
731+
if err := os.RemoveAll(tmpDir); err != nil {
732+
t.Error(err)
733+
}
734+
}()
735+
pkiDir := filepath.Join(tmpDir, "pki")
736+
737+
initConfig := &kubeadmapi.InitConfiguration{
738+
ClusterConfiguration: kubeadmapi.ClusterConfiguration{
739+
CertificatesDir: pkiDir,
740+
},
741+
LocalAPIEndpoint: kubeadmapi.APIEndpoint{
742+
BindPort: 1234,
743+
AdvertiseAddress: "1.2.3.4",
744+
},
745+
}
746+
747+
// Creates CA, write to pkiDir and remove ca.key to get into external CA mode
748+
caCert, caKey := certstestutil.SetupCertificateAuthority(t)
749+
750+
// Setup multiple intermediate certificate authorities (CAs) for testing purposes.
751+
// This is "Root CA" signs "Intermediate Authority 1A" signs "Intermediate Authority 2A"
752+
intermediateCACert1a, intermediateCAKey1a := certstestutil.SetupIntermediateCertificateAuthority(t, caCert, caKey, "Intermediate Authority 1A")
753+
intermediateCACert2a, intermediateCAKey2a := certstestutil.SetupIntermediateCertificateAuthority(t, intermediateCACert1a, intermediateCAKey1a, "Intermediate Authority 1A")
754+
755+
// These two CA certificates should both validate using the Intermediate CA 2B certificate
756+
// This is "Root CA" signs "Intermediate Authority 1B" signs "Intermediate Authority 2B"
757+
intermediateCACert1b, intermediateCAKey1b := certstestutil.SetupIntermediateCertificateAuthority(t, caCert, caKey, "Intermediate Authority 1B")
758+
intermediateCACert2b, intermediateCAKey2b := certstestutil.SetupIntermediateCertificateAuthority(t, intermediateCACert1b, intermediateCAKey1b, "Intermediate Authority 2B")
759+
760+
notAfter, _ := time.Parse(time.RFC3339, "2036-01-02T15:04:05Z")
761+
clusterName := "myOrg1"
762+
763+
var validCaCertBundle []*x509.Certificate
764+
validCaCertBundle = append(validCaCertBundle, caCert, intermediateCACert1a, intermediateCACert2a)
765+
multipleCAConfigRootCAIssuer := setupKubeConfigWithClientAuth(t, caCert, caKey, notAfter, "https://1.2.3.4:1234", "test-cluster", clusterName)
766+
multipleCAConfigIntermediateCA1aIssuer := setupKubeConfigWithClientAuth(t, intermediateCACert1a, intermediateCAKey1a, notAfter, "https://1.2.3.4:1234", "test-cluster", clusterName)
767+
multipleCAConfigIntermediateCA2aIssuer := setupKubeConfigWithClientAuth(t, intermediateCACert2a, intermediateCAKey2a, notAfter, "https://1.2.3.4:1234", "test-cluster", clusterName)
768+
769+
var caBundleMissingRootCA []*x509.Certificate
770+
caBundleMissingRootCA = append(caBundleMissingRootCA, intermediateCACert1b, intermediateCACert2b)
771+
multipleCAConfigNoRootCA := setupKubeConfigWithClientAuth(t, intermediateCACert2b, intermediateCAKey2b, notAfter, "https://1.2.3.4:1234", "test-cluster", clusterName)
772+
multipleCAConfigDifferentIssuer := setupKubeConfigWithClientAuth(t, intermediateCACert2a, intermediateCAKey2a, notAfter, "https://1.2.3.4:1234", "test-cluster", clusterName)
773+
774+
var caBundlePartialChain []*x509.Certificate
775+
caBundlePartialChain = append(caBundlePartialChain, intermediateCACert1a)
776+
multipleCaPartialCA := setupKubeConfigWithClientAuth(t, intermediateCACert2b, intermediateCAKey2b, notAfter, "https://1.2.3.4:1234", "test-cluster", clusterName)
777+
778+
tests := map[string]struct {
779+
filesToWrite map[string]*clientcmdapi.Config
780+
initConfig *kubeadmapi.InitConfiguration
781+
expectedError bool
782+
caCertificate []*x509.Certificate
783+
}{
784+
// Positive test cases
785+
"valid config issued from RootCA": {
786+
filesToWrite: map[string]*clientcmdapi.Config{
787+
kubeadmconstants.AdminKubeConfigFileName: multipleCAConfigRootCAIssuer,
788+
kubeadmconstants.SuperAdminKubeConfigFileName: multipleCAConfigRootCAIssuer,
789+
kubeadmconstants.KubeletKubeConfigFileName: multipleCAConfigRootCAIssuer,
790+
kubeadmconstants.ControllerManagerKubeConfigFileName: multipleCAConfigRootCAIssuer,
791+
kubeadmconstants.SchedulerKubeConfigFileName: multipleCAConfigRootCAIssuer,
792+
},
793+
caCertificate: validCaCertBundle,
794+
initConfig: initConfig,
795+
expectedError: false,
796+
},
797+
"valid config issued from IntermediateCA 1A": {
798+
filesToWrite: map[string]*clientcmdapi.Config{
799+
kubeadmconstants.AdminKubeConfigFileName: multipleCAConfigIntermediateCA1aIssuer,
800+
kubeadmconstants.SuperAdminKubeConfigFileName: multipleCAConfigIntermediateCA1aIssuer,
801+
kubeadmconstants.KubeletKubeConfigFileName: multipleCAConfigIntermediateCA1aIssuer,
802+
kubeadmconstants.ControllerManagerKubeConfigFileName: multipleCAConfigIntermediateCA1aIssuer,
803+
kubeadmconstants.SchedulerKubeConfigFileName: multipleCAConfigIntermediateCA1aIssuer,
804+
},
805+
caCertificate: validCaCertBundle,
806+
initConfig: initConfig,
807+
expectedError: false,
808+
},
809+
"valid config issued from IntermediateCA 2A": {
810+
filesToWrite: map[string]*clientcmdapi.Config{
811+
kubeadmconstants.AdminKubeConfigFileName: multipleCAConfigIntermediateCA2aIssuer,
812+
kubeadmconstants.SuperAdminKubeConfigFileName: multipleCAConfigIntermediateCA2aIssuer,
813+
kubeadmconstants.KubeletKubeConfigFileName: multipleCAConfigIntermediateCA2aIssuer,
814+
kubeadmconstants.ControllerManagerKubeConfigFileName: multipleCAConfigIntermediateCA2aIssuer,
815+
kubeadmconstants.SchedulerKubeConfigFileName: multipleCAConfigIntermediateCA2aIssuer,
816+
},
817+
caCertificate: validCaCertBundle,
818+
initConfig: initConfig,
819+
expectedError: false,
820+
},
821+
"valid config issued from IntermediateCA 2B, CA missing root certificate": {
822+
filesToWrite: map[string]*clientcmdapi.Config{
823+
kubeadmconstants.AdminKubeConfigFileName: multipleCAConfigNoRootCA,
824+
kubeadmconstants.SuperAdminKubeConfigFileName: multipleCAConfigNoRootCA,
825+
kubeadmconstants.KubeletKubeConfigFileName: multipleCAConfigNoRootCA,
826+
kubeadmconstants.ControllerManagerKubeConfigFileName: multipleCAConfigNoRootCA,
827+
kubeadmconstants.SchedulerKubeConfigFileName: multipleCAConfigNoRootCA,
828+
},
829+
caCertificate: caBundleMissingRootCA,
830+
initConfig: initConfig,
831+
expectedError: false,
832+
},
833+
// Negative test cases
834+
"invalid config issued from IntermediateCA 2A, testing a chain with a different issuer": {
835+
filesToWrite: map[string]*clientcmdapi.Config{
836+
kubeadmconstants.AdminKubeConfigFileName: multipleCAConfigDifferentIssuer,
837+
kubeadmconstants.SuperAdminKubeConfigFileName: multipleCAConfigDifferentIssuer,
838+
kubeadmconstants.KubeletKubeConfigFileName: multipleCAConfigDifferentIssuer,
839+
kubeadmconstants.ControllerManagerKubeConfigFileName: multipleCAConfigDifferentIssuer,
840+
kubeadmconstants.SchedulerKubeConfigFileName: multipleCAConfigDifferentIssuer,
841+
},
842+
caCertificate: caBundleMissingRootCA,
843+
initConfig: initConfig,
844+
expectedError: true,
845+
},
846+
"invalid config issued from IntermediateCA 2B chain, CA only contains Intermediate 1A": {
847+
filesToWrite: map[string]*clientcmdapi.Config{
848+
kubeadmconstants.AdminKubeConfigFileName: multipleCaPartialCA,
849+
kubeadmconstants.SuperAdminKubeConfigFileName: multipleCaPartialCA,
850+
kubeadmconstants.KubeletKubeConfigFileName: multipleCaPartialCA,
851+
kubeadmconstants.ControllerManagerKubeConfigFileName: multipleCaPartialCA,
852+
kubeadmconstants.SchedulerKubeConfigFileName: multipleCaPartialCA,
853+
},
854+
caCertificate: caBundlePartialChain,
855+
initConfig: initConfig,
856+
expectedError: true,
857+
},
858+
}
859+
860+
for name, test := range tests {
861+
t.Run(name, func(t *testing.T) {
862+
tmpdir := testutil.SetupTempDir(t)
863+
defer func() {
864+
if err := os.RemoveAll(tmpdir); err != nil {
865+
t.Error(err)
866+
}
867+
}()
868+
869+
for name, config := range test.filesToWrite {
870+
if err := createKubeConfigFileIfNotExists(tmpdir, name, config); err != nil {
871+
t.Errorf("createKubeConfigFileIfNotExists failed: %v", err)
872+
}
873+
}
874+
875+
if err := pkiutil.WriteCertBundle(pkiDir, kubeadmconstants.CACertAndKeyBaseName, test.caCertificate); err != nil {
876+
t.Fatalf("Failure while saving CA certificate: %v", err)
877+
}
878+
879+
err := ValidateKubeconfigsForExternalCA(tmpdir, test.initConfig)
880+
if (err != nil) != test.expectedError {
881+
t.Fatalf("ValidateKubeconfigsForExternalCA failed\n%s\nexpected error: %t\n\tgot: %t\nerror: %v",
882+
name, test.expectedError, (err != nil), err)
883+
}
884+
})
885+
}
886+
}
887+
722888
// setupKubeConfigWithClientAuth is a test utility function that wraps buildKubeConfigFromSpec for building a KubeConfig object With ClientAuth
723889
func setupKubeConfigWithClientAuth(t *testing.T, caCert *x509.Certificate, caKey crypto.Signer, notAfter time.Time, apiServer, clientName, clustername string, organizations ...string) *clientcmdapi.Config {
724890
spec := &kubeConfigSpec{
@@ -740,7 +906,7 @@ func setupKubeConfigWithClientAuth(t *testing.T, caCert *x509.Certificate, caKey
740906
return config
741907
}
742908

743-
// setupKubeConfigWithClientAuth is a test utility function that wraps buildKubeConfigFromSpec for building a KubeConfig object With Token
909+
// setupKubeConfigWithTokenAuth is a test utility function that wraps buildKubeConfigFromSpec for building a KubeConfig object With Token
744910
func setupKubeConfigWithTokenAuth(t *testing.T, caCert *x509.Certificate, apiServer, clientName, token, clustername string) *clientcmdapi.Config {
745911
spec := &kubeConfigSpec{
746912
CACert: caCert,

cmd/kubeadm/app/util/certs/util.go

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,20 @@ func SetupCertificateAuthority(t *testing.T) (*x509.Certificate, crypto.Signer)
3838
Config: certutil.Config{CommonName: "kubernetes"},
3939
})
4040
if err != nil {
41-
t.Fatalf("failure while generating CA certificate and key: %v", err)
41+
t.Fatalf("Failure while generating CA certificate and key: %v", err)
42+
}
43+
44+
return caCert, caKey
45+
}
46+
47+
// SetupIntermediateCertificateAuthority is a utility function for kubeadm testing that creates a
48+
// Intermediate CertificateAuthority cert/key pair
49+
func SetupIntermediateCertificateAuthority(t *testing.T, parentCert *x509.Certificate, parentKey crypto.Signer, cn string) (*x509.Certificate, crypto.Signer) {
50+
caCert, caKey, err := pkiutil.NewIntermediateCertificateAuthority(parentCert, parentKey, &pkiutil.CertConfig{
51+
Config: certutil.Config{CommonName: cn},
52+
})
53+
if err != nil {
54+
t.Fatalf("Failure while generating intermediate CA certificate and key: %v", err)
4255
}
4356

4457
return caCert, caKey

0 commit comments

Comments
 (0)