From 6de48c60ed8ed2474df8538b21597f0c70979af7 Mon Sep 17 00:00:00 2001 From: Daneyon Hansen Date: Thu, 8 Aug 2019 23:39:46 -0700 Subject: [PATCH] Adds Trust Bundle Publishing to Proxy Controller --- pkg/controller/proxyconfig/controller.go | 363 +++++++++++++++++++---- pkg/controller/proxyconfig/validation.go | 85 +++++- pkg/names/names.go | 14 +- 3 files changed, 396 insertions(+), 66 deletions(-) diff --git a/pkg/controller/proxyconfig/controller.go b/pkg/controller/proxyconfig/controller.go index cffa010528..90e890b23a 100644 --- a/pkg/controller/proxyconfig/controller.go +++ b/pkg/controller/proxyconfig/controller.go @@ -11,12 +11,15 @@ import ( corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/event" "sigs.k8s.io/controller-runtime/pkg/handler" "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/predicate" "sigs.k8s.io/controller-runtime/pkg/reconcile" "sigs.k8s.io/controller-runtime/pkg/source" ) @@ -48,6 +51,21 @@ func add(mgr manager.Manager, r reconcile.Reconciler) error { return err } + // We only care about a configmap source with a specific name/namespace, + // so filter events before they are provided to the controller event handlers. + pred := predicate.Funcs{ + UpdateFunc: func(e event.UpdateEvent) bool { return handleConfigMap(e.MetaNew) }, + DeleteFunc: func(e event.DeleteEvent) bool { return handleConfigMap(e.Meta) }, + CreateFunc: func(e event.CreateEvent) bool { return handleConfigMap(e.Meta) }, + GenericFunc: func(e event.GenericEvent) bool { return handleConfigMap(e.Meta) }, + } + + // Watch for changes to the additional trust bundle configmap. + err = c.Watch(&source.Kind{Type: &corev1.ConfigMap{}}, &handler.EnqueueRequestForObject{}, pred) + if err != nil { + return err + } + // Watch for changes to the proxy resource. err = c.Watch(&source.Kind{Type: &configv1.Proxy{}}, &handler.EnqueueRequestForObject{}) if err != nil { @@ -57,6 +75,11 @@ func add(mgr manager.Manager, r reconcile.Reconciler) error { return nil } +// handleConfigMap returns true if meta namespace is "openshift-config". +func handleConfigMap(meta metav1.Object) bool { + return meta.GetNamespace() == names.ADDL_TRUST_BUNDLE_CONFIGMAP_NS +} + // ReconcileProxyConfig reconciles a Proxy object type ReconcileProxyConfig struct { // This client, initialized using mgr.Client() above, is a split client @@ -66,73 +89,177 @@ type ReconcileProxyConfig struct { status *statusmanager.StatusManager } -// Reconcile expects request to refer to a proxy object named "cluster" -// in the default namespace and will ensure the proxy object is in the -// desired state. +// Reconcile expects request to refer to a cluster-scoped proxy object +// named "cluster" or a configmap object in namespace "openshift-config" +// and will ensure either object is in the desired state. func (r *ReconcileProxyConfig) Reconcile(request reconcile.Request) (reconcile.Result, error) { - log.Printf("Reconciling proxy '%s'", request.Name) - proxyConfig := &configv1.Proxy{} - err := r.client.Get(context.TODO(), request.NamespacedName, proxyConfig) - if err != nil { - if apierrors.IsNotFound(err) { - // Request object not found, could have been deleted after reconcile request. - // Return and don't requeue - log.Printf("proxy '%s' not found; reconciliation will be skipped", request.Name) + validate := true + trustBundle := &corev1.ConfigMap{} + + switch { + case request.NamespacedName == names.Proxy(): + var err error + proxyConfig := &configv1.Proxy{} + infraConfig := &configv1.Infrastructure{} + netConfig := &configv1.Network{} + clusterConfig := &corev1.ConfigMap{} + + log.Printf("Reconciling proxy '%s'", request.Name) + if err := r.client.Get(context.TODO(), request.NamespacedName, proxyConfig); err != nil { + if apierrors.IsNotFound(err) { + // Request object not found, could have been deleted after reconcile request. + // Return and don't requeue + log.Println("proxy not found; reconciliation will be skipped", "request", request) + return reconcile.Result{}, nil + } + // Error reading the object - requeue the request. + return reconcile.Result{}, fmt.Errorf("failed to get proxy '%s': %v", request.Name, err) + } + + // A nil proxy is generated by upgrades and installs not requiring a proxy. + if !isSpecHTTPProxySet(&proxyConfig.Spec) && + !isSpecHTTPSProxySet(&proxyConfig.Spec) && + !isSpecNoProxySet(&proxyConfig.Spec) { + log.Printf("httpProxy, httpsProxy and noProxy not defined for proxy '%s'; validation will be skipped", + request.Name) + validate = false + } + + if validate { + if err := r.ValidateProxyConfig(&proxyConfig.Spec); err != nil { + log.Printf("Failed to validate proxy '%s': %v", proxyConfig.Name, err) + r.status.SetDegraded(statusmanager.ProxyConfig, "InvalidProxyConfig", + fmt.Sprintf("The configuration is invalid for proxy '%s' (%v). "+ + "Use 'oc edit proxy.config.openshift.io %s' to fix.", proxyConfig.Name, err, proxyConfig.Name)) + return reconcile.Result{}, fmt.Errorf("failed to validate proxy '%s': %v", proxyConfig.Name, err) + } + } + + if !isSpecTrustedCASet(&proxyConfig.Spec) { + // Create a configmap containing the system trust bundle. + if trustBundle, err = r.generateSystemTrustBundle(); err != nil { + log.Printf("Failed to generate system trust bundle configmap '%s/%s': %v", + names.TRUSTED_CA_BUNDLE_CONFIGMAP_NS, names.TRUSTED_CA_BUNDLE_CONFIGMAP, err) + r.status.SetDegraded(statusmanager.ProxyConfig, "GenerateConfigMapFailure", + fmt.Sprintf("failed to generate system trust bundle configmap '%s/%s (%v).", + names.TRUSTED_CA_BUNDLE_CONFIGMAP_NS, names.TRUSTED_CA_BUNDLE_CONFIGMAP, err)) + return reconcile.Result{}, fmt.Errorf("failed to generate system trust bundle configmap '%s/%s': %v", + names.TRUSTED_CA_BUNDLE_CONFIGMAP_NS, names.TRUSTED_CA_BUNDLE_CONFIGMAP, err) + } + } else { + // Validate trustedCA of proxy spec. + proxyData, systemData, err := r.validateTrustedCA(proxyConfig.Spec.TrustedCA.Name) + if err != nil { + log.Printf("Failed to validate trustedCA for proxy '%s': %v", proxyConfig.Name, err) + r.status.SetDegraded(statusmanager.ProxyConfig, "InvalidProxyConfig", + fmt.Sprintf("The configuration is invalid for proxy '%s' (%v). "+ + "Use 'oc edit proxy.config.openshift.io %s' to fix.", proxyConfig.Name, err, proxyConfig.Name)) + return reconcile.Result{}, fmt.Errorf("failed to validate trustedCA for proxy '%s': %v", + proxyConfig.Name, err) + } + + // Create a configmap containing the merged proxy.trustedCA/system bundles. + trustBundle, err = r.mergeTrustBundles(proxyData, systemData) + if err != nil { + log.Printf("Failed to merge trustedCA and system bundles for proxy '%s': %v", proxyConfig.Name, err) + r.status.SetDegraded(statusmanager.ProxyConfig, "ProxyCAMergeFailure", + fmt.Sprintf("The configuration is invalid for proxy '%s' (%v). "+ + "Use 'oc edit proxy.config.openshift.io %s' to fix.", proxyConfig.Name, err, proxyConfig.Name)) + return reconcile.Result{}, fmt.Errorf("failed to merge trustedCA and system bundles for proxy '%s': %v", + proxyConfig.Name, err) + } + } + + // Only proceed if the required config objects can be collected. + if err := r.client.Get(context.TODO(), types.NamespacedName{Name: names.CLUSTER_CONFIG}, infraConfig); err != nil { + log.Printf("Failed to get infrastructure config '%s': %v", names.CLUSTER_CONFIG, err) + r.status.SetDegraded(statusmanager.ProxyConfig, "InfraConfigError", + fmt.Sprintf("Error getting infrastructure config %s: %v", names.CLUSTER_CONFIG, err)) + return reconcile.Result{}, fmt.Errorf("failed to get infrastructure config '%s': %v", names.CLUSTER_CONFIG, err) + } + if err := r.client.Get(context.TODO(), types.NamespacedName{Name: names.CLUSTER_CONFIG}, netConfig); err != nil { + log.Printf("Failed to get network config '%s': %v", names.CLUSTER_CONFIG, err) + r.status.SetDegraded(statusmanager.ProxyConfig, "NetworkConfigError", + fmt.Sprintf("Error getting network config '%s': %v.", names.CLUSTER_CONFIG, err)) + return reconcile.Result{}, fmt.Errorf("failed to get network config '%s': %v", names.CLUSTER_CONFIG, err) + } + if err := r.client.Get(context.TODO(), types.NamespacedName{Name: "cluster-config-v1", Namespace: "kube-system"}, + clusterConfig); err != nil { + log.Printf("Failed to get configmap '%s/%s': %v", clusterConfig.Namespace, clusterConfig.Name, err) + r.status.SetDegraded(statusmanager.ProxyConfig, "ClusterConfigError", + fmt.Sprintf("Error getting cluster config configmap '%s/%s': %v.", clusterConfig.Namespace, + clusterConfig.Name, err)) + return reconcile.Result{}, fmt.Errorf("failed to get configmap '%s/%s': %v", clusterConfig.Namespace, clusterConfig.Name, err) + } + // Update proxy status. + if err := r.syncProxyStatus(proxyConfig, infraConfig, netConfig, clusterConfig); err != nil { + log.Printf("Could not sync proxy '%s' status: %v", proxyConfig.Name, err) + r.status.SetDegraded(statusmanager.ProxyConfig, "StatusError", + fmt.Sprintf("Could not update proxy '%s' status: %v", proxyConfig.Name, err)) + return reconcile.Result{}, fmt.Errorf("failed to sync proxy '%s': %v", names.PROXY_CONFIG, err) + } + log.Printf("Reconciling proxy '%s' complete", request.Name) + case request.Namespace == names.ADDL_TRUST_BUNDLE_CONFIGMAP_NS: + log.Printf("Reconciling additional trust bundle configmap '%s/%s'", request.Namespace, request.Name) + if err := r.client.Get(context.TODO(), request.NamespacedName, trustBundle); err != nil { + if apierrors.IsNotFound(err) { + // Request object not found, could have been deleted after reconcile request. + // Return and don't requeue + log.Println("configmap not found; reconciliation will be skipped", "request", request) + return reconcile.Result{}, nil + } + // Error reading the object - requeue the request. + return reconcile.Result{}, fmt.Errorf("failed to get configmap '%s': %v", request, err) + } + + // Only proceed if request matches the configmap referenced by proxy trustedCA. + if err := r.configMapIsProxyTrustedCA(trustBundle.Name); err != nil { + log.Printf("configmap '%s/%s' name differs from trustedCA of proxy '%s' or trustedCA not set; "+ + "reconciliation will be skipped", trustBundle.Namespace, trustBundle.Name, names.PROXY_CONFIG) return reconcile.Result{}, nil } - // Error reading the object - requeue the request. - return reconcile.Result{}, fmt.Errorf("failed to get proxy '%s': %v", request.Name, err) - } - - // A nil proxy is generated by upgrades and installs not requiring a proxy. - if !isSpecHTTPProxySet(&proxyConfig.Spec) && - !isSpecHTTPSProxySet(&proxyConfig.Spec) && - !isSpecNoProxySet(&proxyConfig.Spec) { - log.Printf("httpProxy, httpsProxy and noProxy not defined for proxy '%s'; validation will be skipped", - request.Name) - } - - // Only proceed if the required config objects can be collected. - infraConfig := &configv1.Infrastructure{} - netConfig := &configv1.Network{} - clusterCfgMap := &corev1.ConfigMap{} - if err := r.client.Get(context.TODO(), types.NamespacedName{Name: names.CLUSTER_CONFIG}, infraConfig); err != nil { - log.Printf("failed to get infrastructure config '%s': %v", names.CLUSTER_CONFIG, err) - r.status.SetDegraded(statusmanager.ProxyConfig, "InfraConfigError", - fmt.Sprintf("Error getting infrastructure config %s: %v.", names.CLUSTER_CONFIG, err)) - return reconcile.Result{}, nil - } - if err := r.client.Get(context.TODO(), types.NamespacedName{Name: names.CLUSTER_CONFIG}, netConfig); err != nil { - log.Printf("failed to get network config '%s': %v", names.CLUSTER_CONFIG, err) - r.status.SetDegraded(statusmanager.ProxyConfig, "NetworkConfigError", - fmt.Sprintf("Error getting network config '%s': %v.", names.CLUSTER_CONFIG, err)) - return reconcile.Result{}, nil - } - if err := r.client.Get(context.TODO(), types.NamespacedName{Name: "cluster-config-v1", Namespace: "kube-system"}, - clusterCfgMap); err != nil { - log.Printf("failed to get configmap '%s/%s': %v", clusterCfgMap.Namespace, clusterCfgMap.Name, err) - r.status.SetDegraded(statusmanager.ProxyConfig, "ClusterConfigError", - fmt.Sprintf("Error getting cluster config configmap '%s/%s': %v.", clusterCfgMap.Namespace, - clusterCfgMap.Name, err)) - return reconcile.Result{}, nil - } - if err := r.ValidateProxyConfig(&proxyConfig.Spec); err != nil { - log.Printf("Failed to validate proxy '%s': %v", proxyConfig.Name, err) - r.status.SetDegraded(statusmanager.ProxyConfig, "InvalidProxyConfig", - fmt.Sprintf("The configuration is invalid for proxy '%s' (%v). "+ - "Use 'oc edit proxy.config.openshift.io %s' to fix.", proxyConfig.Name, proxyConfig.Name, err)) + + // Validate the trust bundle configmap. + proxyData, systemData, err := r.validateTrustedCA(trustBundle.Name) + if err != nil { + log.Printf("Failed to validate additional trust bundle configmap '%s/%s': %v", trustBundle.Namespace, + trustBundle.Name, err) + r.status.SetDegraded(statusmanager.ProxyConfig, "TrustBundleValidationFailure", + fmt.Sprintf("Failed to validate additional trust bundle configmap '%s/%s' (%v)", + trustBundle.Namespace, trustBundle.Name, err)) + return reconcile.Result{}, fmt.Errorf("failed to validate additional trust bundle configmap '%s/%s': %v", + trustBundle.Namespace, trustBundle.Name, err) + } + + // Create a configmap containing the merged proxy.trustedCA/system bundles. + trustBundle, err = r.mergeTrustBundles(proxyData, systemData) + if err != nil { + log.Printf("Failed to merge trustedCA and system bundles for proxy '%s': %v", names.PROXY_CONFIG, err) + r.status.SetDegraded(statusmanager.ProxyConfig, "EnsureProxyConfigFailure", + fmt.Sprintf("The configuration is invalid for proxy '%s' (%v). "+ + "Use 'oc edit proxy.config.openshift.io %s' to fix.", names.PROXY_CONFIG, err, names.PROXY_CONFIG)) + return reconcile.Result{}, fmt.Errorf("failed to merge trustedCA and system bundles for proxy '%s': %v", + names.PROXY_CONFIG, err) + } + log.Printf("Reconciling additional trust bundle configmap '%s/%s' complete", request.Namespace, request.Name) + default: + // unknown object + log.Println("Ignoring unknown object, reconciliation will be skipped", "request", request) return reconcile.Result{}, nil } - // Update proxy status. - if err := r.syncProxyStatus(proxyConfig, infraConfig, netConfig, clusterCfgMap); err != nil { - log.Printf("Could not sync proxy '%s' status: %v", proxyConfig.Name, err) - r.status.SetDegraded(statusmanager.ProxyConfig, "StatusError", - fmt.Sprintf("Could not update proxy '%s' status: %v", proxyConfig.Name, err)) - return reconcile.Result{}, err + // Make sure the trust bundle configmap is in sync with the api server. + if err := r.syncTrustedCABundle(trustBundle); err != nil { + log.Printf("Failed to sync additional trust bundle configmap %s/%s: %v", trustBundle.Namespace, + trustBundle.Name, err) + r.status.SetDegraded(statusmanager.ProxyConfig, "TrustBundleSyncFailure", + fmt.Sprintf("Additional trust bundle configmap '%s/%s' not synced (%v)", trustBundle.Namespace, + trustBundle.Name, err)) + return reconcile.Result{}, fmt.Errorf("failed to sync additional trust bundle configmap %s/%s: %v", + trustBundle.Namespace, trustBundle.Name, err) } - log.Printf("Reconciling proxy '%s' complete", request.Name) + // Reconciliation completed, so set status manager accordingly. r.status.SetNotDegraded(statusmanager.ProxyConfig) return reconcile.Result{}, nil @@ -160,3 +287,121 @@ func isSpecNoProxySet(proxyConfig *configv1.ProxySpec) bool { func isSpecReadinessEndpointsSet(proxyConfig *configv1.ProxySpec) bool { return len(proxyConfig.ReadinessEndpoints) > 0 } + +// isSpecTrustedCASet returns true if spec.trustedCA of proxyConfig is set. +func isSpecTrustedCASet(proxyConfig *configv1.ProxySpec) bool { + return len(proxyConfig.TrustedCA.Name) > 0 +} + +// mergeTrustBundles merges the additionalData with systemData +// into a single byte slice, ensures the merged byte slice contains valid +// PEM encoded certificates, embeds the merged byte slice into a ConfigMap +// named "trusted-ca-bundle" in namespace "openshift-config-managed" and +// returns the ConfigMap. It's the caller's responsibility to create the +// ConfigMap in the api server. +func (r *ReconcileProxyConfig) mergeTrustBundles(additionalData, systemData []byte) (*corev1.ConfigMap, error) { + if len(additionalData) == 0 { + return nil, fmt.Errorf("failed to merge ca bundles, additional trust bundle is empty") + } + if len(systemData) == 0 { + return nil, fmt.Errorf("failed to merge ca bundles, system trust bundle is empty") + } + + combinedTrustData := []byte{} + for _, d := range additionalData { + combinedTrustData = append(combinedTrustData, d) + } + for _, d := range systemData { + combinedTrustData = append(combinedTrustData, d) + } + + mergedCfgMap := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: names.TRUSTED_CA_BUNDLE_CONFIGMAP, + Namespace: names.TRUSTED_CA_BUNDLE_CONFIGMAP_NS, + }, + Data: map[string]string{ + names.TRUSTED_CA_BUNDLE_CONFIGMAP_KEY: string(combinedTrustData), + }, + } + if _, _, err := r.validateTrustBundle(mergedCfgMap); err != nil { + return nil, fmt.Errorf("failed to validate merged configmap '%s/%s': %v", mergedCfgMap.Namespace, + mergedCfgMap.Name, err) + } + + return mergedCfgMap, nil +} + +// syncTrustedCABundle checks if ConfigMap named "trusted-ca-bundle" +// in namespace "openshift-config-managed" exists, creating trustedCABundle +// if it doesn't exist or comparing the configmap data values and updating +// trustedCABundle if the values differ. +func (r *ReconcileProxyConfig) syncTrustedCABundle(trustedCABundle *corev1.ConfigMap) error { + currentCfgMap := &corev1.ConfigMap{} + if err := r.client.Get(context.TODO(), names.TrustedCABundleConfigMap(), currentCfgMap); err != nil { + if !apierrors.IsNotFound(err) { + return fmt.Errorf("failed to get trusted CA bundle configmap '%s/%s': %v", + trustedCABundle.Namespace, trustedCABundle.Name, err) + } + if err := r.client.Create(context.TODO(), trustedCABundle); err != nil { + return fmt.Errorf("failed to create trusted CA bundle configmap '%s/%s': %v", + trustedCABundle.Namespace, trustedCABundle.Name, err) + } + } + + if !configMapsEqual(names.TRUSTED_CA_BUNDLE_CONFIGMAP_KEY, currentCfgMap, trustedCABundle) { + if err := r.client.Update(context.TODO(), trustedCABundle); err != nil { + return fmt.Errorf("failed to update trusted CA bundle configmap '%s/%s': %v", + trustedCABundle.Namespace, trustedCABundle.Name, err) + } + } + + return nil +} + +// configMapsEqual compares the data key values between +// a and b ConfigMaps, returning true if they are equal. +func configMapsEqual(key string, a, b *corev1.ConfigMap) bool { + return a.Data[key] == b.Data[key] +} + +// generateSystemTrustBundle creates a ConfigMap object named +// "trusted-ca-bundle" in namespace "openshift-config-managed". The ConfigMap +// consists of a data key named "ca-bundle.crt" that contains a validated +// system trust bundle. It's the caller's responsibility to create the +// ConfigMap in the api server. +func (r *ReconcileProxyConfig) generateSystemTrustBundle() (*corev1.ConfigMap, error) { + bundleData, err := r.validateSystemTrustBundle(names.SYSTEM_TRUST_BUNDLE) + if err != nil { + return nil, fmt.Errorf("failed to validate trust bundle %s: %v", names.SYSTEM_TRUST_BUNDLE, err) + } + + cfgMap := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: names.TRUSTED_CA_BUNDLE_CONFIGMAP, + Namespace: names.TRUSTED_CA_BUNDLE_CONFIGMAP_NS, + }, + Data: map[string]string{ + names.TRUSTED_CA_BUNDLE_CONFIGMAP_KEY: string(bundleData), + }, + } + + return cfgMap, nil +} + +// configMapIsProxyTrustedCA returns an error if cfgMapName does not match the +// ConfigMap name referenced by proxy "cluster" trustedCA. +func (r *ReconcileProxyConfig) configMapIsProxyTrustedCA(cfgMapName string) error { + proxyConfig := &configv1.Proxy{} + err := r.client.Get(context.TODO(), names.Proxy(), proxyConfig) + if err != nil { + return fmt.Errorf("failed to get proxy '%s': %v", names.PROXY_CONFIG, err) + } + + if proxyConfig.Spec.TrustedCA.Name != cfgMapName { + return fmt.Errorf("configmap name '%s' does not match proxy trustedCA name '%s'", cfgMapName, + proxyConfig.Spec.TrustedCA.Name) + } + + return nil +} diff --git a/pkg/controller/proxyconfig/validation.go b/pkg/controller/proxyconfig/validation.go index 855b09ec13..fa9b4084b7 100644 --- a/pkg/controller/proxyconfig/validation.go +++ b/pkg/controller/proxyconfig/validation.go @@ -1,7 +1,11 @@ package proxyconfig import ( + "context" + "crypto/x509" "fmt" + "io/ioutil" + "k8s.io/apimachinery/pkg/types" "net" "net/http" "net/url" @@ -9,23 +13,27 @@ import ( "time" configv1 "github.com/openshift/api/config/v1" + "github.com/openshift/cluster-network-operator/pkg/names" "github.com/openshift/cluster-network-operator/pkg/util/validation" + + corev1 "k8s.io/api/core/v1" ) const ( proxyHTTPScheme = "http" proxyHTTPSScheme = "https" + // noProxyWildcard is the string used to as a wildcard attached to a + // domain suffix in proxy.spec.noProxy to bypass proxying. + noProxyWildcard = "*" // proxyProbeMaxRetries is the number of times to attempt an http GET // to a readinessEndpoints endpoint. proxyProbeMaxRetries = 3 // proxyProbeWaitTime is the time to wait before retrying a failed proxy probe. proxyProbeWaitTime = 1 * time.Second - // noProxyWildcard is the string used to as a wildcard attached to a - // domain suffix in proxy.spec.noProxy to bypass proxying. - noProxyWildcard = "*" ) -// ValidateProxyConfig ensures that proxyConfig is valid. +// ValidateProxyConfig ensures that httpProxy, httpsProxy and +// noProxy fields of proxyConfig are valid. func (r *ReconcileProxyConfig) ValidateProxyConfig(proxyConfig *configv1.ProxySpec) error { if isSpecHTTPProxySet(proxyConfig) { scheme, err := validation.URI(proxyConfig.HTTPProxy) @@ -82,6 +90,75 @@ func (r *ReconcileProxyConfig) ValidateProxyConfig(proxyConfig *configv1.ProxySp return nil } +// validateTrustedCA validates that trustedCA is a valid ConfigMap +// reference and that the ConfigMap contains a valid trust bundle, +// returning the byte slices of the certificate data from the +// validated trustedCA and system trust bundles. +func (r *ReconcileProxyConfig) validateTrustedCA(trustedCA string) ([]byte, []byte, error) { + cfgMap, err := r.validateConfigMapRef(trustedCA) + if err != nil { + return nil, nil, fmt.Errorf("failed to validate configmap reference for proxy trustedCA '%s': %v", + trustedCA, err) + } + + // TODO: Update return values to include []*x509.Certificates for https readinessEndpoint support. + _, bundleData, err := r.validateTrustBundle(cfgMap) + if err != nil { + return nil, nil, fmt.Errorf("failed to validate trust bundle for proxy trustedCA '%s': %v", + trustedCA, err) + } + + systemData, err := r.validateSystemTrustBundle(names.SYSTEM_TRUST_BUNDLE) + if err != nil { + return nil, nil, fmt.Errorf("failed to validate system trust bundle '%s': %v", names.SYSTEM_TRUST_BUNDLE, err) + } + + return bundleData, systemData, nil +} + +// validateConfigMapRef validates that trustedCA is a valid ConfigMap reference, +// returning the validated ConfigMap. +func (r *ReconcileProxyConfig) validateConfigMapRef(trustedCA string) (*corev1.ConfigMap, error) { + cfgMap := &corev1.ConfigMap{} + ns := names.ADDL_TRUST_BUNDLE_CONFIGMAP_NS + if trustedCA == names.TRUSTED_CA_BUNDLE_CONFIGMAP { + ns = names.TRUSTED_CA_BUNDLE_CONFIGMAP_NS + } + if err := r.client.Get(context.TODO(), types.NamespacedName{Namespace: ns, Name: trustedCA}, cfgMap); err != nil { + return nil, fmt.Errorf("failed to get trustedCA configmap for proxy %s: %v", names.PROXY_CONFIG, err) + } + + return cfgMap, nil +} + +// validateTrustBundle is a wrapper for validation.TrustBundleConfigMap(), which +// validates that cfgMap contains a data key named "ca-bundle.crt" and the value +// of the key is one or more valid PEM encoded certificates, returning slices of +// the validated certificates and certificate data. +func (r *ReconcileProxyConfig) validateTrustBundle(cfgMap *corev1.ConfigMap) ([]*x509.Certificate, []byte, error) { + certBundle, bundleData, err := validation.TrustBundleConfigMap(cfgMap) + if err != nil { + return nil, nil, err + } + + return certBundle, bundleData, nil +} + +// validateSystemTrustBundle reads the trustBundle file, ensuring each +// PEM block is type "CERTIFICATE" and the block can be parsed as an +// x509 CA certificate, returning the parsed certificates as a []byte. +func (r *ReconcileProxyConfig) validateSystemTrustBundle(trustBundle string) ([]byte, error) { + bundleData, err := ioutil.ReadFile(trustBundle) + if err != nil { + return nil, err + } + if _, _, err := validation.CertificateData(bundleData); err != nil { + return nil, err + } + + return bundleData, nil +} + // validateHTTPReadinessEndpoint validates an http readinessEndpoint endpoint. func validateHTTPReadinessEndpoint(httpProxy, endpoint string) error { if err := validateHTTPReadinessEndpointWithRetries(httpProxy, endpoint, proxyProbeMaxRetries); err != nil { diff --git a/pkg/names/names.go b/pkg/names/names.go index 96a9fd3406..46e60071a5 100644 --- a/pkg/names/names.go +++ b/pkg/names/names.go @@ -37,6 +37,10 @@ const SERVICE_CA_CONFIGMAP = "openshift-service-ca" // that is used in multus admission controller deployment const MULTUS_VALIDATING_WEBHOOK = "multus.openshift.io" +// ADDL_TRUST_BUNDLE_CONFIGMAP_NS is the namespace for one or more +// ConfigMaps that contain user provided trusted CA bundles. +const ADDL_TRUST_BUNDLE_CONFIGMAP_NS = "openshift-config" + // TRUSTED_CA_BUNDLE_CONFIGMAP_KEY is the name of the data key containing // the PEM encoded trust bundle. const TRUSTED_CA_BUNDLE_CONFIGMAP_KEY = "ca-bundle.crt" @@ -54,6 +58,10 @@ const TRUSTED_CA_BUNDLE_CONFIGMAP_NS = "openshift-config-managed" // determines whether or not to inject the combined ca certificate const TRUSTED_CA_BUNDLE_CONFIGMAP_LABEL = "config.openshift.io/inject-trusted-cabundle" +// SYSTEM_TRUST_BUNDLE is the full path to the file containing +// the system trust bundle. +const SYSTEM_TRUST_BUNDLE = "/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem" + // Proxy returns the namespaced name "cluster" in the // default namespace. func Proxy() types.NamespacedName { @@ -62,9 +70,9 @@ func Proxy() types.NamespacedName { } } -// AddlTrustBundleConfigMapNS returns the namespaced name of the -// namespace containing the user-provided trust bundle ConfigMap. -func AddlTrustBundleConfigMap() types.NamespacedName { +// TrustedCABundleConfigMap returns the namespaced name of the ConfigMap +// openshift-config-managed/trusted-ca-bundle trust bundle. +func TrustedCABundleConfigMap() types.NamespacedName { return types.NamespacedName{ Namespace: TRUSTED_CA_BUNDLE_CONFIGMAP_NS, Name: TRUSTED_CA_BUNDLE_CONFIGMAP,