From 51badba0279c04f8c6a114977cbcdcbb639a0810 Mon Sep 17 00:00:00 2001 From: Patrick Seidensal Date: Tue, 31 Mar 2026 10:23:49 +0200 Subject: [PATCH] bundlereader: merge PROXY_CA_BUNDLE into CABundle for HTTPS git clones gitDownload now appends PROXY_CA_BUNDLE to auth.CABundle before passing the combined PEM to go-git's CloneOptions.CABundle, so that git::https:// repos cloned through an HTTPS proxy with a custom CA certificate are trusted. A defensive copy of auth.CABundle is made to avoid mutating the caller's slice. --- internal/bundlereader/gitclone.go | 30 +++++++++-- internal/bundlereader/gitclone_test.go | 69 ++++++++++++++++++++++++++ 2 files changed, 95 insertions(+), 4 deletions(-) diff --git a/internal/bundlereader/gitclone.go b/internal/bundlereader/gitclone.go index 1bb659e9bc..9dacbca662 100644 --- a/internal/bundlereader/gitclone.go +++ b/internal/bundlereader/gitclone.go @@ -14,6 +14,7 @@ import ( gogit "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing" httpgit "github.com/go-git/go-git/v5/plumbing/transport/http" + "github.com/sirupsen/logrus" fleetssh "github.com/rancher/fleet/internal/ssh" fleetgit "github.com/rancher/fleet/pkg/git" @@ -24,9 +25,13 @@ import ( // rawURL may include ?ref=, ?sshkey=, and ?depth= query parameters. The auth // struct provides credentials, TLS settings, and optional SSH known-hosts. // -// When auth.CABundle is non-empty, TLS verification uses the system cert pool -// augmented with those CA certificates, so that both the explicit CA and any -// system-trusted CA are accepted. +// TLS verification uses the system cert pool augmented with auth.CABundle (if +// non-empty). PROXY_CA_BUNDLE is appended to auth.CABundle before passing to +// go-git, so that HTTPS repos cloned through an HTTPS proxy with a custom +// certificate are trusted while well-known public CAs remain accepted. +// For SSH repos via HTTPS proxy, the CONNECT tunnel is established by +// newHTTPConnectDialer in pkg/git/proxy.go, which likewise starts from the +// system cert pool and appends PROXY_CA_BUNDLE. func gitDownload(ctx context.Context, dst, rawURL string, auth Auth) error { u, err := url.Parse(rawURL) if err != nil { @@ -61,10 +66,27 @@ func gitDownload(ctx context.Context, dst, rawURL string, auth Auth) error { } } + // Merge PROXY_CA_BUNDLE so that HTTPS repos cloned through an HTTPS proxy + // with a custom CA certificate are trusted. Make a defensive copy of + // auth.CABundle so the caller's slice is never modified. + caBundle := append([]byte(nil), auth.CABundle...) + 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...) + } + } + cloneOpts := &gogit.CloneOptions{ URL: cloneURL.String(), InsecureSkipTLS: auth.InsecureSkipVerify, - CABundle: auth.CABundle, + CABundle: caBundle, ProxyOptions: fleetgit.ProxyOptsFromEnvironment(cloneURL.String()), } if err := setGitAuth(cloneOpts, &cloneURL, sshKeyPEM, auth); err != nil { diff --git a/internal/bundlereader/gitclone_test.go b/internal/bundlereader/gitclone_test.go index 938f291932..e4fc007fa7 100644 --- a/internal/bundlereader/gitclone_test.go +++ b/internal/bundlereader/gitclone_test.go @@ -20,6 +20,8 @@ import ( gogit "github.com/go-git/go-git/v5" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + fleetgit "github.com/rancher/fleet/pkg/git" ) // newSelfSignedTLSServer returns an HTTPS test server with a freshly generated @@ -120,6 +122,73 @@ func TestGitDownloadCABundle(t *testing.T) { }) } +// TestGitDownloadProxyCABundle verifies that PROXY_CA_BUNDLE is merged into +// the effective CA bundle used for TLS verification in gitDownload. +// +// - PROXY_CA_BUNDLE alone (no auth.CABundle): TLS succeeds when the env var +// contains the server's cert, confirming the merge happens even without an +// explicit CA bundle in the Auth struct. +// - PROXY_CA_BUNDLE merged with auth.CABundle: both certs are trusted. +// - Empty PROXY_CA_BUNDLE: falls back to auth.CABundle only. +// +// Not parallel: the test mutates the process-global PROXY_CA_BUNDLE env var. +func TestGitDownloadProxyCABundle(t *testing.T) { + srv, srvCertPEM := newSelfSignedTLSServer(t) + otherSrv, otherCertPEM := newSelfSignedTLSServer(t) + + t.Run("PROXY_CA_BUNDLE alone trusts the server", func(t *testing.T) { + t.Setenv(fleetgit.ProxyCABundleEnvVar, string(srvCertPEM)) + dst := t.TempDir() + err := gitDownload(context.Background(), dst, srv.URL, Auth{}) + require.Error(t, err) + // TLS succeeded; expect a git-protocol error, not a certificate error. + assert.NotContains(t, err.Error(), "certificate") + }) + + t.Run("PROXY_CA_BUNDLE is merged with auth.CABundle", func(t *testing.T) { + // auth.CABundle covers srv; PROXY_CA_BUNDLE covers otherSrv. + t.Setenv(fleetgit.ProxyCABundleEnvVar, string(otherCertPEM)) + + // auth.CABundle server: trusted via auth.CABundle (PROXY_CA_BUNDLE not needed). + dst := t.TempDir() + err := gitDownload(context.Background(), dst, srv.URL, Auth{CABundle: srvCertPEM}) + require.Error(t, err) + assert.NotContains(t, err.Error(), "certificate", "auth.CABundle server should get past TLS") + + // PROXY_CA_BUNDLE server: trusted via the merged env var cert. + dst = t.TempDir() + err = gitDownload(context.Background(), dst, otherSrv.URL, Auth{CABundle: srvCertPEM}) + require.Error(t, err) + assert.NotContains(t, err.Error(), "certificate", "PROXY_CA_BUNDLE server should get past TLS via merge") + }) + + t.Run("empty PROXY_CA_BUNDLE uses auth.CABundle only", func(t *testing.T) { + t.Setenv(fleetgit.ProxyCABundleEnvVar, "") + dst := t.TempDir() + err := gitDownload(context.Background(), dst, srv.URL, Auth{CABundle: srvCertPEM}) + require.Error(t, err) + assert.NotContains(t, err.Error(), "certificate") + }) + + t.Run("wrong PROXY_CA_BUNDLE without auth.CABundle fails with TLS error", func(t *testing.T) { + // otherCertPEM does not cover srv, and there is no auth.CABundle fallback, + // so TLS must fail with a certificate error. + t.Setenv(fleetgit.ProxyCABundleEnvVar, string(otherCertPEM)) + dst := t.TempDir() + err := gitDownload(context.Background(), dst, srv.URL, Auth{}) + require.Error(t, err) + assert.Contains(t, err.Error(), "certificate") + }) + + t.Run("no PROXY_CA_BUNDLE and no auth.CABundle fails with TLS error", func(t *testing.T) { + t.Setenv(fleetgit.ProxyCABundleEnvVar, "") + dst := t.TempDir() + err := gitDownload(context.Background(), dst, srv.URL, Auth{}) + require.Error(t, err) + assert.Contains(t, err.Error(), "certificate") + }) +} + // generateEd25519PEM returns a PEM-encoded Ed25519 private key for use in tests. func generateEd25519PEM(t *testing.T) []byte { t.Helper()