Skip to content

Commit 14da059

Browse files
authored
Merge pull request kubernetes-sigs#177 from chuckha/ca-cert-verify
✨ Calculate cert hashes and verify CA certificates
2 parents 12379ca + 6085446 commit 14da059

File tree

3 files changed

+109
-14
lines changed

3 files changed

+109
-14
lines changed

certs/utils.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,19 @@ package certs
1919
import (
2020
"crypto/rand"
2121
"crypto/rsa"
22+
"crypto/sha256"
2223
"crypto/x509"
2324
"crypto/x509/pkix"
25+
"encoding/hex"
2426
"fmt"
2527
"math"
2628
"math/big"
29+
"strings"
2730
"time"
2831

2932
"github.com/pkg/errors"
3033
"k8s.io/client-go/tools/clientcmd/api"
34+
"k8s.io/client-go/util/cert"
3135
)
3236

3337
const (
@@ -172,3 +176,22 @@ func (cfg *Config) NewSignedCert(key *rsa.PrivateKey, caCert *x509.Certificate,
172176

173177
return x509.ParseCertificate(b)
174178
}
179+
180+
// CertificateHashes hash all the certificates stored in a CA certificate.
181+
func CertificateHashes(certData []byte) ([]string, error) {
182+
certificates, err := cert.ParseCertsPEM(certData)
183+
if err != nil {
184+
return nil, errors.Wrap(err, "unable to parse Cluster CA Certificate")
185+
}
186+
out := make([]string, 0)
187+
for _, c := range certificates {
188+
out = append(out, HashCert(c))
189+
}
190+
return out, nil
191+
}
192+
193+
// HashCert will calculate the sha256 of the incoming certificate.
194+
func HashCert(certificate *x509.Certificate) string {
195+
spkiHash := sha256.Sum256(certificate.RawSubjectPublicKeyInfo)
196+
return "sha256:" + strings.ToLower(hex.EncodeToString(spkiHash[:]))
197+
}

controllers/kubeadmconfig_controller.go

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -117,8 +117,6 @@ func (r *KubeadmConfigReconciler) Reconcile(req ctrl.Request) (_ ctrl.Result, re
117117
return ctrl.Result{}, err
118118
}
119119

120-
// Check for infrastructure ready. If it's not ready then we will requeue the machine until it is.
121-
// The cluster-api machine controller set this value.
122120
if !cluster.Status.InfrastructureReady {
123121
log.Info("Infrastructure is not ready, waiting until ready.")
124122
return ctrl.Result{}, nil
@@ -207,14 +205,13 @@ func (r *KubeadmConfigReconciler) Reconcile(req ctrl.Request) (_ ctrl.Result, re
207205

208206
certificates, err := r.getClusterCertificates(ctx, cluster.GetName(), config.GetNamespace())
209207
if err != nil {
210-
if apierrors.IsNotFound(err) {
211-
certificates, err = r.createClusterCertificates(ctx, cluster.GetName(), config)
212-
if err != nil {
213-
log.Error(err, "unable to create cluster certificates")
214-
return ctrl.Result{}, err
215-
}
216-
} else {
217-
log.Error(err, "unable to lookup cluster certificates")
208+
log.Error(err, "unable to lookup cluster certificates")
209+
return ctrl.Result{}, err
210+
}
211+
if certificates == nil {
212+
certificates, err = r.createClusterCertificates(ctx, cluster.GetName(), config)
213+
if err != nil {
214+
log.Error(err, "unable to create cluster certificates")
218215
return ctrl.Result{}, err
219216
}
220217
}
@@ -260,6 +257,21 @@ func (r *KubeadmConfigReconciler) Reconcile(req ctrl.Request) (_ ctrl.Result, re
260257
return ctrl.Result{}, errors.New("Control plane already exists for the cluster, only KubeadmConfig objects with JoinConfiguration are allowed")
261258
}
262259

260+
// Get certificates to improve security of discovery
261+
certificates, err := r.getClusterCertificates(ctx, cluster.GetName(), config.GetNamespace())
262+
if err != nil {
263+
log.Error(err, "unable to lookup cluster certificates")
264+
return ctrl.Result{}, err
265+
}
266+
if certificates != nil {
267+
hashes, err := certs.CertificateHashes(certificates.ClusterCA.Cert)
268+
if err == nil {
269+
config.Spec.JoinConfiguration.Discovery.BootstrapToken = &kubeadmv1beta1.BootstrapTokenDiscovery{
270+
CACertHashes: hashes,
271+
}
272+
}
273+
}
274+
263275
// ensure that joinConfiguration.Discovery is properly set for joining node on the current cluster
264276
if err := r.reconcileDiscovery(cluster, config); err != nil {
265277
if requeueErr, ok := errors.Cause(err).(capierrors.HasRequeueAfterError); ok {
@@ -286,6 +298,10 @@ func (r *KubeadmConfigReconciler) Reconcile(req ctrl.Request) (_ ctrl.Result, re
286298
log.Error(err, "unable to locate cluster certificates")
287299
return ctrl.Result{}, err
288300
}
301+
if certificates == nil {
302+
log.Info("Cluster CAs have not been created; requeuing to try again")
303+
return ctrl.Result{RequeueAfter: 10 * time.Second}, nil
304+
}
289305

290306
cloudJoinData, err := cloudinit.NewJoinControlPlane(&cloudinit.ControlPlaneJoinInput{
291307
JoinConfiguration: joindata,
@@ -429,8 +445,8 @@ func (r *KubeadmConfigReconciler) reconcileDiscovery(cluster *clusterv1.Cluster,
429445
// if BootstrapToken already contains a CACertHashes or UnsafeSkipCAVerification, respect it; otherwise set for UnsafeSkipCAVerification
430446
// TODO: set CACertHashes instead of UnsafeSkipCAVerification
431447
if len(config.Spec.JoinConfiguration.Discovery.BootstrapToken.CACertHashes) == 0 && !config.Spec.JoinConfiguration.Discovery.BootstrapToken.UnsafeSkipCAVerification {
448+
log.Info("No CAs were provided. Falling back to insecure discover method by skipping CA Cert validation")
432449
config.Spec.JoinConfiguration.Discovery.BootstrapToken.UnsafeSkipCAVerification = true
433-
log.Info("Altering JoinConfiguration.Discovery.BootstrapToken", "UnsafeSkipCAVerification", true)
434450
}
435451

436452
return nil
@@ -482,11 +498,14 @@ func (r *KubeadmConfigReconciler) getClusterCertificates(ctx context.Context, cl
482498
secret := &corev1.Secret{}
483499

484500
err := r.Get(ctx, types.NamespacedName{Name: ClusterCertificatesSecretName(clusterName), Namespace: namespace}, secret)
485-
if err != nil {
501+
switch {
502+
case apierrors.IsNotFound(err):
503+
return nil, nil
504+
case err != nil:
486505
return nil, err
506+
default:
507+
return certs.NewCertificatesFromMap(secret.Data), nil
487508
}
488-
489-
return certs.NewCertificatesFromMap(secret.Data), nil
490509
}
491510

492511
func (r *KubeadmConfigReconciler) createClusterCertificates(ctx context.Context, clusterName string, config *bootstrapv1.KubeadmConfig) (*certs.Certificates, error) {

controllers/kubeadmconfig_controller_test.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import (
3030
"k8s.io/apimachinery/pkg/types"
3131
fakeclient "k8s.io/client-go/kubernetes/fake"
3232
typedcorev1 "k8s.io/client-go/kubernetes/typed/core/v1"
33+
"k8s.io/klog/klogr"
3334
bootstrapv1 "sigs.k8s.io/cluster-api-bootstrap-provider-kubeadm/api/v1alpha2"
3435
"sigs.k8s.io/cluster-api-bootstrap-provider-kubeadm/certs"
3536
kubeadmv1beta1 "sigs.k8s.io/cluster-api-bootstrap-provider-kubeadm/kubeadm/v1beta1"
@@ -838,6 +839,58 @@ func TestReconcileTopLevelObjectSettings(t *testing.T) {
838839
}
839840
}
840841

842+
func TestCACertHashesAndUnsafeCAVerifySkip(t *testing.T) {
843+
namespace := "default" // hardcoded in the new* functions
844+
clusterName := "my-cluster"
845+
cluster := newCluster(clusterName)
846+
cluster.Status.ControlPlaneInitialized = true
847+
cluster.Status.InfrastructureReady = true
848+
849+
controlPlaneMachineName := "my-machine"
850+
machine := newMachine(cluster, controlPlaneMachineName)
851+
852+
workerMachineName := "my-worker"
853+
workerMachine := newMachine(cluster, workerMachineName)
854+
855+
controlPlaneConfigName := "my-config"
856+
config := newKubeadmConfig(machine, controlPlaneConfigName)
857+
858+
workerConfigName := "worker-join-cfg"
859+
workerConfig := newWorkerJoinKubeadmConfig(workerMachine)
860+
861+
certificates, _ := certs.NewCertificates()
862+
secret := &corev1.Secret{
863+
ObjectMeta: metav1.ObjectMeta{
864+
Name: ClusterCertificatesSecretName(cluster.GetName()),
865+
Namespace: namespace,
866+
},
867+
Data: certificates.ToMap(),
868+
}
869+
870+
myclient := fake.NewFakeClientWithScheme(setupScheme(), cluster, machine, workerMachine, config, workerConfig, secret)
871+
872+
reconciler := KubeadmConfigReconciler{
873+
Client: myclient,
874+
SecretsClientFactory: newFakeSecretFactory(),
875+
KubeadmInitLock: &myInitLocker{},
876+
Log: klogr.New(),
877+
}
878+
879+
req := ctrl.Request{
880+
NamespacedName: types.NamespacedName{Name: workerConfigName, Namespace: namespace},
881+
}
882+
if _, err := reconciler.Reconcile(req); err != nil {
883+
t.Fatalf("reconciled an error: %v", err)
884+
}
885+
cfg := &bootstrapv1.KubeadmConfig{}
886+
if err := myclient.Get(context.Background(), req.NamespacedName, cfg); err != nil {
887+
t.Fatal(err)
888+
}
889+
if cfg.Spec.JoinConfiguration.Discovery.BootstrapToken.UnsafeSkipCAVerification == true {
890+
t.Fatal("Should not skip unsafe")
891+
}
892+
}
893+
841894
// test utils
842895

843896
// newCluster return a CAPI cluster object

0 commit comments

Comments
 (0)