diff --git a/Dockerfile b/Dockerfile index 8f1344877c..8b865f8824 100644 --- a/Dockerfile +++ b/Dockerfile @@ -60,7 +60,8 @@ RUN if [ "${CRYPTO_LIB}" ]; then assert-fips.sh manager; fi #RUN scan-govulncheck.sh manager ENTRYPOINT [ "/start.sh", "/workspace/manager" ] # Copy the controller-manager into a thin image -FROM gcr.io/distroless/static:nonroot +# FROM gcr.io/distroless/static:nonroot +FROM alpine:3.14 WORKDIR / COPY --from=builder /workspace/manager . # Use uid of nonroot user (65532) because kubernetes expects numeric user when applying pod security policies diff --git a/config/default/credentials.yaml b/config/default/credentials.yaml index d575dfb2ad..bc3be9a253 100644 --- a/config/default/credentials.yaml +++ b/config/default/credentials.yaml @@ -6,12 +6,6 @@ metadata: type: Opaque data: credentials: ${AWS_B64ENCODED_CREDENTIALS} ---- -apiVersion: v1 -kind: Secret -metadata: - name: manager-bootstrap-ca-bundle - namespace: system -type: Opaque -data: - credentials: ${AWS_B64ENCODED_CABUNDLE} + config: ${AWS_B64ENCODED_CONFIG} + ca_bundle: ${AWS_B64ENCODED_CABUNDLE} + permissions_boundary: ${AWS_B64ENCODED_PERMISSIONS_BOUNDARY} # - we should move to https://github.com/kubernetes-sigs/cluster-api-provider-aws/pull/5286 diff --git a/config/default/manager_credentials_patch.yaml b/config/default/manager_credentials_patch.yaml index 7e745bb7e2..5f8836398c 100644 --- a/config/default/manager_credentials_patch.yaml +++ b/config/default/manager_credentials_patch.yaml @@ -24,6 +24,3 @@ spec: - name: credentials secret: secretName: manager-bootstrap-credentials - - name: ca-bundle - secret: - secretName: manager-bootstrap-ca-bundle diff --git a/exp/api/v1alpha3/conversion_test.go b/exp/api/v1alpha3/conversion_test.go index a6447ca8f5..318d519eca 100644 --- a/exp/api/v1alpha3/conversion_test.go +++ b/exp/api/v1alpha3/conversion_test.go @@ -18,13 +18,12 @@ package v1alpha3 import ( "testing" - + fuzz "github.com/google/gofuzz" . "github.com/onsi/gomega" "k8s.io/apimachinery/pkg/api/apitesting/fuzzer" "k8s.io/apimachinery/pkg/runtime" runtimeserializer "k8s.io/apimachinery/pkg/runtime/serializer" - "sigs.k8s.io/cluster-api-provider-aws/exp/api/v1beta1" utilconversion "sigs.k8s.io/cluster-api/util/conversion" ) diff --git a/pkg/cloud/services/ec2/ami.go b/pkg/cloud/services/ec2/ami.go index 760c7be025..cffe9735a6 100644 --- a/pkg/cloud/services/ec2/ami.go +++ b/pkg/cloud/services/ec2/ami.go @@ -221,9 +221,9 @@ func (s *Service) defaultBastionAMILookup(region string) (string, error) { if strings.Contains(region, defaultUsGovPartitionName) { filter := &ec2.Filter{ - Name: aws.String("owner-id"), - Values: []*string{aws.String(ubuntuOwnerIDUsGov)}, - } + Name: aws.String("owner-id"), + Values: []*string{aws.String(ubuntuOwnerIDUsGov)}, + } describeImageInput.Filters = append(describeImageInput.Filters, filter) } else { filter := &ec2.Filter{ diff --git a/pkg/cloud/services/ec2/bastion.go b/pkg/cloud/services/ec2/bastion.go index 12989e33f4..121b75890d 100644 --- a/pkg/cloud/services/ec2/bastion.go +++ b/pkg/cloud/services/ec2/bastion.go @@ -35,7 +35,7 @@ import ( ) const ( - defaultSSHKeyName = "default" + defaultSSHKeyName = "default" defaultUsGovPartitionName = "us-gov" ) diff --git a/pkg/cloud/services/eks/iam/iam.go b/pkg/cloud/services/eks/iam/iam.go index 12c77744e9..505d6707fe 100644 --- a/pkg/cloud/services/eks/iam/iam.go +++ b/pkg/cloud/services/eks/iam/iam.go @@ -17,10 +17,11 @@ limitations under the License. package iam import ( + "context" "crypto/sha1" "encoding/hex" "encoding/json" - "net/http" + "fmt" "net/url" "strings" @@ -36,6 +37,7 @@ import ( infrav1 "sigs.k8s.io/cluster-api-provider-aws/api/v1beta1" "sigs.k8s.io/cluster-api-provider-aws/cmd/clusterawsadm/converters" iamv1 "sigs.k8s.io/cluster-api-provider-aws/iam/api/v1beta1" + "sigs.k8s.io/cluster-api-provider-aws/pkg/utils" ) const ( @@ -413,6 +415,11 @@ func findStringInSlice(slice []*string, toFind string) bool { return false } +// "identity": { +// "oidc": { +// "issuer": "https://oidc.eks.us-east-1.amazonaws.com/id/AFDC82E69B82D086C19F12F5CB446A71" +// } +// }, const stsAWSAudience = "sts.amazonaws.com" // CreateOIDCProvider will create an OIDC provider. @@ -427,7 +434,7 @@ func (s *IAMService) CreateOIDCProvider(cluster *eks.Cluster) (string, error) { thumbprint, err := fetchRootCAThumbprint(issuerURL.String()) if err != nil { - return "", err + return "", errors.Errorf("error fetching root CA thumbprint: %v", err) } input := iam.CreateOpenIDConnectProviderInput{ ClientIDList: aws.StringSlice([]string{stsAWSAudience}), @@ -469,7 +476,17 @@ func (s *IAMService) getOIDCProviderARN(issuer string) (string, error) { } func fetchRootCAThumbprint(issuerURL string) (string, error) { - response, err := http.Get(issuerURL) + httpHandler, err := utils.NewHttpHandlerWithAWSCABundle(context.TODO(), "") + if err != nil { + return "", fmt.Errorf("error creating http handler: %w", err) + } + + httpClient, err := httpHandler.GetHttpClient() + if err != nil { + return "", fmt.Errorf("error creating http client: %w", err) + } + + response, err := httpClient.Get(issuerURL) if err != nil { return "", err } diff --git a/pkg/cloud/services/secretsmanager/secret_fetch_script.go b/pkg/cloud/services/secretsmanager/secret_fetch_script.go index 5e7bc5657d..90dbfab4d8 100644 --- a/pkg/cloud/services/secretsmanager/secret_fetch_script.go +++ b/pkg/cloud/services/secretsmanager/secret_fetch_script.go @@ -175,6 +175,13 @@ log::info "aws.cluster.x-k8s.io encrypted cloud-init script $0 started" log::info "secret prefix: ${SECRET_PREFIX}" log::info "secret count: ${CHUNKS}" +{{if .B64CABundle}} +log::info "writing AWS CA bundle to /etc/ssl/certs/aws-ca-bundle.crt.pem" +echo "{{.B64CABundle}}" | base64 -d > /etc/ssl/certs/aws-ca-bundle.crt.pem +update-ca-certificates +export AWS_CA_BUNDLE=/etc/ssl/certs/aws-ca-bundle.crt.pem +{{end}} + if test -f "${FILE}"; then log::info "encrypted userdata already written to disk" log::success_exit diff --git a/pkg/cloud/services/ssm/secret_fetch_script.go b/pkg/cloud/services/ssm/secret_fetch_script.go index 0337875c9f..86387ee9a7 100644 --- a/pkg/cloud/services/ssm/secret_fetch_script.go +++ b/pkg/cloud/services/ssm/secret_fetch_script.go @@ -175,6 +175,13 @@ log::info "aws.cluster.x-k8s.io encrypted cloud-init script $0 started" log::info "secret prefix: ${SECRET_PREFIX}" log::info "secret count: ${CHUNKS}" +{{if .B64CABundle}} +log::info "writing AWS CA bundle to /etc/ssl/certs/aws-ca-bundle.crt.pem" +echo "{{.B64CABundle}}" | base64 -d > /etc/ssl/certs/aws-ca-bundle.crt.pem +update-ca-certificates +export AWS_CA_BUNDLE=/etc/ssl/certs/aws-ca-bundle.crt.pem +{{end}} + if test -f "${FILE}"; then log::info "encrypted userdata already written to disk" log::success_exit diff --git a/pkg/internal/mime/mime.go b/pkg/internal/mime/mime.go index 1324482f9f..89463a6a06 100644 --- a/pkg/internal/mime/mime.go +++ b/pkg/internal/mime/mime.go @@ -18,11 +18,15 @@ package mime import ( "bytes" + "context" + "encoding/base64" "fmt" "html/template" "mime/multipart" "net/textproto" "strings" + + "sigs.k8s.io/cluster-api-provider-aws/pkg/utils" ) const ( @@ -50,6 +54,7 @@ type scriptVariables struct { Chunks int32 Region string Endpoint string + B64CABundle string } // GenerateInitDocument renders a given template, applies MIME properties @@ -73,6 +78,14 @@ func GenerateInitDocument(secretPrefix string, chunks int32, region string, endp Endpoint: endpoint, } + caBundle, err := utils.GetAWSCABundle(context.TODO(), "") + if err != nil { + return []byte{}, fmt.Errorf("failed to get AWS CA bundle: %w", err) + } + if caBundle != nil { + scriptVariables.B64CABundle = base64.StdEncoding.EncodeToString(caBundle) + } + var scriptBuf bytes.Buffer if err := secretFetchTemplate.Execute(&scriptBuf, scriptVariables); err != nil { return []byte{}, err diff --git a/pkg/utils/ca_bundle.go b/pkg/utils/ca_bundle.go new file mode 100644 index 0000000000..f767f05367 --- /dev/null +++ b/pkg/utils/ca_bundle.go @@ -0,0 +1,58 @@ +package utils + +import ( + "context" + "fmt" + "gopkg.in/ini.v1" + "os" +) + +const ( + defaultProfile = "default" +) + +func GetAWSCABundlePath(ctx context.Context, profile string) (string, error) { + configFile := os.Getenv("AWS_CONFIG_FILE") + if configFile == "" { + return "", nil + } + + cfg, err := ini.Load(configFile) + if err != nil { + return "", fmt.Errorf("failed to load AWS config: %w", err) + } + + if profile == "" { + profile = defaultProfile + } + + section, err := cfg.GetSection(profile) + if err != nil { + return "", fmt.Errorf("failed to get profile %s: %w", profile, err) + } + + cabundlePath := section.Key("ca_bundle") + if cabundlePath == nil || cabundlePath.String() == "" { + return "", nil + } + + return cabundlePath.String(), nil +} + +func GetAWSCABundle(ctx context.Context, profile string) ([]byte, error) { + cabundlePath, err := GetAWSCABundlePath(ctx, profile) + if err != nil { + return nil, fmt.Errorf("failed to get AWS CA bundle path: %w", err) + } + + if cabundlePath == "" { + return nil, nil + } + + cabundle, err := os.ReadFile(cabundlePath) + if err != nil { + return nil, fmt.Errorf("failed to read ca bundle file: %w", err) + } + + return cabundle, nil +} diff --git a/pkg/utils/ca_bundle_test.go b/pkg/utils/ca_bundle_test.go new file mode 100644 index 0000000000..bc6f275543 --- /dev/null +++ b/pkg/utils/ca_bundle_test.go @@ -0,0 +1,121 @@ +package utils + +import ( + "context" + "os" + "testing" + + . "github.com/onsi/gomega" + "gopkg.in/ini.v1" +) + +func TestGetAWSCABundle(t *testing.T) { + g := NewWithT(t) + + tests := []struct { + name string + profile string + setupEnv func() + setupConfig func() string + expectedError string + expectedData []byte + }{ + { + name: "Should return nil if AWS_CONFIG_FILE is not set", + profile: "default", + setupEnv: func() { + os.Unsetenv("AWS_CONFIG_FILE") + }, + expectedData: nil, + }, + { + name: "Should return error if AWS config file cannot be loaded", + profile: "default", + setupEnv: func() { + os.Setenv("AWS_CONFIG_FILE", "invalid_path") + }, + expectedError: "failed to load AWS config", + }, + { + name: "Should return error if profile section is not found", + profile: "nonexistent", + setupEnv: func() { + os.Setenv("AWS_CONFIG_FILE", "test_config.ini") + }, + setupConfig: func() string { + cfg := ini.Empty() + cfg.Section("default").Key("ca_bundle").SetValue("test_bundle.pem") + cfg.SaveTo("test_config.ini") + return "test_config.ini" + }, + expectedError: "failed to get profile nonexistent", + }, + { + name: "Should return nil if ca_bundle is not set in profile", + profile: "default", + setupEnv: func() { + os.Setenv("AWS_CONFIG_FILE", "test_config.ini") + }, + setupConfig: func() string { + cfg := ini.Empty() + cfg.Section("default") + cfg.SaveTo("test_config.ini") + return "test_config.ini" + }, + expectedData: nil, + }, + { + name: "Should return error if ca_bundle file cannot be read", + profile: "default", + setupEnv: func() { + os.Setenv("AWS_CONFIG_FILE", "test_config.ini") + }, + setupConfig: func() string { + cfg := ini.Empty() + cfg.Section("default").Key("ca_bundle").SetValue("invalid_path") + cfg.SaveTo("test_config.ini") + return "test_config.ini" + }, + expectedError: "failed to read ca bundle file", + }, + { + name: "Should return ca_bundle content if everything is correct", + profile: "default", + setupEnv: func() { + os.Setenv("AWS_CONFIG_FILE", "test_config.ini") + }, + setupConfig: func() string { + cfg := ini.Empty() + cfg.Section("default").Key("ca_bundle").SetValue("test_bundle.pem") + cfg.SaveTo("test_config.ini") + os.WriteFile("test_bundle.pem", []byte("test content"), 0644) + return "test_config.ini" + }, + expectedData: []byte("test content"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.setupEnv != nil { + tt.setupEnv() + } + if tt.setupConfig != nil { + tt.setupConfig() + } + + data, err := GetAWSCABundle(context.Background(), tt.profile) + if tt.expectedError != "" { + g.Expect(err).To(HaveOccurred()) + g.Expect(err.Error()).To(ContainSubstring(tt.expectedError)) + } else { + g.Expect(err).NotTo(HaveOccurred()) + } + g.Expect(data).To(Equal(tt.expectedData)) + + // Clean up + os.Remove("test_config.ini") + os.Remove("test_bundle.pem") + }) + } +} diff --git a/pkg/utils/http.go b/pkg/utils/http.go new file mode 100644 index 0000000000..edaaafdd9b --- /dev/null +++ b/pkg/utils/http.go @@ -0,0 +1,69 @@ +package utils + +import ( + "context" + "crypto/tls" + "crypto/x509" + "fmt" + "net/http" +) + +type HttpHandler struct { + CaCert []byte + InsecureSkipVerify bool +} + +// NewHttpHandler creates a new HttpHandler with the given CA certificate and insecure skip verify flag. +func NewHttpHandler(caCert []byte, insecureSkipVerify bool) (*HttpHandler, error) { + return &HttpHandler{ + CaCert: caCert, + InsecureSkipVerify: insecureSkipVerify, + }, nil +} + +// NewHttpHandlerWithAWSCABundle creates a new HttpHandler with the AWS CA bundle for the specified profile. +func NewHttpHandlerWithAWSCABundle(ctx context.Context, profile string) (*HttpHandler, error) { + caBundle, err := GetAWSCABundle(ctx, profile) + if err != nil { + return nil, fmt.Errorf("failed to get AWS CA bundle: %w", err) + } + + return NewHttpHandler(caBundle, caBundle == nil) +} + +// GetHttpClient returns an HTTP client with the specified CA certificate and insecure skip verify flag. +func (h *HttpHandler) GetHttpClient() (*http.Client, error) { + var tlsCfg tls.Config + + if h.InsecureSkipVerify { + tlsCfg = tls.Config{ + InsecureSkipVerify: true, + MinVersion: tls.VersionTLS12, + } + } else { + caCertPool, err := x509.SystemCertPool() + if err != nil { + return nil, fmt.Errorf("failed to load system cert pool: %w", err) + } else if caCertPool == nil { + caCertPool = x509.NewCertPool() + } + if len(h.CaCert) > 0 { + caCertPool.AppendCertsFromPEM(h.CaCert) + } + + tlsCfg = tls.Config{ + RootCAs: caCertPool, + MinVersion: tls.VersionTLS12, + InsecureSkipVerify: false, + } + } + + httpClient := &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tlsCfg, + Proxy: http.ProxyFromEnvironment, + }, + } + + return httpClient, nil +} diff --git a/pkg/utils/permissions_boundary.go b/pkg/utils/permissions_boundary.go index 13fc289220..ff818c7bc3 100644 --- a/pkg/utils/permissions_boundary.go +++ b/pkg/utils/permissions_boundary.go @@ -11,7 +11,7 @@ import ( ) const ( - permissionsBoundaryFile = "/home/.aws/permissionsBoundary" + permissionsBoundaryFile = "/home/.aws/permissions_boundary" ) var ( diff --git a/spectro/base/patch_credentials.yaml b/spectro/base/patch_credentials.yaml index 5035095099..d9a3b55790 100644 --- a/spectro/base/patch_credentials.yaml +++ b/spectro/base/patch_credentials.yaml @@ -1,6 +1,3 @@ - op: replace path: "/spec/template/spec/volumes/0/secret/secretName" - value: "capa-manager-bootstrap-credentials" -- op: replace - path: "/spec/template/spec/volumes/1/secret/secretName" - value: "capa-manager-bootstrap-ca-bundle" \ No newline at end of file + value: "capa-manager-bootstrap-credentials" \ No newline at end of file diff --git a/spectro/generated/core-base.yaml b/spectro/generated/core-base.yaml index a5180b03a0..f58ef33887 100644 --- a/spectro/generated/core-base.yaml +++ b/spectro/generated/core-base.yaml @@ -388,18 +388,10 @@ subjects: --- apiVersion: v1 data: - credentials: ${AWS_B64ENCODED_CABUNDLE} -kind: Secret -metadata: - labels: - cluster.x-k8s.io/provider: infrastructure-aws - name: capa-manager-bootstrap-ca-bundle - namespace: capa-system -type: Opaque ---- -apiVersion: v1 -data: + ca_bundle: ${AWS_B64ENCODED_CABUNDLE} + config: ${AWS_B64ENCODED_CONFIG} credentials: ${AWS_B64ENCODED_CREDENTIALS} + permissions_boundary: ${AWS_B64ENCODED_PERMISSIONS_BOUNDARY} kind: Secret metadata: labels: @@ -460,8 +452,6 @@ spec: volumeMounts: - mountPath: /home/.aws name: credentials - - mountPath: /home/.aws/ca-bundle - name: ca-bundle securityContext: fsGroup: 1000 terminationGracePeriodSeconds: 10 @@ -474,6 +464,3 @@ spec: - name: credentials secret: secretName: capa-manager-bootstrap-credentials - - name: ca-bundle - secret: - secretName: capa-manager-bootstrap-ca-bundle diff --git a/spectro/generated/core-global.yaml b/spectro/generated/core-global.yaml index 366b2beb1c..afce832c8a 100644 --- a/spectro/generated/core-global.yaml +++ b/spectro/generated/core-global.yaml @@ -14051,18 +14051,10 @@ status: --- apiVersion: v1 data: - credentials: ${AWS_B64ENCODED_CABUNDLE} -kind: Secret -metadata: - labels: - cluster.x-k8s.io/provider: infrastructure-aws - name: capa-manager-bootstrap-ca-bundle - namespace: capi-webhook-system -type: Opaque ---- -apiVersion: v1 -data: + ca_bundle: ${AWS_B64ENCODED_CABUNDLE} + config: ${AWS_B64ENCODED_CONFIG} credentials: ${AWS_B64ENCODED_CREDENTIALS} + permissions_boundary: ${AWS_B64ENCODED_PERMISSIONS_BOUNDARY} kind: Secret metadata: labels: @@ -14191,6 +14183,7 @@ kind: MutatingWebhookConfiguration metadata: annotations: cert-manager.io/inject-ca-from: capi-webhook-system/capa-serving-cert + creationTimestamp: null labels: cluster.x-k8s.io/provider: infrastructure-aws name: capa-mutating-webhook-configuration