diff --git a/Dockerfile b/Dockerfile index d03331952..3dca4b89d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,7 +5,7 @@ RUN make build FROM registry.ci.openshift.org/ocp/4.10:base COPY --from=builder /go/src/github.com/openshift/cluster-cloud-controller-manager-operator/bin/cluster-controller-manager-operator . -COPY --from=builder /go/src/github.com/openshift/cluster-cloud-controller-manager-operator/bin/cloud-config-sync-controller . +COPY --from=builder /go/src/github.com/openshift/cluster-cloud-controller-manager-operator/bin/config-sync-controllers . COPY --from=builder /go/src/github.com/openshift/cluster-cloud-controller-manager-operator/bin/azure-config-credentials-injector . COPY --from=builder /go/src/github.com/openshift/cluster-cloud-controller-manager-operator/manifests manifests diff --git a/Makefile b/Makefile index 711be21bd..17a1cd95d 100644 --- a/Makefile +++ b/Makefile @@ -26,13 +26,13 @@ unit: hack/unit-tests.sh # Build operator binaries -build: operator cloud-config-sync-controller azure-config-credentials-injector +build: operator config-sync-controllers azure-config-credentials-injector operator: go build -o bin/cluster-controller-manager-operator cmd/cluster-cloud-controller-manager-operator/main.go -cloud-config-sync-controller: - go build -o bin/cloud-config-sync-controller cmd/cloud-config-sync-controller/main.go +config-sync-controllers: + go build -o bin/config-sync-controllers cmd/config-sync-controllers/main.go azure-config-credentials-injector: go build -o bin/azure-config-credentials-injector cmd/azure-config-credentials-injector/main.go diff --git a/cmd/cloud-config-sync-controller/main.go b/cmd/config-sync-controllers/main.go similarity index 89% rename from cmd/cloud-config-sync-controller/main.go rename to cmd/config-sync-controllers/main.go index f1191d2b0..6bf033c6b 100644 --- a/cmd/cloud-config-sync-controller/main.go +++ b/cmd/config-sync-controllers/main.go @@ -92,7 +92,7 @@ func main() { options.BindLeaderElectionFlags(&leaderElectionConfig, pflag.CommandLine) pflag.Parse() - ctrl.SetLogger(klogr.New().WithName("CCMOCloudConfigSyncController")) + ctrl.SetLogger(klogr.New().WithName("CCCMOConfigSyncControllers")) syncPeriod := 10 * time.Minute cacheBuilder := cache.MultiNamespacedCacheBuilder([]string{ @@ -120,12 +120,22 @@ func main() { if err = (&controllers.CloudConfigReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), - Recorder: mgr.GetEventRecorderFor("cloud-controller-manager-operator-config-sync-controller"), + Recorder: mgr.GetEventRecorderFor("cloud-controller-manager-operator-cloud-config-sync-controller"), TargetNamespace: *managedNamespace, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create cloud-config sync controller", "controller", "ClusterOperator") os.Exit(1) } + + if err = (&controllers.TrustedCABundleReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + Recorder: mgr.GetEventRecorderFor("cloud-controller-manager-operator-ca-sync-controller"), + TargetNamespace: *managedNamespace, + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create Trusted CA sync controller", "controller", "ClusterOperator") + os.Exit(1) + } // +kubebuilder:scaffold:builder if err := mgr.AddHealthzCheck("health", healthz.Ping); err != nil { diff --git a/docs/dev/cloud-config-sync.md b/docs/dev/cloud-config-sync.md index 06ab79c0b..4ff804643 100644 --- a/docs/dev/cloud-config-sync.md +++ b/docs/dev/cloud-config-sync.md @@ -10,7 +10,7 @@ There are two places where this ConfigMap is stored on a running cluster at the 1. `kube-cloud-config` ConfigMap in `openshift-config-managed` namespace. 2. ConfigMap with an arbitrary name in `openshift-config` namespace. Such name might be taken from the `cluster` Infrastructure resource spec. -This ConfigMap should be copied from one of the places described above and kept in sync within the CCCMO managed namespace for further mounting onto cloud provider pods. For such purposes `cloud-config-sync-controller` has been introduced as a [separate binary](https://github.com/openshift/cluster-cloud-controller-manager-operator/pull/86) in CCCMO pod. +This ConfigMap should be copied from one of the places described above and kept in sync within the CCCMO managed namespace for further mounting onto cloud provider pods. For such purposes `config-sync-controllers` has been introduced as a [separate binary](https://github.com/openshift/cluster-cloud-controller-manager-operator/pull/86) in CCCMO pod. ## Implementation Description @@ -26,3 +26,7 @@ If `openshift-config-managed/kube-cloud-config` does not exists - the controller ## Links - [library-go implementation](https://github.com/openshift/library-go/blob/master/pkg/operator/configobserver/cloudprovider/observe_cloudprovider.go#L82) - [cluster-config-operator repository](https://github.com/openshift/cluster-config-operator) + +## Notable changes + +- 10.11.2021: `cloud-config-sync-controller` binary was renamed to `config-sync-controllers` \ No newline at end of file diff --git a/docs/dev/trusted_ca_bundle_sync.md b/docs/dev/trusted_ca_bundle_sync.md new file mode 100644 index 000000000..669b90cc8 --- /dev/null +++ b/docs/dev/trusted_ca_bundle_sync.md @@ -0,0 +1,39 @@ +# Managing trusted ca for CCMs + +## Intro + +On some, mainly on-prem platforms, such as OpenStack, vSphere, or Azure Stack, +privately or self-signed SSL certificates might be used for its endpoints. + +In order to be able to communicate with the underlying platform, CA certificates should be +propagated into CCMs pods trust store. + +Openshift documentation [prescribes](https://docs.openshift.com/container-platform/4.8/networking/configuring-a-custom-pki.html) +to use cluster-wide **_Proxy_** object as a common way to configure trusted CA for an entire cluster. + +All necessary logic around `trustedCA` management already exists in Openshift platform, but can not be leveraged by CCM/CCCMO +in its current state due to CCCMO's role in the cluster bootstrap process. +All necessary steps are performing by [cluster-network-operator](https://github.com/openshift/cluster-network-operator/), only once control plane nodes have been initialized by the CCM. +Such node initialization might require communication with the underlying platform (OpenStack, vSphere, ASH), +which would not be successful without configured trust to platform endpoint certificates. + +For solving this 'chicken-and-egg' problem with a minimum amount of risk, a separate `trusted_ca_bundle_controller` was introduced. + +## Implementation Description + +Implementation mostly replicates logic from `cluster-network-operator`. + +The controller has been [introduced](https://github.com/openshift/cluster-cloud-controller-manager-operator/pull/136) as a part of `config-sync-controllers` binary in CCCMO pod and lives as separate control loop along with [cloud-config-sync](cloud-config-sync.md) controller. + +The controller performs sync and merges CA from user defined ConfigMap +(located in `openshift-config` and referenced by cluster scoped Proxy resource) with system bundle. +Merged CA bundle will be written to `ccm-trusted-ca` ConfigMap in `openshift-cloud-controller-manager` namespace and intended to be mounted in all CCM pods. + +Top-level overview: +- In case when Proxy resource contains the `trustedCA` parameter in its spec, user's CA will be taken from a config map with a name specified by `trustedCA` parameter. +- In case if Proxy resource does not contain the `trustedCA` parameter, only the system bundle from the CCCMO pod will be used. +- In case if user defined CA is invalid (PEM can not be parsed, ConfigMap format is unexpected) only the system bundle from the CCCMO pod will be used + +# Links +- [cluster-network-operator implementation](https://github.com/openshift/cluster-network-operator/blob/master/pkg/controller/proxyconfig/controller.go#L91) +- [related openshift documentation](https://docs.openshift.com/container-platform/4.8/networking/configuring-a-custom-pki.html) \ No newline at end of file diff --git a/manifests/0000_26_cloud-controller-manager-operator_11_deployment.yaml b/manifests/0000_26_cloud-controller-manager-operator_11_deployment.yaml index 074219663..5d42f766e 100644 --- a/manifests/0000_26_cloud-controller-manager-operator_11_deployment.yaml +++ b/manifests/0000_26_cloud-controller-manager-operator_11_deployment.yaml @@ -58,7 +58,7 @@ spec: - mountPath: /etc/kubernetes name: host-etc-kube readOnly: true - - name: cloud-config-sync-controller + - name: config-sync-controllers image: quay.io/openshift/origin-cluster-cloud-controller-manager-operator command: - /bin/bash @@ -71,7 +71,7 @@ spec: else URL_ONLY_KUBECONFIG=/etc/kubernetes/kubeconfig fi - exec /cloud-config-sync-controller \ + exec /config-sync-controllers \ --leader-elect=true \ --leader-elect-lease-duration=137s \ --leader-elect-renew-deadline=107s \ @@ -185,7 +185,7 @@ spec: - mountPath: /etc/kubernetes name: host-etc-kube readOnly: true - - name: cloud-config-sync-controller + - name: config-sync-controllers image: quay.io/openshift/origin-cluster-cloud-controller-manager-operator command: - /bin/bash @@ -196,7 +196,7 @@ spec: if [[ -f /etc/kubernetes/apiserver-url.env ]]; then source /etc/kubernetes/apiserver-url.env fi - exec /cloud-config-sync-controller \ + exec /config-sync-controllers \ --leader-elect \ --metrics-bind-address=:8081 \ --health-addr=:9441 diff --git a/pkg/cloud/aws/assets/deployment.yaml b/pkg/cloud/aws/assets/deployment.yaml index c972bbdd4..c6e65606a 100644 --- a/pkg/cloud/aws/assets/deployment.yaml +++ b/pkg/cloud/aws/assets/deployment.yaml @@ -55,6 +55,9 @@ spec: - mountPath: /etc/kubernetes name: host-etc-kube readOnly: true + - name: trusted-ca + mountPath: /etc/pki/ca-trust/extracted/pem + readOnly: true hostNetwork: true nodeSelector: node-role.kubernetes.io/master: "" @@ -85,6 +88,12 @@ spec: key: node.kubernetes.io/not-ready operator: Exists volumes: + - name: trusted-ca + configMap: + name: ccm-trusted-ca + items: + - key: ca-bundle.crt + path: tls-ca-bundle.pem - name: host-etc-kube hostPath: path: /etc/kubernetes diff --git a/pkg/cloud/azure/assets/cloud-controller-manager-deployment.yaml b/pkg/cloud/azure/assets/cloud-controller-manager-deployment.yaml index 849970b8a..d622ea964 100644 --- a/pkg/cloud/azure/assets/cloud-controller-manager-deployment.yaml +++ b/pkg/cloud/azure/assets/cloud-controller-manager-deployment.yaml @@ -93,6 +93,9 @@ spec: - name: config-accm mountPath: /etc/kubernetes-cloud-config readOnly: true + - name: trusted-ca + mountPath: /etc/pki/ca-trust/extracted/pem + readOnly: true volumes: - name: config-accm configMap: @@ -100,6 +103,12 @@ spec: items: - key: cloud.conf path: cloud.conf + - name: trusted-ca + configMap: + name: ccm-trusted-ca + items: + - key: ca-bundle.crt + path: tls-ca-bundle.pem - name: host-etc-kube hostPath: path: /etc/kubernetes diff --git a/pkg/cloud/azure/assets/cloud-node-manager-daemonset.yaml b/pkg/cloud/azure/assets/cloud-node-manager-daemonset.yaml index 51b2c5c83..2e8201111 100644 --- a/pkg/cloud/azure/assets/cloud-node-manager-daemonset.yaml +++ b/pkg/cloud/azure/assets/cloud-node-manager-daemonset.yaml @@ -73,11 +73,20 @@ spec: - name: host-etc-kube mountPath: /etc/kubernetes readOnly: true + - name: trusted-ca + mountPath: /etc/pki/ca-trust/extracted/pem + readOnly: true resources: requests: cpu: 50m memory: 50Mi volumes: + - name: trusted-ca + configMap: + name: ccm-trusted-ca + items: + - key: ca-bundle.crt + path: tls-ca-bundle.pem - name: host-etc-kube hostPath: path: /etc/kubernetes diff --git a/pkg/cloud/azurestack/assets/cloud-controller-manager-deployment.yaml b/pkg/cloud/azurestack/assets/cloud-controller-manager-deployment.yaml index c3280a782..8c33df9f4 100644 --- a/pkg/cloud/azurestack/assets/cloud-controller-manager-deployment.yaml +++ b/pkg/cloud/azurestack/assets/cloud-controller-manager-deployment.yaml @@ -123,6 +123,9 @@ spec: - name: cloud-config mountPath: /etc/cloud-config readOnly: true + - name: trusted-ca + mountPath: /etc/pki/ca-trust/extracted/pem + readOnly: true volumes: - name: config-accm configMap: @@ -136,5 +139,11 @@ spec: hostPath: path: /etc/kubernetes type: Directory + - name: trusted-ca + configMap: + name: ccm-trusted-ca + items: + - key: ca-bundle.crt + path: tls-ca-bundle.pem - name: cloud-config emptyDir: {} diff --git a/pkg/cloud/azurestack/assets/cloud-node-manager-daemonset.yaml b/pkg/cloud/azurestack/assets/cloud-node-manager-daemonset.yaml index 6c8febc13..6b08a0e3e 100644 --- a/pkg/cloud/azurestack/assets/cloud-node-manager-daemonset.yaml +++ b/pkg/cloud/azurestack/assets/cloud-node-manager-daemonset.yaml @@ -111,6 +111,9 @@ spec: - name: cloud-config mountPath: /etc/cloud-config readOnly: true + - name: trusted-ca + mountPath: /etc/pki/ca-trust/extracted/pem + readOnly: true resources: requests: cpu: 50m @@ -128,5 +131,11 @@ spec: path: cloud.conf - key: endpoints path: endpoints.conf + - name: trusted-ca + configMap: + name: ccm-trusted-ca + items: + - key: ca-bundle.crt + path: tls-ca-bundle.pem - name: cloud-config emptyDir: {} diff --git a/pkg/cloud/cloud_test.go b/pkg/cloud/cloud_test.go index a17a03b98..6f8dcc583 100644 --- a/pkg/cloud/cloud_test.go +++ b/pkg/cloud/cloud_test.go @@ -213,6 +213,7 @@ func TestPodSpec(t *testing.T) { checkResourceRunsBeforeCNI(t, podSpec) checkLeaderElection(t, podSpec) checkCloudControllerManagerFlags(t, podSpec) + checkTrustedCAMounted(t, podSpec) } }) } @@ -441,3 +442,22 @@ func checkDeploymentStrategy(t *testing.T, strategy appsv1.DeploymentStrategy) { t.Errorf("Deployment should set strategy type to \"Recreate\"") } } + +func checkTrustedCAMounted(t *testing.T, podSpec corev1.PodSpec) { + trustedCAVolume := corev1.Volume{ + Name: "trusted-ca", + VolumeSource: corev1.VolumeSource{ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{Name: "ccm-trusted-ca"}, + Items: []corev1.KeyToPath{{Key: "ca-bundle.crt", Path: "tls-ca-bundle.pem"}}, + }}, + } + trustedCAVolumeMount := corev1.VolumeMount{ + MountPath: "/etc/pki/ca-trust/extracted/pem", + Name: "trusted-ca", + ReadOnly: true, + } + assert.Contains(t, podSpec.Volumes, trustedCAVolume, "PodSpec %s volumes should contain trusted-ca volume") + for _, c := range podSpec.Containers { + assert.Contains(t, c.VolumeMounts, trustedCAVolumeMount, "Container VolumeMounts should contain trusted ca volume mount") + } +} diff --git a/pkg/cloud/ibm/assets/deployment.yaml b/pkg/cloud/ibm/assets/deployment.yaml index 7cc6e817e..b202d277b 100644 --- a/pkg/cloud/ibm/assets/deployment.yaml +++ b/pkg/cloud/ibm/assets/deployment.yaml @@ -109,7 +109,16 @@ spec: name: cloud-conf - mountPath: /etc/vpc name: ibm-cloud-credentials + - name: trusted-ca + mountPath: /etc/pki/ca-trust/extracted/pem + readOnly: true volumes: + - name: trusted-ca + configMap: + name: ccm-trusted-ca + items: + - key: ca-bundle.crt + path: tls-ca-bundle.pem - name: host-etc-kube hostPath: path: /etc/kubernetes diff --git a/pkg/cloud/openstack/assets/deployment.yaml b/pkg/cloud/openstack/assets/deployment.yaml index 85c051ffc..0c2625bf5 100644 --- a/pkg/cloud/openstack/assets/deployment.yaml +++ b/pkg/cloud/openstack/assets/deployment.yaml @@ -89,6 +89,9 @@ spec: - name: config-occm mountPath: /etc/openstack/config readOnly: true + - name: trusted-ca + mountPath: /etc/pki/ca-trust/extracted/pem + readOnly: true volumes: - name: host-etc-kube hostPath: @@ -100,6 +103,12 @@ spec: items: - key: clouds.yaml path: clouds.yaml + - name: trusted-ca + configMap: + name: ccm-trusted-ca + items: + - key: ca-bundle.crt + path: tls-ca-bundle.pem - name: config-occm configMap: name: openstack-cloud-controller-manager-config diff --git a/pkg/controllers/clusteroperator_controller.go b/pkg/controllers/clusteroperator_controller.go index 00fb2b237..6676456dc 100644 --- a/pkg/controllers/clusteroperator_controller.go +++ b/pkg/controllers/clusteroperator_controller.go @@ -40,7 +40,6 @@ import ( const ( externalFeatureGateName = "cluster" - proxyResourceName = "cluster" kcmResourceName = "cluster" // Condition type for Cloud Controller ownership diff --git a/pkg/controllers/common_consts.go b/pkg/controllers/common_consts.go index 18947fa0a..09ba846b9 100644 --- a/pkg/controllers/common_consts.go +++ b/pkg/controllers/common_consts.go @@ -7,4 +7,6 @@ const ( OpenshiftConfigNamespace = "openshift-config" OpenshiftManagedConfigNamespace = "openshift-config-managed" + + proxyResourceName = "cluster" ) diff --git a/pkg/controllers/fixtures/additional_ca_amazon.pem b/pkg/controllers/fixtures/additional_ca_amazon.pem new file mode 100644 index 000000000..309108068 --- /dev/null +++ b/pkg/controllers/fixtures/additional_ca_amazon.pem @@ -0,0 +1,14 @@ +# Random amazon cert from fedora trust store +# Amazon Root CA 3 +-----BEGIN CERTIFICATE----- +MIIBtjCCAVugAwIBAgITBmyf1XSXNmY/Owua2eiedgPySjAKBggqhkjOPQQDAjA5 +MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24g +Um9vdCBDQSAzMB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkG +A1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJvb3Qg +Q0EgMzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABCmXp8ZBf8ANm+gBG1bG8lKl +ui2yEujSLtf6ycXYqm0fc4E7O5hrOXwzpcVOho6AF2hiRVd9RFgdszflZwjrZt6j +QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSr +ttvXBp43rDCGB5Fwx5zEGbF4wDAKBggqhkjOPQQDAgNJADBGAiEA4IWSoxe3jfkr +BqWTrBqYaGFy+uGh0PsceGCmQ5nFuMQCIQCcAu/xlJyzlvnrxir4tiz+OpAUFteM +YyRIHN8wfdVoOw== +-----END CERTIFICATE----- diff --git a/pkg/controllers/fixtures/additional_ca_ms.pem b/pkg/controllers/fixtures/additional_ca_ms.pem new file mode 100644 index 000000000..4428e1d90 --- /dev/null +++ b/pkg/controllers/fixtures/additional_ca_ms.pem @@ -0,0 +1,17 @@ +# Random ms cert from fedora trust store +# Microsoft ECC Root Certificate Authority 2017 +-----BEGIN CERTIFICATE----- +MIICWTCCAd+gAwIBAgIQZvI9r4fei7FK6gxXMQHC7DAKBggqhkjOPQQDAzBlMQsw +CQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYD +VQQDEy1NaWNyb3NvZnQgRUNDIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIw +MTcwHhcNMTkxMjE4MjMwNjQ1WhcNNDIwNzE4MjMxNjA0WjBlMQswCQYDVQQGEwJV +UzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYDVQQDEy1NaWNy +b3NvZnQgRUNDIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcwdjAQBgcq +hkjOPQIBBgUrgQQAIgNiAATUvD0CQnVBEyPNgASGAlEvaqiBYgtlzPbKnR5vSmZR +ogPZnZH6thaxjG7efM3beaYvzrvOcS/lpaso7GMEZpn4+vKTEAXhgShC48Zo9OYb +hGBKia/teQ87zvH2RPUBeMCjVDBSMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8E +BTADAQH/MB0GA1UdDgQWBBTIy5lycFIM+Oa+sgRXKSrPQhDtNTAQBgkrBgEEAYI3 +FQEEAwIBADAKBggqhkjOPQQDAwNoADBlAjBY8k3qDPlfXu5gKcs68tvWMoQZP3zV +L8KxzJOuULsJMsbG7X7JNpQS5GiFBqIb0C8CMQCZ6Ra0DvpWSNSkMBaReNtUjGUB +iudQZsIxtzm6uBoiB078a1QWIP8rtedMDE2mT3M= +-----END CERTIFICATE----- diff --git a/pkg/controllers/fixtures/trust_bundle_invalid.pem b/pkg/controllers/fixtures/trust_bundle_invalid.pem new file mode 100644 index 000000000..f0a45253d --- /dev/null +++ b/pkg/controllers/fixtures/trust_bundle_invalid.pem @@ -0,0 +1 @@ +this one is broken diff --git a/pkg/controllers/fixtures/trust_bundle_valid.pem b/pkg/controllers/fixtures/trust_bundle_valid.pem new file mode 100644 index 000000000..803c33872 --- /dev/null +++ b/pkg/controllers/fixtures/trust_bundle_valid.pem @@ -0,0 +1,33 @@ +# Random certs from fedora trust store +# GlobalSign ECC Root CA - R4 +-----BEGIN CERTIFICATE----- +MIIB4TCCAYegAwIBAgIRKjikHJYKBN5CsiilC+g0mAIwCgYIKoZIzj0EAwIwUDEk +MCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI0MRMwEQYDVQQKEwpH +bG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWduMB4XDTEyMTExMzAwMDAwMFoX +DTM4MDExOTAzMTQwN1owUDEkMCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBD +QSAtIFI0MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWdu +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEuMZ5049sJQ6fLjkZHAOkrprlOQcJ +FspjsbmG+IpXwVfOQvpzofdlQv8ewQCybnMO/8ch5RikqtlxP6jUuc6MHaNCMEAw +DgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFFSwe61F +uOJAf/sKbvu+M8k8o4TVMAoGCCqGSM49BAMCA0gAMEUCIQDckqGgE6bPA7DmxCGX +kPoUVy0D7O48027KqGx2vKLeuwIgJ6iFJzWbVsaj8kfSt24bAgAXqmemFZHe+pTs +ewv4n4Q= +-----END CERTIFICATE----- + +# COMODO ECC Certification Authority +-----BEGIN CERTIFICATE----- +MIICiTCCAg+gAwIBAgIQH0evqmIAcFBUTAGem2OZKjAKBggqhkjOPQQDAzCBhTEL +MAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE +BxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMT +IkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDgwMzA2MDAw +MDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdy +ZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09N +T0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlv +biBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQDR3svdcmCFYX7deSR +FtSrYpn1PlILBs5BAH+X4QokPB0BBO490o0JlwzgdeT6+3eKKvUDYEs2ixYjFq0J +cfRK9ChQtP6IHG4/bC8vCVlbpVsLM5niwz2J+Wos77LTBumjQjBAMB0GA1UdDgQW +BBR1cacZSBm8nZ3qQUfflMRId5nTeTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/ +BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjEA7wNbeqy3eApyt4jf/7VGFAkK+qDm +fQjGGoe9GKhzvSbKYAydzpmfz1wPMOG+FDHqAjAU9JM8SaczepBGR7NjfRObTrdv +GDeAU/7dIOA1mjbRxwG55tzd8/8dLDoWV9mSOdY= +-----END CERTIFICATE----- diff --git a/pkg/controllers/trusted_ca_bundle_controller.go b/pkg/controllers/trusted_ca_bundle_controller.go new file mode 100644 index 000000000..41d3971b4 --- /dev/null +++ b/pkg/controllers/trusted_ca_bundle_controller.go @@ -0,0 +1,213 @@ +package controllers + +import ( + "context" + "crypto/x509" + "fmt" + "io/ioutil" + + configv1 "github.com/openshift/api/config/v1" + 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" + "k8s.io/client-go/tools/record" + "k8s.io/klog/v2" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/builder" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/predicate" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + "sigs.k8s.io/controller-runtime/pkg/source" + + "github.com/openshift/cluster-cloud-controller-manager-operator/pkg/util" +) + +const ( + trustedCAConfigMapName = "ccm-trusted-ca" + trustedCABundleConfigMapKey = "ca-bundle.crt" + systemTrustBundlePath = "/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem" +) + +type TrustedCABundleReconciler struct { + client.Client + Scheme *runtime.Scheme + Recorder record.EventRecorder + TargetNamespace string + trustBundlePath string +} + +// isSpecTrustedCASet returns true if spec.trustedCA of proxyConfig is set. +func isSpecTrustedCASet(proxyConfig *configv1.ProxySpec) bool { + return len(proxyConfig.TrustedCA.Name) > 0 +} + +func (r *TrustedCABundleReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + klog.Infof("%s emitted event, syncing %s ConfigMap", req, trustedCAConfigMapName) + + proxyConfig := &configv1.Proxy{} + if err := r.Get(ctx, types.NamespacedName{Name: proxyResourceName}, proxyConfig); err != nil { + if apierrors.IsNotFound(err) { + // Request object not found, could have been deleted after reconcile request. + // Return and don't requeue + klog.Infof("proxy not found; reconciliation will be skipped") + return reconcile.Result{}, nil + } + // Error reading the object - requeue the request. + return reconcile.Result{}, fmt.Errorf("failed to get proxy '%s': %v", req.Name, err) + } + + // check if changed config map in 'openshift-config' namespace is proxy trusted ca + if req.Namespace == OpenshiftConfigNamespace { + if proxyConfig.Spec.TrustedCA.Name != req.Name { + klog.Infof("changed config map %s is not a proxy trusted ca, skipping", req) + return reconcile.Result{}, nil + } + } + + systemTrustBundle, err := r.getSystemTrustBundle() + if err != nil { + return reconcile.Result{}, fmt.Errorf("failed to get system trust bundle: %v", err) + } + ccmTrustedConfigMap := r.makeCABundleConfigMap(systemTrustBundle) + + if isSpecTrustedCASet(&proxyConfig.Spec) { + userCABundle, err := r.getUserCABundle(ctx, proxyConfig.Spec.TrustedCA.Name) + if err != nil { + klog.Warningf("failed to get user defined trust bundle, system CA will be used: %v", err) + } else { + mergedTrustBundle, err := r.mergeCABundles(userCABundle, systemTrustBundle) + if err != nil { + return reconcile.Result{}, fmt.Errorf("can not merge system and user trust bundles: %v", err) + } + ccmTrustedConfigMap = r.makeCABundleConfigMap(mergedTrustBundle) + } + } + + if err := r.createOrUpdateConfigMap(ctx, ccmTrustedConfigMap); err != nil { + return reconcile.Result{}, fmt.Errorf("can not update target trust bundle configmap: %v", err) + } + + return ctrl.Result{}, nil +} + +func (r *TrustedCABundleReconciler) getUserCABundle(ctx context.Context, trustedCA string) ([]byte, error) { + cfgMap, err := r.getUserCABundleConfigMap(ctx, trustedCA) + if err != nil { + return nil, fmt.Errorf("failed to validate configmap reference for proxy trustedCA '%s': %v", + trustedCA, err) + } + + _, bundleData, err := r.getCABundleConfigMapData(cfgMap) + if err != nil { + return nil, fmt.Errorf("failed to validate trust bundle for proxy trustedCA '%s': %v", + trustedCA, err) + } + + return bundleData, nil +} + +func (r *TrustedCABundleReconciler) getUserCABundleConfigMap(ctx context.Context, trustedCA string) (*corev1.ConfigMap, error) { + cfgMap := &corev1.ConfigMap{} + if err := r.Get(ctx, types.NamespacedName{Namespace: OpenshiftConfigNamespace, Name: trustedCA}, cfgMap); err != nil { + return nil, fmt.Errorf("failed to get trustedCA configmap for proxy %s: %v", proxyResourceName, err) + } + + return cfgMap, nil +} + +func (r *TrustedCABundleReconciler) getCABundleConfigMapData(cfgMap *corev1.ConfigMap) ([]*x509.Certificate, []byte, error) { + certBundle, bundleData, err := util.TrustBundleConfigMap(cfgMap) + if err != nil { + return nil, nil, err + } + + return certBundle, bundleData, nil +} + +func (r *TrustedCABundleReconciler) makeCABundleConfigMap(trustBundle []byte) *corev1.ConfigMap { + return &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: trustedCAConfigMapName, + Namespace: r.TargetNamespace, + }, + Data: map[string]string{ + trustedCABundleConfigMapKey: string(trustBundle), + }, + } +} + +func (r *TrustedCABundleReconciler) createOrUpdateConfigMap(ctx context.Context, cm *corev1.ConfigMap) error { + // check if target config exists, create if not + err := r.Get(ctx, client.ObjectKeyFromObject(cm), &corev1.ConfigMap{}) + if err != nil && apierrors.IsNotFound(err) { + return r.Create(ctx, cm) + } else if err != nil { + return err + } + + return r.Update(ctx, cm) +} + +// for test purposes only, normally it returns value from 'trustBundlePath' constant in this module +func (r *TrustedCABundleReconciler) getTrustBundlePath() string { + if r.trustBundlePath != "" { + return r.trustBundlePath + } + return systemTrustBundlePath +} + +func (r *TrustedCABundleReconciler) getSystemTrustBundle() ([]byte, error) { + bundleData, err := ioutil.ReadFile(r.getTrustBundlePath()) + if err != nil { + return nil, err + } + _, err = util.CertificateData(bundleData) + if err != nil { + return nil, err + } + + return bundleData, nil +} + +func (r *TrustedCABundleReconciler) mergeCABundles(additionalData, systemData []byte) ([]byte, 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{} + combinedTrustData = append(combinedTrustData, additionalData...) + combinedTrustData = append(combinedTrustData, []byte("\n")...) + combinedTrustData = append(combinedTrustData, systemData...) + + if _, err := util.CertificateData(combinedTrustData); err != nil { + return nil, err + } + + return combinedTrustData, nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *TrustedCABundleReconciler) SetupWithManager(mgr ctrl.Manager) error { + build := ctrl.NewControllerManagedBy(mgr). + For( + &corev1.ConfigMap{}, + builder.WithPredicates( + predicate.Or( + openshiftConfigNamespacedPredicate(), + ccmTrustedCABundleConfigMapPredicates(r.TargetNamespace), + ), + ), + ). + Watches( + &source.Kind{Type: &configv1.Proxy{}}, + &handler.EnqueueRequestForObject{}, + ) + + return build.Complete(r) +} diff --git a/pkg/controllers/trusted_ca_bundle_controller_test.go b/pkg/controllers/trusted_ca_bundle_controller_test.go new file mode 100644 index 000000000..df3ebbe88 --- /dev/null +++ b/pkg/controllers/trusted_ca_bundle_controller_test.go @@ -0,0 +1,261 @@ +package controllers + +import ( + "context" + "io/ioutil" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + v1 "github.com/openshift/api/config/v1" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/tools/record" + "k8s.io/utils/pointer" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/manager" + + "github.com/openshift/cluster-cloud-controller-manager-operator/pkg/util" +) + +const ( + systemCAValid = "./fixtures/trust_bundle_valid.pem" + systemCAInvalid = "./fixtures/trust_bundle_invalid.pem" + + additionalAmazonCAPemPath = "./fixtures/additional_ca_amazon.pem" + additionalMsCAPemPath = "./fixtures/additional_ca_ms.pem" + + // https://docs.openshift.com/container-platform/4.8/networking/configuring-a-custom-pki.html#nw-proxy-configure-object_configuring-a-custom-pki + additionalCAConfigMapName = "user-ca-bundle" + additionalCAConfigMapKey = trustedCABundleConfigMapKey +) + +func makeValidUserCAConfigMap(pemPath string) (*corev1.ConfigMap, error) { + testTrustBundle, err := ioutil.ReadFile(pemPath) + if err != nil { + return nil, err + } + return &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: additionalCAConfigMapName, + Namespace: OpenshiftConfigNamespace, + }, + Data: map[string]string{ + additionalCAConfigMapKey: string(testTrustBundle), + }, + }, nil +} + +func makeProxyResource() *v1.Proxy { + return &v1.Proxy{ + ObjectMeta: metav1.ObjectMeta{Name: proxyResourceName}, + Spec: v1.ProxySpec{ + TrustedCA: v1.ConfigMapNameReference{Name: additionalCAConfigMapName}, + }, + } +} + +var _ = Describe("Trusted CA bundle sync controller", func() { + var rec *record.FakeRecorder + + var mgrCtxCancel context.CancelFunc + var mgrStopped chan struct{} + ctx := context.Background() + + targetNamespaceName := testManagedNamespace + + var reconciler *TrustedCABundleReconciler + + var proxyResource *v1.Proxy + var additionalCAConfigMap *corev1.ConfigMap + + mergedCAObjectKey := client.ObjectKey{Namespace: targetNamespaceName, Name: trustedCAConfigMapName} + + BeforeEach(func() { + By("Setting up a new manager") + mgr, err := manager.New(cfg, manager.Options{MetricsBindAddress: "0"}) + Expect(err).NotTo(HaveOccurred()) + + reconciler = &TrustedCABundleReconciler{ + Client: cl, + Scheme: scheme.Scheme, + Recorder: rec, + TargetNamespace: targetNamespaceName, + trustBundlePath: systemCAValid, + } + Expect(reconciler.SetupWithManager(mgr)).To(Succeed()) + + By("Creating needed ConfigMaps and Update Proxy") + proxyResource = makeProxyResource() + additionalCAConfigMap, err = makeValidUserCAConfigMap(additionalAmazonCAPemPath) + Expect(err).NotTo(HaveOccurred()) + Expect(cl.Create(ctx, proxyResource)).To(Succeed()) + Expect(cl.Create(ctx, additionalCAConfigMap)).To(Succeed()) + + var mgrCtx context.Context + mgrCtx, mgrCtxCancel = context.WithCancel(ctx) + mgrStopped = make(chan struct{}) + + By("Starting the manager") + go func() { + defer GinkgoRecover() + defer close(mgrStopped) + + Expect(mgr.Start(mgrCtx)).To(Succeed()) + }() + }) + + AfterEach(func() { + By("Closing the manager") + mgrCtxCancel() + Eventually(mgrStopped, timeout).Should(BeClosed()) + + By("Cleanup resources") + deleteOptions := &client.DeleteOptions{ + GracePeriodSeconds: pointer.Int64(0), + } + + if proxyResource != nil { + Expect(cl.Delete(ctx, proxyResource, deleteOptions)).To(Succeed()) + Eventually( + apierrors.IsNotFound(cl.Get(ctx, client.ObjectKeyFromObject(proxyResource), &v1.Proxy{})), + ).Should(BeTrue()) + } + + if additionalCAConfigMap != nil { + Expect(cl.Delete(ctx, additionalCAConfigMap, deleteOptions)).To(Succeed()) + Eventually( + apierrors.IsNotFound(cl.Get(ctx, client.ObjectKeyFromObject(additionalCAConfigMap), &corev1.ConfigMap{})), + ).Should(BeTrue()) + } + + proxyResource = nil + additionalCAConfigMap = nil + }) + + It("CA should be synced and merged up after first reconcile", func() { + Eventually(func() { + mergedTrustedCA := &corev1.ConfigMap{} + Expect(cl.Get(ctx, mergedCAObjectKey, mergedTrustedCA)).To(Succeed()) + certs, err := util.CertificateData([]byte(mergedTrustedCA.Data[additionalCAConfigMapKey])) + Expect(err).NotTo(HaveOccurred()) + Expect(len(certs)).Should(BeEquivalentTo(3)) + Expect(certs[0].Issuer.Organization[0]).Should(BeEquivalentTo("Amazon")) + }).Should(Succeed()) + }) + + It("ca bundle should be synced up if own one was deleted or changed", func() { + mergedTrustedCA := &corev1.ConfigMap{} + Expect(cl.Get(ctx, mergedCAObjectKey, mergedTrustedCA)).Should(Succeed()) + certs, err := util.CertificateData([]byte(mergedTrustedCA.Data[additionalCAConfigMapKey])) + Expect(err).NotTo(HaveOccurred()) + Expect(len(certs)).Should(BeEquivalentTo(3)) + Expect(certs[0].Issuer.Organization[0]).Should(BeEquivalentTo("Amazon")) + + mergedTrustedCA.Data = map[string]string{additionalCAConfigMapKey: "KEKEKE"} + Expect(cl.Update(ctx, mergedTrustedCA)).To(Succeed()) + Eventually(func() { + Expect(cl.Get(ctx, mergedCAObjectKey, mergedTrustedCA)).To(Succeed()) + certs, err := util.CertificateData([]byte(mergedTrustedCA.Data[additionalCAConfigMapKey])) + Expect(err).NotTo(HaveOccurred()) + Expect(len(certs)).Should(BeEquivalentTo(3)) + Expect(certs[0].Issuer.Organization[0]).Should(BeEquivalentTo("Amazon")) + }).Should(Succeed()) + + Expect(cl.Delete(ctx, mergedTrustedCA)).To(Succeed()) + Eventually(func() { + Expect(cl.Get(ctx, mergedCAObjectKey, mergedTrustedCA)).To(Succeed()) + certs, err := util.CertificateData([]byte(mergedTrustedCA.Data[additionalCAConfigMapKey])) + Expect(err).NotTo(HaveOccurred()) + Expect(len(certs)).Should(BeEquivalentTo(3)) + Expect(certs[0].Issuer.Organization[0]).Should(BeEquivalentTo("Amazon")) + }).Should(Succeed()) + }) + + It("ca bundle should be synced up if user one in openshift-config was changed", func() { + mergedTrustedCA := &corev1.ConfigMap{} + Expect(cl.Get(ctx, mergedCAObjectKey, mergedTrustedCA)).Should(Succeed()) + certs, err := util.CertificateData([]byte(mergedTrustedCA.Data[additionalCAConfigMapKey])) + Expect(err).NotTo(HaveOccurred()) + Expect(len(certs)).Should(BeEquivalentTo(3)) + Expect(certs[0].Issuer.Organization[0]).Should(BeEquivalentTo("Amazon")) + + msCA, err := ioutil.ReadFile(additionalMsCAPemPath) + Expect(err).To(Succeed()) + additionalCAConfigMap.Data = map[string]string{additionalCAConfigMapKey: string(msCA)} + Expect(cl.Update(ctx, additionalCAConfigMap)).To(Succeed()) + Eventually(func() { + Expect(cl.Get(ctx, mergedCAObjectKey, mergedTrustedCA)).To(Succeed()) + certs, err := util.CertificateData([]byte(mergedTrustedCA.Data[additionalCAConfigMapKey])) + Expect(err).NotTo(HaveOccurred()) + Expect(len(certs)).Should(BeEquivalentTo(3)) + Expect(certs[0].Issuer.Organization[0]).Should(BeEquivalentTo("Microsoft Corporation")) + }).Should(Succeed()) + }) + + It("ca bundle should be set to system one if additional ca bundle is invalid PEM", func() { + additionalCAConfigMap.Data = map[string]string{additionalCAConfigMapKey: "kekekeke"} + Expect(cl.Update(ctx, additionalCAConfigMap)).To(Succeed()) + Eventually(func() { + mergedTrustedCA := &corev1.ConfigMap{} + Expect(cl.Get(ctx, mergedCAObjectKey, mergedTrustedCA)).To(Succeed()) + certs, err := util.CertificateData([]byte(mergedTrustedCA.Data[additionalCAConfigMapKey])) + Expect(err).NotTo(HaveOccurred()) + Expect(len(certs)).Should(BeEquivalentTo(2)) + Expect(certs[0].Issuer.Organization[0]).Should(BeEquivalentTo("GlobalSign")) + }).Should(Succeed()) + }) + + It("ca bundle should be set to system one if additional ca bundle has invalid key", func() { + additionalCAConfigMap.Data = map[string]string{"foo": "bar"} + Expect(cl.Update(ctx, additionalCAConfigMap)).To(Succeed()) + Eventually(func() { + mergedTrustedCA := &corev1.ConfigMap{} + Expect(cl.Get(ctx, mergedCAObjectKey, mergedTrustedCA)).To(Succeed()) + certs, err := util.CertificateData([]byte(mergedTrustedCA.Data[additionalCAConfigMapKey])) + Expect(err).NotTo(HaveOccurred()) + Expect(len(certs)).Should(BeEquivalentTo(2)) + Expect(certs[0].Issuer.Organization[0]).Should(BeEquivalentTo("GlobalSign")) + }).Should(Succeed()) + }) + + It("ca bundle should be set to system one if proxy points nowhere", func() { + proxyResource.Spec.TrustedCA.Name = "SomewhereNowhere" + Expect(cl.Update(ctx, proxyResource)).To(Succeed()) + Eventually(func() { + mergedTrustedCA := &corev1.ConfigMap{} + Expect(cl.Get(ctx, mergedCAObjectKey, mergedTrustedCA)).To(Succeed()) + certs, err := util.CertificateData([]byte(mergedTrustedCA.Data[additionalCAConfigMapKey])) + Expect(err).NotTo(HaveOccurred()) + Expect(len(certs)).Should(BeEquivalentTo(2)) + Expect(certs[0].Issuer.Organization[0]).Should(BeEquivalentTo("GlobalSign")) + }).Should(Succeed()) + }) +}) + +var _ = Describe("Trusted CA reconciler methods", func() { + It("Get system CA should be fine if bundle is valid", func() { + reconciler := &TrustedCABundleReconciler{ + trustBundlePath: systemCAValid, + } + _, err := reconciler.getSystemTrustBundle() + Expect(err).NotTo(HaveOccurred()) + }) + + It("Get system CA should return err if bundle is not valid", func() { + reconciler := &TrustedCABundleReconciler{ + trustBundlePath: systemCAInvalid, + } + _, err := reconciler.getSystemTrustBundle() + Expect(err.Error()).Should(BeEquivalentTo("failed to parse certificate PEM")) + }) + + It("Get system CA should return err if bundle not found", func() { + reconciler := &TrustedCABundleReconciler{ + trustBundlePath: "/broken/ca/path.pem", + } + _, err := reconciler.getSystemTrustBundle() + Expect(err.Error()).Should(BeEquivalentTo("open /broken/ca/path.pem: no such file or directory")) + }) +}) diff --git a/pkg/controllers/watch_predicates.go b/pkg/controllers/watch_predicates.go index f1b5af69c..bd1fbf909 100644 --- a/pkg/controllers/watch_predicates.go +++ b/pkg/controllers/watch_predicates.go @@ -93,3 +93,30 @@ func openshiftCloudConfigMapPredicates() predicate.Funcs { DeleteFunc: func(e event.DeleteEvent) bool { return isCloudConfigMap(e.Object) }, } } + +func ccmTrustedCABundleConfigMapPredicates(targetNamespace string) predicate.Funcs { + isTrustedCaConfigMap := func(obj runtime.Object) bool { + configMap, ok := obj.(*corev1.ConfigMap) + return ok && configMap.GetNamespace() == targetNamespace && configMap.GetName() == trustedCAConfigMapName + } + return predicate.Funcs{ + CreateFunc: func(e event.CreateEvent) bool { return isTrustedCaConfigMap(e.Object) }, + UpdateFunc: func(e event.UpdateEvent) bool { return isTrustedCaConfigMap(e.ObjectNew) }, + GenericFunc: func(e event.GenericEvent) bool { return isTrustedCaConfigMap(e.Object) }, + DeleteFunc: func(e event.DeleteEvent) bool { return isTrustedCaConfigMap(e.Object) }, + } +} + +// Config maps from 'openshift-config' namespace +func openshiftConfigNamespacedPredicate() predicate.Funcs { + isTrustedCaConfigMap := func(obj runtime.Object) bool { + configMap, ok := obj.(*corev1.ConfigMap) + return ok && configMap.GetNamespace() == OpenshiftConfigNamespace + } + return predicate.Funcs{ + CreateFunc: func(e event.CreateEvent) bool { return isTrustedCaConfigMap(e.Object) }, + UpdateFunc: func(e event.UpdateEvent) bool { return isTrustedCaConfigMap(e.ObjectNew) }, + GenericFunc: func(e event.GenericEvent) bool { return isTrustedCaConfigMap(e.Object) }, + DeleteFunc: func(e event.DeleteEvent) bool { return isTrustedCaConfigMap(e.Object) }, + } +} diff --git a/pkg/util/trustbundle.go b/pkg/util/trustbundle.go new file mode 100644 index 000000000..00867acf9 --- /dev/null +++ b/pkg/util/trustbundle.go @@ -0,0 +1,61 @@ +package util + +import ( + "crypto/x509" + "encoding/pem" + "fmt" + + corev1 "k8s.io/api/core/v1" +) + +const ( + // certPEMBlock is the type taken from the preamble of a PEM-encoded structure. + certPEMBlock = "CERTIFICATE" + trustedCABundleConfigMapKey = "ca-bundle.crt" +) + +// TrustBundleConfigMap validates that ConfigMap contains a +// trust bundle named "ca-bundle.crt" and that "ca-bundle.crt" +// contains one or more valid PEM encoded certificates, returning +// a byte slice of "ca-bundle.crt" contents upon success. +func TrustBundleConfigMap(cfgMap *corev1.ConfigMap) ([]*x509.Certificate, []byte, error) { + if _, ok := cfgMap.Data[trustedCABundleConfigMapKey]; !ok { + return nil, nil, fmt.Errorf("ConfigMap %q is missing %q", cfgMap.Name, trustedCABundleConfigMapKey) + } + trustBundleData := []byte(cfgMap.Data[trustedCABundleConfigMapKey]) + if len(trustBundleData) == 0 { + return nil, nil, fmt.Errorf("data key %q is empty from ConfigMap %q", trustedCABundleConfigMapKey, cfgMap.Name) + } + certBundle, err := CertificateData(trustBundleData) + if err != nil { + return nil, nil, fmt.Errorf("failed parsing certificate data from ConfigMap %q: %v", cfgMap.Name, err) + } + + return certBundle, trustBundleData, nil +} + +// CertificateData decodes certData, ensuring each PEM block is type +// "CERTIFICATE" and the block can be parsed as an x509 certificate, +// returning slice of parsed certificates +func CertificateData(certData []byte) ([]*x509.Certificate, error) { + certBundle := []*x509.Certificate{} + for len(certData) != 0 { + var block *pem.Block + block, certData = pem.Decode(certData) + if block == nil { + return nil, fmt.Errorf("failed to parse certificate PEM") + } + if block.Type != certPEMBlock { + return nil, fmt.Errorf("invalid certificate PEM, must be of type %q", certPEMBlock) + + } + + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return nil, fmt.Errorf("failed to parse certificate: %v", err) + } + certBundle = append(certBundle, cert) + } + + return certBundle, nil +}