From 40a18fca020cf153836e2c42feedbc3f0afc028a Mon Sep 17 00:00:00 2001 From: Xavi Garcia Date: Fri, 22 May 2026 11:25:49 +0200 Subject: [PATCH 1/2] Use Custom CAs when pulling OCI artifact (#5128) * Use Custom CAs when pulling OCI artifact This PR adds custom CAs when downloading OCI artifacts in a similar way we already use them when downloading helm charts. Refers to: https://github.com/rancher/fleet/issues/4897 Signed-off-by: Xavi Garcia --- _typos.toml | 1 + internal/cmd/cli/apply/apply.go | 27 ++-- internal/ocistorage/ociwrapper.go | 62 +++++++-- internal/ocistorage/ociwrapper_test.go | 181 +++++++++++++++++++++++-- internal/ocistorage/secret.go | 4 + internal/ocistorage/secret_test.go | 61 +++++++++ 6 files changed, 311 insertions(+), 25 deletions(-) diff --git a/_typos.toml b/_typos.toml index 3f9e1584ff..910c0edaff 100644 --- a/_typos.toml +++ b/_typos.toml @@ -18,4 +18,5 @@ extend-exclude = [ extend-ignore-re = [ "-{5}BEGIN RSA PRIVATE KEY-{5}(?:$|[^-]{63,}-{5}END)", "-{5}BEGIN PRIVATE KEY-{5}(?:$|[^-]{63,}-{5}END)", + "-{5}BEGIN CERTIFICATE-{5}(?:$|[^-]{63,}-{5}END CERTIFICATE)" ] diff --git a/internal/cmd/cli/apply/apply.go b/internal/cmd/cli/apply/apply.go index 28d45b59c4..b845706f38 100644 --- a/internal/cmd/cli/apply/apply.go +++ b/internal/cmd/cli/apply/apply.go @@ -575,6 +575,7 @@ func shouldStoreInOCIRegistry(ctx context.Context, c client.Reader, ociSecretKey ociOpts.AgentPassword = opts.AgentPassword ociOpts.BasicHTTP = opts.BasicHTTP ociOpts.InsecureSkipTLS = opts.InsecureSkipTLS + ociOpts.CABundle = opts.CABundle return true, nil } @@ -756,19 +757,27 @@ func newOCISecret(manifestID string, bundle *fleet.Bundle, opts ocistorage.OCIOp }, }, }, - Data: map[string][]byte{ - ocistorage.OCISecretReference: []byte(opts.Reference), - ocistorage.OCISecretUsername: []byte(opts.Username), - ocistorage.OCISecretPassword: []byte(opts.Password), - ocistorage.OCISecretAgentUsername: []byte(opts.AgentUsername), - ocistorage.OCISecretAgentPassword: []byte(opts.AgentPassword), - ocistorage.OCISecretBasicHTTP: []byte(strconv.FormatBool(opts.BasicHTTP)), - ocistorage.OCISecretInsecureSkipTLS: []byte(strconv.FormatBool(opts.InsecureSkipTLS)), - }, + Data: newOCISecretData(opts), Type: fleet.SecretTypeOCIStorage, } } +func newOCISecretData(opts ocistorage.OCIOpts) map[string][]byte { + data := map[string][]byte{ + ocistorage.OCISecretReference: []byte(opts.Reference), + ocistorage.OCISecretUsername: []byte(opts.Username), + ocistorage.OCISecretPassword: []byte(opts.Password), + ocistorage.OCISecretAgentUsername: []byte(opts.AgentUsername), + ocistorage.OCISecretAgentPassword: []byte(opts.AgentPassword), + ocistorage.OCISecretBasicHTTP: []byte(strconv.FormatBool(opts.BasicHTTP)), + ocistorage.OCISecretInsecureSkipTLS: []byte(strconv.FormatBool(opts.InsecureSkipTLS)), + } + if len(opts.CABundle) > 0 { + data[ocistorage.OCISecretCABundle] = opts.CABundle + } + return data +} + func newValuesSecret(bundle *fleet.Bundle, data map[string][]byte) *corev1.Secret { return &corev1.Secret{ Type: fleet.SecretTypeBundleValues, diff --git a/internal/ocistorage/ociwrapper.go b/internal/ocistorage/ociwrapper.go index d5025df0d6..276abeaac0 100644 --- a/internal/ocistorage/ociwrapper.go +++ b/internal/ocistorage/ociwrapper.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "crypto/tls" + "crypto/x509" "encoding/json" "fmt" "io" @@ -15,13 +16,16 @@ import ( "github.com/opencontainers/go-digest" ocispec "github.com/opencontainers/image-spec/specs-go/v1" - "github.com/rancher/fleet/internal/manifest" + "github.com/sirupsen/logrus" "oras.land/oras-go/v2" "oras.land/oras-go/v2/content" "oras.land/oras-go/v2/content/memory" "oras.land/oras-go/v2/registry/remote" "oras.land/oras-go/v2/registry/remote/auth" "oras.land/oras-go/v2/registry/remote/retry" + + "github.com/rancher/fleet/internal/manifest" + fleetgit "github.com/rancher/fleet/pkg/git" ) const ( @@ -39,6 +43,7 @@ type OCIOpts struct { AgentPassword string BasicHTTP bool InsecureSkipTLS bool + CABundle []byte } type OrasOps interface { @@ -71,20 +76,61 @@ func NewOCIWrapper() *OCIWrapper { } } -func getHTTPClient(insecureSkipTLS bool) *http.Client { - if !insecureSkipTLS { +func getHTTPClient(insecureSkipTLS bool, caBundle []byte) *http.Client { + // Defensive copy to avoid mutations + caBundle = append([]byte(nil), caBundle...) + + // Merge proxy CA bundle if present + if proxyCAPEM, ok := os.LookupEnv(fleetgit.ProxyCABundleEnvVar); ok && proxyCAPEM != "" { + proxyBytes := []byte(proxyCAPEM) + tmpPool := x509.NewCertPool() + if !tmpPool.AppendCertsFromPEM(proxyBytes) { + logrus.Warnf("%s is set but contains no valid PEM certificates; ignoring proxy CA bundle", fleetgit.ProxyCABundleEnvVar) + } else { + if len(caBundle) > 0 && caBundle[len(caBundle)-1] != '\n' { + caBundle = append(caBundle, '\n') + } + caBundle = append(caBundle, proxyBytes...) + } + } + + // If no custom TLS config needed, use default ORAS client + if !insecureSkipTLS && len(caBundle) == 0 { return retry.DefaultClient } - return &http.Client{ - Transport: &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, // #nosec G402 - }, + + // Clone the default transport to preserve proxy, timeout, and + // connection-pooling settings. + baseTransport, ok := http.DefaultTransport.(*http.Transport) + if !ok { + baseTransport = &http.Transport{ + Proxy: http.ProxyFromEnvironment, + } + } + transport := baseTransport.Clone() + transport.TLSClientConfig = &tls.Config{ + InsecureSkipVerify: insecureSkipTLS, // #nosec G402 + } + + // Merge custom CA bundle with system cert pool + if len(caBundle) > 0 { + pool, err := x509.SystemCertPool() + if err != nil { + pool = x509.NewCertPool() + } + if !pool.AppendCertsFromPEM(caBundle) { + logrus.Warnf("CA bundle contains no valid PEM certificates; proceeding with system cert pool only") + } + transport.TLSClientConfig.RootCAs = pool + transport.TLSClientConfig.MinVersion = tls.VersionTLS12 } + + return &http.Client{Transport: retry.NewTransport(transport)} } func getAuthClient(opts OCIOpts) *auth.Client { client := &auth.Client{ - Client: getHTTPClient(opts.InsecureSkipTLS), + Client: getHTTPClient(opts.InsecureSkipTLS, opts.CABundle), Cache: auth.NewCache(), } if opts.Username != "" { diff --git a/internal/ocistorage/ociwrapper_test.go b/internal/ocistorage/ociwrapper_test.go index 93e63fb855..2b6f4aa70a 100644 --- a/internal/ocistorage/ociwrapper_test.go +++ b/internal/ocistorage/ociwrapper_test.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "crypto/tls" + "crypto/x509" "fmt" "net/http" "os" @@ -136,17 +137,181 @@ var _ = Describe("OCIUtils tests", func() { Expect(repo.PlainHTTP).To(BeFalse()) }) It("return the expected tls client", func() { - client := getHTTPClient(true) - expected := &http.Client{ - Transport: &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, - }, - } - Expect(client).To(Equal(expected)) + client := getHTTPClient(true, nil) + + // Custom path wraps transport in retry.Transport + retryTransport, ok := client.Transport.(*retry.Transport) + Expect(ok).To(BeTrue()) + innerTransport, ok := retryTransport.Base.(*http.Transport) + Expect(ok).To(BeTrue()) + Expect(innerTransport.TLSClientConfig).ToNot(BeNil()) + Expect(innerTransport.TLSClientConfig.InsecureSkipVerify).To(BeTrue()) + Expect(innerTransport.Proxy).ToNot(BeNil()) + + client = getHTTPClient(false, nil) + Expect(client).To(Equal(retry.DefaultClient)) + }) + It("should use custom CA bundle when provided", func() { + // Use a valid test certificate from the codebase pattern (same as netutils_test.go) + caBundle := []byte(`-----BEGIN CERTIFICATE----- +MIICGTCCAZ+gAwIBAgIQCeCTZaz32ci5PhwLBCou8zAKBggqhkjOPQQDAzBOMQsw +CQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xJjAkBgNVBAMTHURp +Z2lDZXJ0IFRMUyBFQ0MgUDM4NCBSb290IEc1MB4XDTIxMDExNTAwMDAwMFoXDTQ2 +MDExNDIzNTk1OVowTjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJ +bmMuMSYwJAYDVQQDEx1EaWdpQ2VydCBUTFMgRUNDIFAzODQgUm9vdCBHNTB2MBAG +ByqGSM49AgEGBSuBBAAiA2IABMFEoc8Rl1Ca3iOCNQfN0MsYndLxf3c1TzvdlHJS +7cI7+Oz6e2tYIOyZrsn8aLN1udsJ7MgT9U7GCh1mMEy7H0cKPGEQQil8pQgO4CLp +0zVozptjn4S1mU1YoI71VOeVyaNCMEAwHQYDVR0OBBYEFMFRRVBZqz7nLFr6ICIS +B4CIfBFqMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MAoGCCqGSM49 +BAMDA2gAMGUCMQCJao1H5+z8blUD2WdsJk6Dxv3J+ysTvLd6jLRl0mlpYxNjOyZQ +LgGheQaRnUi/wr4CMEfDFXuxoJGZSZOoPHzoRgaLLPIxAJSdYsiJvRmEFOml+wG4 +DXZDjC5Ty3zfDBeWUA== +-----END CERTIFICATE-----`) + client := getHTTPClient(false, caBundle) + + // Verify transport is configured with custom TLS config + retryT, ok := client.Transport.(*retry.Transport) + Expect(ok).To(BeTrue()) + transport, ok := retryT.Base.(*http.Transport) + Expect(ok).To(BeTrue()) + Expect(transport.TLSClientConfig).ToNot(BeNil()) + Expect(transport.TLSClientConfig.RootCAs).ToNot(BeNil()) + Expect(transport.TLSClientConfig.MinVersion).To(Equal(uint16(tls.VersionTLS12))) + Expect(transport.TLSClientConfig.InsecureSkipVerify).To(BeFalse()) + }) + It("should merge proxy CA bundle from environment variable", func() { + // Use a valid certificate for proxy CA (same as custom test) + proxyCA := `-----BEGIN CERTIFICATE----- +MIICGTCCAZ+gAwIBAgIQCeCTZaz32ci5PhwLBCou8zAKBggqhkjOPQQDAzBOMQsw +CQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xJjAkBgNVBAMTHURp +Z2lDZXJ0IFRMUyBFQ0MgUDM4NCBSb290IEc1MB4XDTIxMDExNTAwMDAwMFoXDTQ2 +MDExNDIzNTk1OVowTjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJ +bmMuMSYwJAYDVQQDEx1EaWdpQ2VydCBUTFMgRUNDIFAzODQgUm9vdCBHNTB2MBAG +ByqGSM49AgEGBSuBBAAiA2IABMFEoc8Rl1Ca3iOCNQfN0MsYndLxf3c1TzvdlHJS +7cI7+Oz6e2tYIOyZrsn8aLN1udsJ7MgT9U7GCh1mMEy7H0cKPGEQQil8pQgO4CLp +0zVozptjn4S1mU1YoI71VOeVyaNCMEAwHQYDVR0OBBYEFMFRRVBZqz7nLFr6ICIS +B4CIfBFqMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MAoGCCqGSM49 +BAMDA2gAMGUCMQCJao1H5+z8blUD2WdsJk6Dxv3J+ysTvLd6jLRl0mlpYxNjOyZQ +LgGheQaRnUi/wr4CMEfDFXuxoJGZSZOoPHzoRgaLLPIxAJSdYsiJvRmEFOml+wG4 +DXZDjC5Ty3zfDBeWUA== +-----END CERTIFICATE-----` + os.Setenv("PROXY_CA_BUNDLE", proxyCA) + defer os.Unsetenv("PROXY_CA_BUNDLE") + + // Use the same valid certificate for custom CA (simpler for testing) + customCA := []byte(proxyCA) + client := getHTTPClient(false, customCA) + + // Should have merged both CAs + retryT, ok := client.Transport.(*retry.Transport) + Expect(ok).To(BeTrue()) + transport, ok := retryT.Base.(*http.Transport) + Expect(ok).To(BeTrue()) + Expect(transport.TLSClientConfig).ToNot(BeNil()) + Expect(transport.TLSClientConfig.RootCAs).ToNot(BeNil()) + Expect(transport.TLSClientConfig.MinVersion).To(Equal(uint16(tls.VersionTLS12))) + + // Verify both CAs are valid and would have been loaded + testPool := x509.NewCertPool() + ok = testPool.AppendCertsFromPEM(customCA) + Expect(ok).To(BeTrue(), "custom CA bundle should be valid PEM") + ok = testPool.AppendCertsFromPEM([]byte(proxyCA)) + Expect(ok).To(BeTrue(), "proxy CA bundle should be valid PEM") + }) + It("should handle invalid CA bundle gracefully", func() { + invalidCA := []byte("not a valid PEM certificate") + client := getHTTPClient(false, invalidCA) + + // Should still create a client with TLS config (warning logged) + retryT, ok := client.Transport.(*retry.Transport) + Expect(ok).To(BeTrue()) + transport, ok := retryT.Base.(*http.Transport) + Expect(ok).To(BeTrue()) + Expect(transport.TLSClientConfig).ToNot(BeNil()) + Expect(transport.TLSClientConfig.RootCAs).ToNot(BeNil()) + }) + It("should handle invalid proxy CA bundle gracefully", func() { + os.Setenv("PROXY_CA_BUNDLE", "invalid proxy PEM") + defer os.Unsetenv("PROXY_CA_BUNDLE") + + client := getHTTPClient(false, nil) - client = getHTTPClient(false) + // Should return default client when no valid CA and not insecure Expect(client).To(Equal(retry.DefaultClient)) }) + It("should combine insecureSkipTLS with CA bundle", func() { + caBundle := []byte(`-----BEGIN CERTIFICATE----- +MIICGTCCAZ+gAwIBAgIQCeCTZaz32ci5PhwLBCou8zAKBggqhkjOPQQDAzBOMQsw +CQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xJjAkBgNVBAMTHURp +Z2lDZXJ0IFRMUyBFQ0MgUDM4NCBSb290IEc1MB4XDTIxMDExNTAwMDAwMFoXDTQ2 +MDExNDIzNTk1OVowTjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJ +bmMuMSYwJAYDVQQDEx1EaWdpQ2VydCBUTFMgRUNDIFAzODQgUm9vdCBHNTB2MBAG +ByqGSM49AgEGBSuBBAAiA2IABMFEoc8Rl1Ca3iOCNQfN0MsYndLxf3c1TzvdlHJS +7cI7+Oz6e2tYIOyZrsn8aLN1udsJ7MgT9U7GCh1mMEy7H0cKPGEQQil8pQgO4CLp +0zVozptjn4S1mU1YoI71VOeVyaNCMEAwHQYDVR0OBBYEFMFRRVBZqz7nLFr6ICIS +B4CIfBFqMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MAoGCCqGSM49 +BAMDA2gAMGUCMQCJao1H5+z8blUD2WdsJk6Dxv3J+ysTvLd6jLRl0mlpYxNjOyZQ +LgGheQaRnUi/wr4CMEfDFXuxoJGZSZOoPHzoRgaLLPIxAJSdYsiJvRmEFOml+wG4 +DXZDjC5Ty3zfDBeWUA== +-----END CERTIFICATE-----`) + client := getHTTPClient(true, caBundle) + + retryT, ok := client.Transport.(*retry.Transport) + Expect(ok).To(BeTrue()) + transport, ok := retryT.Base.(*http.Transport) + Expect(ok).To(BeTrue()) + Expect(transport.TLSClientConfig).ToNot(BeNil()) + Expect(transport.TLSClientConfig.InsecureSkipVerify).To(BeTrue()) + Expect(transport.TLSClientConfig.RootCAs).ToNot(BeNil()) + Expect(transport.TLSClientConfig.MinVersion).To(Equal(uint16(tls.VersionTLS12))) + + // Verify the CA bundle is valid PEM (even though InsecureSkipVerify is set) + testPool := x509.NewCertPool() + ok = testPool.AppendCertsFromPEM(caBundle) + Expect(ok).To(BeTrue(), "CA bundle should be valid PEM") + }) + It("should not mutate the original CA bundle slice", func() { + // validPEM is a real certificate so that AppendCertsFromPEM succeeds and + // getHTTPClient actually reaches the append(caBundle, proxyBytes...) path. + // An invalid PEM would make the function return early without appending, + // meaning the defensive copy would never be exercised. + validPEM := []byte(`-----BEGIN CERTIFICATE----- +MIICGTCCAZ+gAwIBAgIQCeCTZaz32ci5PhwLBCou8zAKBggqhkjOPQQDAzBOMQsw +CQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xJjAkBgNVBAMTHURp +Z2lDZXJ0IFRMUyBFQ0MgUDM4NCBSb290IEc1MB4XDTIxMDExNTAwMDAwMFoXDTQ2 +MDExNDIzNTk1OVowTjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJ +bmMuMSYwJAYDVQQDEx1EaWdpQ2VydCBUTFMgRUNDIFAzODQgUm9vdCBHNTB2MBAG +ByqGSM49AgEGBSuBBAAiA2IABMFEoc8Rl1Ca3iOCNQfN0MsYndLxf3c1TzvdlHJS +7cI7+Oz6e2tYIOyZrsn8aLN1udsJ7MgT9U7GCh1mMEy7H0cKPGEQQil8pQgO4CLp +0zVozptjn4S1mU1YoI71VOeVyaNCMEAwHQYDVR0OBBYEFMFRRVBZqz7nLFr6ICIS +B4CIfBFqMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MAoGCCqGSM49 +BAMDA2gAMGUCMQCJao1H5+z8blUD2WdsJk6Dxv3J+ysTvLd6jLRl0mlpYxNjOyZQ +LgGheQaRnUi/wr4CMEfDFXuxoJGZSZOoPHzoRgaLLPIxAJSdYsiJvRmEFOml+wG4 +DXZDjC5Ty3zfDBeWUA== +-----END CERTIFICATE-----`) + + os.Setenv("PROXY_CA_BUNDLE", string(validPEM)) + defer os.Unsetenv("PROXY_CA_BUNDLE") + + // Allocate with excess capacity so that append can write into the backing + // array without reallocating. With len == cap (e.g. []byte("literal")), + // append always reallocates and the defensive copy makes no observable + // difference — the test would pass even without it. + originalCA := make([]byte, len(validPEM), len(validPEM)+512) + copy(originalCA, validPEM) + + // backing covers the full allocation so we can detect writes past len(originalCA). + backing := originalCA[:cap(originalCA)] + + _ = getHTTPClient(false, originalCA) + + // The slice header and visible content must be unchanged. + Expect(originalCA).To(Equal(validPEM)) + // Without the defensive copy, getHTTPClient would append '\n'+proxyBytes + // directly into the excess capacity of the caller's backing array. + Expect(backing[len(validPEM):]).To(Equal(make([]byte, 512)), + "backing array beyond len(originalCA) must not be written to") + }) It("return the expected credentials", func() { opts := OCIOpts{ Reference: "test.com", diff --git a/internal/ocistorage/secret.go b/internal/ocistorage/secret.go index d495edb9cb..1bb9ebfe8b 100644 --- a/internal/ocistorage/secret.go +++ b/internal/ocistorage/secret.go @@ -21,6 +21,7 @@ const ( OCISecretBasicHTTP = "basicHTTP" OCISecretInsecureSkipTLS = "insecureSkipTLS" OCISecretInsecure = "insecure" // legacy alias + OCISecretCABundle = "cacerts" ) // ReadOptsFromSecret reads the secret identified by the given NamespacedName and @@ -84,6 +85,9 @@ func ReadOptsFromSecret(ctx context.Context, c client.Reader, ns client.ObjectKe return OCIOpts{}, err } + // Read optional CA bundle + opts.CABundle = secret.Data[OCISecretCABundle] + return opts, nil } diff --git a/internal/ocistorage/secret_test.go b/internal/ocistorage/secret_test.go index c2ce1f8e12..ff423f974c 100644 --- a/internal/ocistorage/secret_test.go +++ b/internal/ocistorage/secret_test.go @@ -2,6 +2,7 @@ package ocistorage import ( "context" + "crypto/x509" "errors" "fmt" @@ -168,6 +169,66 @@ var _ = Describe("OCIOpts loaded from secret", func() { Expect(opts.AgentPassword).To(BeEmpty()) Expect(opts.BasicHTTP).To(BeFalse()) Expect(opts.InsecureSkipTLS).To(BeFalse()) + Expect(opts.CABundle).To(BeNil()) + }) + }) + + When("the given oci storage secret contains a CA bundle", func() { + BeforeEach(func() { + secretName = "test" + secretData = map[string][]byte{ + OCISecretReference: []byte("reference"), + OCISecretCABundle: []byte(`-----BEGIN CERTIFICATE----- +MIICGTCCAZ+gAwIBAgIQCeCTZaz32ci5PhwLBCou8zAKBggqhkjOPQQDAzBOMQsw +CQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xJjAkBgNVBAMTHURp +Z2lDZXJ0IFRMUyBFQ0MgUDM4NCBSb290IEc1MB4XDTIxMDExNTAwMDAwMFoXDTQ2 +MDExNDIzNTk1OVowTjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJ +bmMuMSYwJAYDVQQDEx1EaWdpQ2VydCBUTFMgRUNDIFAzODQgUm9vdCBHNTB2MBAG +ByqGSM49AgEGBSuBBAAiA2IABMFEoc8Rl1Ca3iOCNQfN0MsYndLxf3c1TzvdlHJS +7cI7+Oz6e2tYIOyZrsn8aLN1udsJ7MgT9U7GCh1mMEy7H0cKPGEQQil8pQgO4CLp +0zVozptjn4S1mU1YoI71VOeVyaNCMEAwHQYDVR0OBBYEFMFRRVBZqz7nLFr6ICIS +B4CIfBFqMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MAoGCCqGSM49 +BAMDA2gAMGUCMQCJao1H5+z8blUD2WdsJk6Dxv3J+ysTvLd6jLRl0mlpYxNjOyZQ +LgGheQaRnUi/wr4CMEfDFXuxoJGZSZOoPHzoRgaLLPIxAJSdYsiJvRmEFOml+wG4 +DXZDjC5Ty3zfDBeWUA== +-----END CERTIFICATE-----`), + } + secretType = fleet.SecretTypeOCIStorage + secretGetErrorMessage = "" + secretGetNotFoundError = false + }) + It("returns the CA bundle in OCIOpts", func() { + ns := client.ObjectKey{Name: secretName, Namespace: "test"} + opts, err := ReadOptsFromSecret(context.TODO(), mockClient, ns) + Expect(err).ToNot(HaveOccurred()) + Expect(opts.Reference).To(Equal(string(secretData[OCISecretReference]))) + Expect(opts.CABundle).To(Equal(secretData[OCISecretCABundle])) + Expect(string(opts.CABundle)).To(ContainSubstring("BEGIN CERTIFICATE")) + + // Verify the CA bundle is valid PEM that can be parsed + testPool := x509.NewCertPool() + ok := testPool.AppendCertsFromPEM(opts.CABundle) + Expect(ok).To(BeTrue(), "CA bundle should be valid PEM") + }) + }) + + When("the given oci storage secret has an empty CA bundle field", func() { + BeforeEach(func() { + secretName = "test" + secretData = map[string][]byte{ + OCISecretReference: []byte("reference"), + OCISecretCABundle: []byte(""), + } + secretType = fleet.SecretTypeOCIStorage + secretGetErrorMessage = "" + secretGetNotFoundError = false + }) + It("returns an empty CA bundle in OCIOpts", func() { + ns := client.ObjectKey{Name: secretName, Namespace: "test"} + opts, err := ReadOptsFromSecret(context.TODO(), mockClient, ns) + Expect(err).ToNot(HaveOccurred()) + Expect(opts.Reference).To(Equal(string(secretData[OCISecretReference]))) + Expect(opts.CABundle).To(Equal([]byte(""))) }) }) From 9008d807d64e3deeb235b171fb17c28c564473a1 Mon Sep 17 00:00:00 2001 From: Xavi Garcia Date: Fri, 22 May 2026 12:38:59 +0200 Subject: [PATCH 2/2] Change to be compatible with 0.15 Signed-off-by: Xavi Garcia --- internal/ocistorage/ociwrapper.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/internal/ocistorage/ociwrapper.go b/internal/ocistorage/ociwrapper.go index 276abeaac0..6ee72920ab 100644 --- a/internal/ocistorage/ociwrapper.go +++ b/internal/ocistorage/ociwrapper.go @@ -25,7 +25,6 @@ import ( "oras.land/oras-go/v2/registry/remote/retry" "github.com/rancher/fleet/internal/manifest" - fleetgit "github.com/rancher/fleet/pkg/git" ) const ( @@ -81,11 +80,11 @@ func getHTTPClient(insecureSkipTLS bool, caBundle []byte) *http.Client { caBundle = append([]byte(nil), caBundle...) // Merge proxy CA bundle if present - if proxyCAPEM, ok := os.LookupEnv(fleetgit.ProxyCABundleEnvVar); ok && proxyCAPEM != "" { + if proxyCAPEM, ok := os.LookupEnv("PROXY_CA_BUNDLE"); ok && proxyCAPEM != "" { proxyBytes := []byte(proxyCAPEM) tmpPool := x509.NewCertPool() if !tmpPool.AppendCertsFromPEM(proxyBytes) { - logrus.Warnf("%s is set but contains no valid PEM certificates; ignoring proxy CA bundle", fleetgit.ProxyCABundleEnvVar) + logrus.Warnf("%s is set but contains no valid PEM certificates; ignoring proxy CA bundle", "PROXY_CA_BUNDLE") } else { if len(caBundle) > 0 && caBundle[len(caBundle)-1] != '\n' { caBundle = append(caBundle, '\n')