Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 26 additions & 4 deletions internal/bundlereader/gitclone.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
69 changes: 69 additions & 0 deletions internal/bundlereader/gitclone_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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")
})

Comment thread
p-se marked this conversation as resolved.
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()
Expand Down