Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Dynamically choose which certificates.k8s.io version to support (#1011)
Signed-off-by: Artiom Diomin <kron82@gmail.com>
- Loading branch information
Showing
3 changed files
with
295 additions
and
15 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
232 changes: 232 additions & 0 deletions
232
pkg/controller/nodecsrapprover/node_csr_approver_v1beta1.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} |