Skip to content

Commit 51badba

Browse files
committed
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.
1 parent 1be8722 commit 51badba

2 files changed

Lines changed: 95 additions & 4 deletions

File tree

internal/bundlereader/gitclone.go

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
gogit "github.com/go-git/go-git/v5"
1515
"github.com/go-git/go-git/v5/plumbing"
1616
httpgit "github.com/go-git/go-git/v5/plumbing/transport/http"
17+
"github.com/sirupsen/logrus"
1718

1819
fleetssh "github.com/rancher/fleet/internal/ssh"
1920
fleetgit "github.com/rancher/fleet/pkg/git"
@@ -24,9 +25,13 @@ import (
2425
// rawURL may include ?ref=, ?sshkey=, and ?depth= query parameters. The auth
2526
// struct provides credentials, TLS settings, and optional SSH known-hosts.
2627
//
27-
// When auth.CABundle is non-empty, TLS verification uses the system cert pool
28-
// augmented with those CA certificates, so that both the explicit CA and any
29-
// system-trusted CA are accepted.
28+
// TLS verification uses the system cert pool augmented with auth.CABundle (if
29+
// non-empty). PROXY_CA_BUNDLE is appended to auth.CABundle before passing to
30+
// go-git, so that HTTPS repos cloned through an HTTPS proxy with a custom
31+
// certificate are trusted while well-known public CAs remain accepted.
32+
// For SSH repos via HTTPS proxy, the CONNECT tunnel is established by
33+
// newHTTPConnectDialer in pkg/git/proxy.go, which likewise starts from the
34+
// system cert pool and appends PROXY_CA_BUNDLE.
3035
func gitDownload(ctx context.Context, dst, rawURL string, auth Auth) error {
3136
u, err := url.Parse(rawURL)
3237
if err != nil {
@@ -61,10 +66,27 @@ func gitDownload(ctx context.Context, dst, rawURL string, auth Auth) error {
6166
}
6267
}
6368

69+
// Merge PROXY_CA_BUNDLE so that HTTPS repos cloned through an HTTPS proxy
70+
// with a custom CA certificate are trusted. Make a defensive copy of
71+
// auth.CABundle so the caller's slice is never modified.
72+
caBundle := append([]byte(nil), auth.CABundle...)
73+
if proxyCAPEM, ok := os.LookupEnv(fleetgit.ProxyCABundleEnvVar); ok && proxyCAPEM != "" {
74+
proxyBytes := []byte(proxyCAPEM)
75+
tmpPool := x509.NewCertPool()
76+
if !tmpPool.AppendCertsFromPEM(proxyBytes) {
77+
logrus.Warnf("%s is set but contains no valid PEM certificates; ignoring proxy CA bundle", fleetgit.ProxyCABundleEnvVar)
78+
} else {
79+
if len(caBundle) > 0 && caBundle[len(caBundle)-1] != '\n' {
80+
caBundle = append(caBundle, '\n')
81+
}
82+
caBundle = append(caBundle, proxyBytes...)
83+
}
84+
}
85+
6486
cloneOpts := &gogit.CloneOptions{
6587
URL: cloneURL.String(),
6688
InsecureSkipTLS: auth.InsecureSkipVerify,
67-
CABundle: auth.CABundle,
89+
CABundle: caBundle,
6890
ProxyOptions: fleetgit.ProxyOptsFromEnvironment(cloneURL.String()),
6991
}
7092
if err := setGitAuth(cloneOpts, &cloneURL, sshKeyPEM, auth); err != nil {

internal/bundlereader/gitclone_test.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import (
2020
gogit "github.com/go-git/go-git/v5"
2121
"github.com/stretchr/testify/assert"
2222
"github.com/stretchr/testify/require"
23+
24+
fleetgit "github.com/rancher/fleet/pkg/git"
2325
)
2426

2527
// newSelfSignedTLSServer returns an HTTPS test server with a freshly generated
@@ -120,6 +122,73 @@ func TestGitDownloadCABundle(t *testing.T) {
120122
})
121123
}
122124

125+
// TestGitDownloadProxyCABundle verifies that PROXY_CA_BUNDLE is merged into
126+
// the effective CA bundle used for TLS verification in gitDownload.
127+
//
128+
// - PROXY_CA_BUNDLE alone (no auth.CABundle): TLS succeeds when the env var
129+
// contains the server's cert, confirming the merge happens even without an
130+
// explicit CA bundle in the Auth struct.
131+
// - PROXY_CA_BUNDLE merged with auth.CABundle: both certs are trusted.
132+
// - Empty PROXY_CA_BUNDLE: falls back to auth.CABundle only.
133+
//
134+
// Not parallel: the test mutates the process-global PROXY_CA_BUNDLE env var.
135+
func TestGitDownloadProxyCABundle(t *testing.T) {
136+
srv, srvCertPEM := newSelfSignedTLSServer(t)
137+
otherSrv, otherCertPEM := newSelfSignedTLSServer(t)
138+
139+
t.Run("PROXY_CA_BUNDLE alone trusts the server", func(t *testing.T) {
140+
t.Setenv(fleetgit.ProxyCABundleEnvVar, string(srvCertPEM))
141+
dst := t.TempDir()
142+
err := gitDownload(context.Background(), dst, srv.URL, Auth{})
143+
require.Error(t, err)
144+
// TLS succeeded; expect a git-protocol error, not a certificate error.
145+
assert.NotContains(t, err.Error(), "certificate")
146+
})
147+
148+
t.Run("PROXY_CA_BUNDLE is merged with auth.CABundle", func(t *testing.T) {
149+
// auth.CABundle covers srv; PROXY_CA_BUNDLE covers otherSrv.
150+
t.Setenv(fleetgit.ProxyCABundleEnvVar, string(otherCertPEM))
151+
152+
// auth.CABundle server: trusted via auth.CABundle (PROXY_CA_BUNDLE not needed).
153+
dst := t.TempDir()
154+
err := gitDownload(context.Background(), dst, srv.URL, Auth{CABundle: srvCertPEM})
155+
require.Error(t, err)
156+
assert.NotContains(t, err.Error(), "certificate", "auth.CABundle server should get past TLS")
157+
158+
// PROXY_CA_BUNDLE server: trusted via the merged env var cert.
159+
dst = t.TempDir()
160+
err = gitDownload(context.Background(), dst, otherSrv.URL, Auth{CABundle: srvCertPEM})
161+
require.Error(t, err)
162+
assert.NotContains(t, err.Error(), "certificate", "PROXY_CA_BUNDLE server should get past TLS via merge")
163+
})
164+
165+
t.Run("empty PROXY_CA_BUNDLE uses auth.CABundle only", func(t *testing.T) {
166+
t.Setenv(fleetgit.ProxyCABundleEnvVar, "")
167+
dst := t.TempDir()
168+
err := gitDownload(context.Background(), dst, srv.URL, Auth{CABundle: srvCertPEM})
169+
require.Error(t, err)
170+
assert.NotContains(t, err.Error(), "certificate")
171+
})
172+
173+
t.Run("wrong PROXY_CA_BUNDLE without auth.CABundle fails with TLS error", func(t *testing.T) {
174+
// otherCertPEM does not cover srv, and there is no auth.CABundle fallback,
175+
// so TLS must fail with a certificate error.
176+
t.Setenv(fleetgit.ProxyCABundleEnvVar, string(otherCertPEM))
177+
dst := t.TempDir()
178+
err := gitDownload(context.Background(), dst, srv.URL, Auth{})
179+
require.Error(t, err)
180+
assert.Contains(t, err.Error(), "certificate")
181+
})
182+
183+
t.Run("no PROXY_CA_BUNDLE and no auth.CABundle fails with TLS error", func(t *testing.T) {
184+
t.Setenv(fleetgit.ProxyCABundleEnvVar, "")
185+
dst := t.TempDir()
186+
err := gitDownload(context.Background(), dst, srv.URL, Auth{})
187+
require.Error(t, err)
188+
assert.Contains(t, err.Error(), "certificate")
189+
})
190+
}
191+
123192
// generateEd25519PEM returns a PEM-encoded Ed25519 private key for use in tests.
124193
func generateEd25519PEM(t *testing.T) []byte {
125194
t.Helper()

0 commit comments

Comments
 (0)