Skip to content

Commit

Permalink
Dynamically choose which certificates.k8s.io version to support (#1011)
Browse files Browse the repository at this point in the history
Signed-off-by: Artiom Diomin <kron82@gmail.com>
  • Loading branch information
kron4eg committed Jun 30, 2021
1 parent 9e8cd17 commit 6c98ea6
Show file tree
Hide file tree
Showing 3 changed files with 295 additions and 15 deletions.
4 changes: 0 additions & 4 deletions cmd/machine-controller/main.go
Expand Up @@ -44,7 +44,6 @@ import (
"github.com/kubermatic/machine-controller/pkg/node"
"github.com/kubermatic/machine-controller/pkg/signals"

certificatesv1 "k8s.io/api/certificates/v1"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/kubernetes"
Expand Down Expand Up @@ -181,9 +180,6 @@ func main() {
if err := apiextensionsv1.AddToScheme(scheme.Scheme); err != nil {
klog.Fatalf("failed to add apiextensionsv1 api to scheme: %v", err)
}
if err := certificatesv1.AddToScheme(scheme.Scheme); err != nil {
klog.Fatalf("failed to add certificatesv1 api to scheme: %v", err)
}
if err := clusterv1alpha1.AddToScheme(scheme.Scheme); err != nil {
klog.Fatalf("failed to add clusterv1alpha1 api to scheme: %v", err)
}
Expand Down
74 changes: 63 additions & 11 deletions pkg/controller/nodecsrapprover/node_csr_approver.go
Expand Up @@ -26,11 +26,14 @@ import (
"github.com/kubermatic/machine-controller/pkg/apis/cluster/v1alpha1"

certificatesv1 "k8s.io/api/certificates/v1"
certificatesv1beta1 "k8s.io/api/certificates/v1beta1"
corev1 "k8s.io/api/core/v1"
kerrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/client-go/discovery"
certificatesv1client "k8s.io/client-go/kubernetes/typed/certificates/v1"
certificatesv1beta1client "k8s.io/client-go/kubernetes/typed/certificates/v1beta1"
"k8s.io/klog"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller"
Expand Down Expand Up @@ -59,19 +62,73 @@ type reconciler struct {
}

func Add(mgr manager.Manager) error {
certClient, err := certificatesv1client.NewForConfig(mgr.GetConfig())
// TODO: delete whole file node_csr_approver_v1beta1.go and dynamic API groups discovery
// after we drop kubernetes 1.18 support
discoveryClient, err := discovery.NewDiscoveryClientForConfig(mgr.GetConfig())
if err != nil {
return fmt.Errorf("failed to create certificate client: %v", err)
return fmt.Errorf("failed to init discovery client: %w", err)
}

r := &reconciler{Client: mgr.GetClient(), certClient: certClient.CertificateSigningRequests()}
c, err := controller.New(ControllerName, mgr, controller.Options{Reconciler: r})
srvGroups, err := discoveryClient.ServerGroups()
if err != nil {
return fmt.Errorf("failed to get server API groups: %w", err)
}

certificatesVersionFound := ""
for _, group := range srvGroups.Groups {
if group.Name != "certificates.k8s.io" {
continue
}

for _, groupVersion := range group.Versions {
if groupVersion.Version == "v1" {
certificatesVersionFound = "v1"
}

if certificatesVersionFound == "" {
certificatesVersionFound = "v1beta1"
}
}
}

var (
rec reconcile.Reconciler
watchType client.Object
)

switch certificatesVersionFound {
case "v1":
certClient, err := certificatesv1client.NewForConfig(mgr.GetConfig())
if err != nil {
return fmt.Errorf("failed to create certificate client: %v", err)
}
rec = &reconciler{Client: mgr.GetClient(), certClient: certClient.CertificateSigningRequests()}
watchType = &certificatesv1.CertificateSigningRequest{}
case "v1beta1":
certClient, err := certificatesv1beta1client.NewForConfig(mgr.GetConfig())
if err != nil {
return fmt.Errorf("failed to create certificate client: %v", err)
}
rec = &reconcilerv1beta1{Client: mgr.GetClient(), certClient: certClient.CertificateSigningRequests()}
watchType = &certificatesv1beta1.CertificateSigningRequest{}
}

cntrl, err := controller.New(ControllerName, mgr, controller.Options{Reconciler: rec})
if err != nil {
return fmt.Errorf("failed to construct controller: %v", err)
}
return c.Watch(&source.Kind{Type: &certificatesv1.CertificateSigningRequest{}}, &handler.EnqueueRequestForObject{})

return cntrl.Watch(&source.Kind{Type: watchType}, &handler.EnqueueRequestForObject{})
}

var (
allowedUsages = []certificatesv1.KeyUsage{
certificatesv1.UsageDigitalSignature,
certificatesv1.UsageKeyEncipherment,
certificatesv1.UsageServerAuth,
}
)

func (r *reconciler) Reconcile(ctx context.Context, request reconcile.Request) (reconcile.Result, error) {
err := r.reconcile(ctx, request)
if err != nil {
Expand All @@ -80,10 +137,6 @@ func (r *reconciler) Reconcile(ctx context.Context, request reconcile.Request) (
return reconcile.Result{}, err
}

var allowedUsages = []certificatesv1.KeyUsage{certificatesv1.UsageDigitalSignature,
certificatesv1.UsageKeyEncipherment,
certificatesv1.UsageServerAuth}

func (r *reconciler) reconcile(ctx context.Context, request reconcile.Request) error {
// Get the CSR object
csr := &certificatesv1.CertificateSigningRequest{}
Expand Down Expand Up @@ -233,8 +286,7 @@ func (r *reconciler) validateX509CSR(csr *certificatesv1.CertificateSigningReque
func (r *reconciler) getMachineForNode(ctx context.Context, nodeName string) (v1alpha1.Machine, bool, error) {
// List all Machines in all namespaces
machines := &v1alpha1.MachineList{}
listOptions := &client.ListOptions{Namespace: ""}
if err := r.Client.List(ctx, machines, listOptions); err != nil {
if err := r.Client.List(ctx, machines); err != nil {
return v1alpha1.Machine{}, false, fmt.Errorf("failed to list all machine objects: %v", err)
}

Expand Down
232 changes: 232 additions & 0 deletions pkg/controller/nodecsrapprover/node_csr_approver_v1beta1.go
@@ -0,0 +1,232 @@
/*
Copyright 2020 The Machine Controller Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package nodecsrapprover

import (
"context"
"crypto/x509"
"encoding/pem"
"fmt"
"strings"

certificatesv1beta1 "k8s.io/api/certificates/v1beta1"
corev1 "k8s.io/api/core/v1"
kerrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/sets"
certificatesv1beta1client "k8s.io/client-go/kubernetes/typed/certificates/v1beta1"
"k8s.io/klog"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/reconcile"

"github.com/kubermatic/machine-controller/pkg/apis/cluster/v1alpha1"
)

var (
allowedUsagesV1beta1 = []certificatesv1beta1.KeyUsage{
certificatesv1beta1.UsageDigitalSignature,
certificatesv1beta1.UsageKeyEncipherment,
certificatesv1beta1.UsageServerAuth,
}
)

type reconcilerv1beta1 struct {
client.Client
// Have to use the typed client because csr approval is a subresource
// the dynamic client does not approve
certClient certificatesv1beta1client.CertificateSigningRequestInterface
}

func (r *reconcilerv1beta1) Reconcile(ctx context.Context, request reconcile.Request) (reconcile.Result, error) {
err := r.reconcile(ctx, request)
if err != nil {
klog.Errorf("Reconciliation of request %s failed: %v", request.NamespacedName.String(), err)
}
return reconcile.Result{}, err
}

func (r *reconcilerv1beta1) reconcile(ctx context.Context, request reconcile.Request) error {
// Get the CSR object
csr := &certificatesv1beta1.CertificateSigningRequest{}
if err := r.Get(ctx, request.NamespacedName, csr); err != nil {
if kerrors.IsNotFound(err) {
return nil
}
return err
}
klog.V(4).Infof("Reconciling CSR %s", csr.ObjectMeta.Name)

// If CSR is approved, skip it
for _, condition := range csr.Status.Conditions {
if condition.Type == certificatesv1beta1.CertificateApproved {
klog.V(4).Infof("CSR %s already approved, skipping reconciling", csr.ObjectMeta.Name)
return nil
}
}

// Validate the CSR object and get the node name
nodeName, err := r.validateCSRObject(csr)
if err != nil {
klog.V(4).Infof("Skipping reconciling CSR '%s' because CSR object is not valid: %v", csr.ObjectMeta.Name, err)
return nil
}

// Get machine name for the appropriate node
machine, found, err := r.getMachineForNode(ctx, nodeName)
if err != nil {
return fmt.Errorf("failed to get machine for node '%s': %v", nodeName, err)
}
if !found {
return fmt.Errorf("no machine found for given node '%s'", nodeName)
}

// Parse the certificate request
csrBlock, rest := pem.Decode(csr.Spec.Request)
if csrBlock == nil {
return fmt.Errorf("no certificate request found for the given CSR")
}
if len(rest) != 0 {
return fmt.Errorf("found more than one PEM encoded block in the result")
}
certRequest, err := x509.ParseCertificateRequest(csrBlock.Bytes)
if err != nil {
return err
}

// Validate the certificate request
if err := r.validateX509CSR(csr, certRequest, machine); err != nil {
return fmt.Errorf("error validating the x509 certificate request: %v", err)
}

// Approve CSR
klog.V(4).Infof("Approving CSR %s", csr.ObjectMeta.Name)
approvalCondition := certificatesv1beta1.CertificateSigningRequestCondition{
Type: certificatesv1beta1.CertificateApproved,
Reason: "machine-controller NodeCSRApprover controller approved node serving cert",
Status: corev1.ConditionTrue,
}
csr.Status.Conditions = append(csr.Status.Conditions, approvalCondition)

if _, err := r.certClient.UpdateApproval(ctx, csr, metav1.UpdateOptions{}); err != nil {
return fmt.Errorf("failed to approve CSR %q: %v", csr.Name, err)
}

klog.Infof("Successfully approved CSR %s", csr.ObjectMeta.Name)
return nil
}

// validateCSRObject valides the CSR object and returns name of the node that requested the certificate
func (r *reconcilerv1beta1) validateCSRObject(csr *certificatesv1beta1.CertificateSigningRequest) (string, error) {
// Get and validate the node name
if !strings.HasPrefix(csr.Spec.Username, nodeUserPrefix) {
return "", fmt.Errorf("username must have the '%s' prefix", nodeUserPrefix)
}
nodeName := strings.TrimPrefix(csr.Spec.Username, nodeUserPrefix)
if len(nodeName) == 0 {
return "", fmt.Errorf("node name is empty")
}

// Ensure system:nodes and system:authenticated are in groups
if len(csr.Spec.Groups) < 2 {
return "", fmt.Errorf("there are less than 2 groups")
}
if !sets.NewString(csr.Spec.Groups...).HasAll(nodeGroup, authenticatedGroup) {
return "", fmt.Errorf("'%s' and/or '%s' are not in its groups", nodeGroup, authenticatedGroup)
}

// Check are present usages matching allowed usages
if len(csr.Spec.Usages) != 3 {
return "", fmt.Errorf("there are no exactly three usages defined")
}
for _, usage := range csr.
Spec.Usages {
if !isUsageInUsageListV1beta1(usage, allowedUsagesV1beta1) {
return "", fmt.Errorf("usage %v is not in the list of allowed usages (%v)", usage, allowedUsages)
}
}

return nodeName, nil
}

// validateX509CSR validates the certificate request by comparing CN with username,
// and organization with groups.
func (r *reconcilerv1beta1) validateX509CSR(csr *certificatesv1beta1.CertificateSigningRequest, certReq *x509.CertificateRequest, machine v1alpha1.Machine) error {
// Validate Subject CommonName
if certReq.Subject.CommonName != csr.Spec.Username {
return fmt.Errorf("commonName '%s' is different then CSR username '%s'", certReq.Subject.CommonName, csr.Spec.Username)
}

// Validate Subject Organization
if len(certReq.Subject.Organization) != 1 {
return fmt.Errorf("expected only one organization but got %d instead", len(certReq.Subject.Organization))
}
if certReq.Subject.Organization[0] != nodeGroup {
return fmt.Errorf("organization '%s' doesn't match node group '%s'", certReq.Subject.Organization[0], nodeGroup)
}

machineAddressSet := sets.NewString(machine.Status.NodeRef.Name)
for _, addr := range machine.Status.Addresses {
machineAddressSet.Insert(addr.Address)
}

// Validate SAN DNS names
for _, dns := range certReq.DNSNames {
if len(dns) == 0 {
continue
}
if !machineAddressSet.Has(dns) {
return fmt.Errorf("dns name '%s' cannot be associated with node '%s'", dns, machine.Status.NodeRef.Name)
}
}

// Validate SAN IP addresses
for _, ip := range certReq.IPAddresses {
if len(ip) == 0 {
continue
}
if !machineAddressSet.Has(ip.String()) {
return fmt.Errorf("ip address '%v' cannot be associated with node '%s'", ip, machine.Status.NodeRef.Name)
}
}

return nil
}

func (r *reconcilerv1beta1) getMachineForNode(ctx context.Context, nodeName string) (v1alpha1.Machine, bool, error) {
// List all Machines in all namespaces
machines := &v1alpha1.MachineList{}
if err := r.Client.List(ctx, machines); err != nil {
return v1alpha1.Machine{}, false, fmt.Errorf("failed to list all machine objects: %v", err)
}

for _, machine := range machines.Items {
if machine.Status.NodeRef != nil && machine.Status.NodeRef.Name == nodeName {
return machine, true, nil
}
}

return v1alpha1.Machine{}, false, fmt.Errorf("failed to get machine for given node name '%s'", nodeName)
}

func isUsageInUsageListV1beta1(usage certificatesv1beta1.KeyUsage, usageList []certificatesv1beta1.KeyUsage) bool {
for _, usageListItem := range usageList {
if usage == usageListItem {
return true
}
}
return false
}

0 comments on commit 6c98ea6

Please sign in to comment.