From e61b30e3a6bdf6e5cca18b747225451e248e4e95 Mon Sep 17 00:00:00 2001 From: staebler Date: Wed, 11 Nov 2020 17:08:15 -0500 Subject: [PATCH] support custom CA bundle for AWS API When a cluster is installed in a AWS C2S region, access to the AWS API requires a custom CA bundle for trust. The custom CA bundle is read from the "ca-bundle.pem" key of the kube-cloud-config ConfigMap in the openshift-config-managed namespace. https://issues.redhat.com/browse/CORS-1584 --- pkg/client/client.go | 36 +++++++++++++++++++ pkg/client/client_test.go | 76 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 112 insertions(+) create mode 100644 pkg/client/client_test.go diff --git a/pkg/client/client.go b/pkg/client/client.go index 59db48066c..4963834c26 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -22,6 +22,7 @@ import ( "fmt" "io/ioutil" "os" + "strings" corev1 "k8s.io/api/core/v1" "sigs.k8s.io/cluster-api-provider-aws/pkg/version" @@ -53,6 +54,13 @@ const ( // globalInfrastuctureName default name for infrastructure object globalInfrastuctureName = "cluster" + + // kubeCloudConfigNamespace is the namespace where the kube cloud config ConfigMap is located + kubeCloudConfigNamespace = "openshift-config-managed" + // kubeCloudConfigName is the name of the kube cloud config ConfigMap + kubeCloudConfigName = "kube-cloud-config" + // cloudCABundleKey is the key in the kube cloud config ConfigMap where the custom CA bundle is located + cloudCABundleKey = "ca-bundle.pem" ) // AwsClientBuilderFuncType is function type for building aws client @@ -176,6 +184,10 @@ func NewClient(ctrlRuntimeClient client.Client, secretName, namespace, region st return nil, err } + if err := useCustomCABundle(&sessionOptions, ctrlRuntimeClient); err != nil { + return nil, fmt.Errorf("failed to set the custom CA bundle: %w", err) + } + // Otherwise default to relying on the IAM role of the masters where the actuator is running: s, err := session.NewSessionWithOptions(sessionOptions) if err != nil { @@ -308,3 +320,27 @@ func newConfigForStaticCreds(accessKey string, accessSecret string) []byte { fmt.Fprintf(buf, "aws_secret_access_key = %s\n", accessSecret) return buf.Bytes() } + +// useCustomCABundle will set up a custom CA bundle in the AWS options if a CA bundle is configured in the +// kube cloud config. +func useCustomCABundle(awsOptions *session.Options, ctrlRuntimeClient client.Client) error { + cm := &corev1.ConfigMap{} + switch err := ctrlRuntimeClient.Get( + context.Background(), + client.ObjectKey{Namespace: kubeCloudConfigNamespace, Name: kubeCloudConfigName}, + cm, + ); { + case apimachineryerrors.IsNotFound(err): + // no cloud config ConfigMap, so no custom CA bundle + return nil + case err != nil: + return fmt.Errorf("failed to get kube-cloud-config ConfigMap: %w", err) + } + caBundle, ok := cm.Data[cloudCABundleKey] + if !ok { + // no "ca-bundle.pem" key in the ConfigMap, so no custom CA bundle + return nil + } + awsOptions.CustomCABundle = strings.NewReader(caBundle) + return nil +} diff --git a/pkg/client/client_test.go b/pkg/client/client_test.go new file mode 100644 index 0000000000..320f89abc7 --- /dev/null +++ b/pkg/client/client_test.go @@ -0,0 +1,76 @@ +package client + +import ( + "io/ioutil" + "testing" + + "github.com/aws/aws-sdk-go/aws/session" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +func TestUseCustomCABundle(t *testing.T) { + cases := []struct { + name string + cm *corev1.ConfigMap + expectedCABundle string + }{ + { + name: "no configmap", + }, + { + name: "no CA bundle in configmap", + cm: &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "openshift-config-managed", + Name: "kube-cloud-config", + }, + Data: map[string]string{ + "other-key": "other-data", + }, + }, + }, + { + name: "custom CA bundle", + cm: &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "openshift-config-managed", + Name: "kube-cloud-config", + }, + Data: map[string]string{ + "ca-bundle.pem": "a custom bundle", + }, + }, + expectedCABundle: "a custom bundle", + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + scheme := runtime.NewScheme() + corev1.AddToScheme(scheme) + resources := []runtime.Object{} + if tc.cm != nil { + resources = append(resources, tc.cm) + } + ctrlRuntimeClient := fake.NewFakeClientWithScheme(scheme, resources...) + awsOptions := &session.Options{} + err := useCustomCABundle(awsOptions, ctrlRuntimeClient) + if err != nil { + t.Fatalf("unexpected error from useCustomCABundle: %v", err) + } + actualCABundle := "" + if awsOptions.CustomCABundle != nil { + bundleBytes, err := ioutil.ReadAll(awsOptions.CustomCABundle) + if err != nil { + t.Fatalf("unexpected error reading bundle: %v", err) + } + actualCABundle = string(bundleBytes) + } + if a, e := actualCABundle, tc.expectedCABundle; a != e { + t.Errorf("unexpected CA bundle: expected=%s; got %s", e, a) + } + }) + } +}