Repository: rancher/fleet
Pull request #3233 (2025-01-22 08:33:42)

Fall back to Rancher-configured CA bundles
This enables Fleet to rely on Rancher-configured CA bundle [secrets](https://ranchermanager.docs.rancher.com/getting-started/installation-and-upgrade/installation-references/helm-chart-options#additional-trusted-cas) in the following clients:
* git remote lister and cloner: those CA bundles no longer need to be provided in a `GitRepo`'s spec. Fleet will extract CA bundle data from Rancher secrets if (and only if) no CA bundle is provided for a `GitRepo`.
* Helm client: Fleet will extract, populate and mount Rancher CA bundles in the following cases:
    * no `HelmSecretName` nor `HelmSecretNameForPaths` secret is provided on a `GitRepo`
    * one of those secrets is provided, but only specifies username(s) and password(s), without a CA bundle

This is tested through a combination of:
* unit tests for common Rancher CA bundle data extraction logic
* integration tests for mounting and use of that data by the gitOps controller
* end-to-end tests for successful deployment of workloads with git repositories and Helm charts hosted on infrastructure using a custom CA. Those tests required adapting configuration and deployment of our test git server to support HTTPS beside HTTP and make use of already generated custom certificates.

## Limitations

This does not cover imagescan (pending further discussion on the future of that feature), nor the HelmOps controller.

Falling back to Rancher CA bundles in HelmOps would require reading the HelmApp's `HelmSecretName` field, and populating a CA bundle if that field is empty or if the referenced secret does not contain any CA bundle.

How this should be done to support both [version handling](https://github.com/rancher/fleet/blob/main/internal/cmd/controller/helmops/reconciler/helmapp_controller.go#L294) in the controller and chart downloads from the agent, is unclear for now: should an empty `HelmSecretName` be overwritten by the HelmOps controller, or an incomplete secret edited to add a Rancher CA bundle? This could be handled in a follow-up patch.
    
Refers to #2750
Related docs PR: https://github.com/rancher/fleet-docs/pull/212


Patch:
diff --git a/.github/scripts/create-secrets.sh b/.github/scripts/create-secrets.sh
new file mode 100755
index 00000000..9505c394
--- /dev/null
+++ b/.github/scripts/create-secrets.sh
@@ -0,0 +1,14 @@
+#!/bin/bash
+
+dir=${1-'FleetCI-RootCA'}
+
+# Create secret with certs, needed by test git server
+kubectl -n fleet-local create secret generic git-server-certs \
+    --from-file=./"$dir"/helm.crt \
+    --from-file=./"$dir"/helm.key
+
+# Create cattle-system namespace
+kubectl create ns cattle-system
+
+# Create Rancher CA bundle secret
+kubectl -n cattle-system create secret generic tls-ca-additional --from-file=ca-additional.pem=./"$dir"/root.crt
diff --git a/.github/scripts/create-zot-certs.sh b/.github/scripts/create-zot-certs.sh
index ae488641..47c8151a 100755
--- a/.github/scripts/create-zot-certs.sh
+++ b/.github/scripts/create-zot-certs.sh
@@ -45,6 +45,7 @@ if [ $? -eq 1 ]; then
     [alt_names]
     DNS.1 = zot-service.$FLEET_E2E_NS.svc.cluster.local
     DNS.2 = chartmuseum-service.$FLEET_E2E_NS.svc.cluster.local
+    DNS.3 = git-service.$FLEET_E2E_NS.svc.cluster.local
 EOF
 
     # sign Zot cert with CA root key
diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml
index 7dde188a..4590ba5e 100644
--- a/.github/workflows/golangci-lint.yml
+++ b/.github/workflows/golangci-lint.yml
@@ -28,7 +28,7 @@ jobs:
           export PATH=$PATH:/home/runner/go/bin/
 
       - name: golangci-lint
-        uses: golangci/golangci-lint-action@v6.3.2
+        uses: golangci/golangci-lint-action@v6.4.1
         with:
           # Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version.
           version: v1.61.0
diff --git a/.gitignore b/.gitignore
index 785eee57..282b0478 100644
--- a/.gitignore
+++ b/.gitignore
@@ -18,3 +18,4 @@ FleetCI-RootCA
 env.multi-cluster
 env.single-cluster
 /fossa
+benchmarks/db
diff --git a/benchmarks/cmd/dataset.go b/benchmarks/cmd/dataset.go
new file mode 100644
index 00000000..74f9f1d1
--- /dev/null
+++ b/benchmarks/cmd/dataset.go
@@ -0,0 +1,158 @@
+package main
+
+import (
+	"math"
+	"strings"
+
+	"github.com/rancher/fleet/benchmarks/cmd/parser"
+
+	"gonum.org/v1/gonum/stat"
+)
+
+type Dataset map[string]map[string]Measurements
+
+// Measurements contains all measurements for an experiment in the population
+// and some statistics.
+type Measurements struct {
+	Mean   float64
+	StdDev float64
+	ZScore float64
+	Values []float64
+}
+
+type scoresByXP map[string]scores
+
+type scores struct {
+	ZScores    []float64
+	Weights    []float64
+	MeanZScore float64
+}
+
+func (s scoresByXP) AvgZScores() float64 {
+	zscores := []float64{}
+	for _, xp := range s {
+		zscores = append(zscores, xp.ZScores...)
+	}
+
+	return stat.Mean(zscores, nil)
+}
+
+func skip(name string) bool {
+	switch name {
+	case "GCDuration", "Mem", "MemDuring", "ResourceCount":
+		return true
+
+	}
+	return strings.HasPrefix(name, "RESTClient")
+}
+
+// transformDataset takes a sample and transforms it into a dataset.
+// The output is organized by experiment, for example:
+//
+//	{ "50-gitrepo": {
+//	   "CPU": { "mean": 0.5, "stddev": 0.1, values: [0.4, 0.5, 0.6] },
+//	   "GC": { "mean": 0.5, "stddev": 0.1, values: [0.4, 0.5, 0.6] },
+//	  },
+//	 "50-bundle": {
+//	   "CPU": { "mean": 0.5, "stddev": 0.1, values: [0.4, 0.5, 0.6] },
+//	   "GC": { "mean": 0.5, "stddev": 0.1, values: [0.4, 0.5, 0.6] },
+//	  },
+//	}
+func transformDataset(ds Dataset, sample parser.Sample) {
+	for name, experiment := range sample.Experiments {
+		for measurement, value := range experiment.Measurements {
+			if _, ok := ds[name]; !ok {
+				ds[name] = map[string]Measurements{}
+			}
+			if _, ok := ds[name][measurement]; !ok {
+				ds[name][measurement] = Measurements{
+					Values: []float64{},
+				}
+			}
+			tmp := ds[name][measurement]
+			tmp.Values = append(tmp.Values, value.Value)
+			ds[name][measurement] = tmp
+		}
+	}
+}
+
+// calculate calculates the mean, stddev of the measurement. It calculates the zscore of the sample.
+// This mutates dsPop and scores.
+func calculate(sample *parser.Sample, dsPop Dataset, scores scoresByXP) {
+	// foreach experiment in population, calculate mean and stddev
+	for experiment, xp := range dsPop {
+		for measurement, sg := range xp {
+			mean, stddev := stat.MeanStdDev(sg.Values, nil)
+			if math.IsNaN(stddev) || stddev == 0 {
+				continue
+			}
+
+			if _, ok := sample.Experiments[experiment]; !ok {
+				//fmt.Printf("missing experiment %s\n", name)
+				continue
+			}
+
+			if _, ok := sample.Experiments[experiment].Measurements[measurement]; !ok {
+				//fmt.Printf("missing measurement %s for experiments %s\n", measurement, name)
+				continue
+			}
+
+			if skip(measurement) {
+				continue
+			}
+
+			// calculate zscore
+			m := sample.Experiments[experiment].Measurements[measurement]
+			zscore := stat.StdScore(m.Value, mean, stddev)
+			//fmt.Printf("zscore %s - %s %v %v %v\n", experiment, measurement, m, mean, zscore)
+
+			// store in dsPop
+			sg.Mean = mean
+			sg.StdDev = stddev
+			sg.ZScore = zscore
+			dsPop[experiment][measurement] = sg
+
+			// store to summarize by experiment
+			xp := scores[experiment]
+			xp.ZScores = append(xp.ZScores, zscore)
+			xp.Weights = append(xp.Weights, weight(measurement))
+			scores[experiment] = xp
+		}
+	}
+
+	// Summarize experiments
+	for name, xp := range scores {
+		avg := stat.Mean(xp.ZScores, xp.Weights)
+		xp.MeanZScore = avg
+		scores[name] = xp
+		//fmt.Printf("%s %v %v %v\n", name, avg, xp.ZScores, xp.Weights)
+	}
+
+}
+
+// Some measurements have a higher volatility than others, or are duplicated.
+// Only TotalDuration is used, as it is shown in :he result table.
+//
+// "CPU": 14.029999999999973,
+// "GCDuration": 1.9185229570000004,
+// "Mem": 4,
+// "MemDuring": 4,
+// "NetworkRX": 68288672,
+// "NetworkTX": 30662826,
+// "ReconcileErrors": 0,
+// "ReconcileRequeue": 65,
+// "ReconcileRequeueAfter": 462,
+// "ReconcileSuccess": 2329,
+// "ReconcileTime": 8153.420151420956,
+// "ResourceCount": 300,
+// "WorkqueueAdds": 2844,
+// "WorkqueueQueueDuration": 3911.157310051014,
+// "WorkqueueRetries": 527,
+// "WorkqueueWorkDuration": 8169.425508522996
+func weight(name string) float64 {
+	if name == "TotalDuration" {
+		return 1.0
+	}
+
+	return 0.0
+}
diff --git a/benchmarks/cmd/db.go b/benchmarks/cmd/db.go
new file mode 100644
index 00000000..b958b082
--- /dev/null
+++ b/benchmarks/cmd/db.go
@@ -0,0 +1,102 @@
+package main
+
+import (
+	"encoding/json"
+	"fmt"
+	"os"
+	"path/filepath"
+
+	"github.com/rancher/fleet/benchmarks/cmd/parser"
+
+	gm "github.com/onsi/gomega/gmeasure"
+
+	"github.com/onsi/ginkgo/v2/types"
+)
+
+// Population has data from all json reports
+type Population struct {
+	Samples []parser.Sample
+}
+
+// loadDB loads all the json files from db folder and parses them.
+// The json files are generated by ginkgo --json-report.
+//
+// To inspect them manually use jq:
+//
+//	jq '.[0].SpecReports.[].State' < b-2024-11-11_19:15:08.json
+//	jq '.[0].SpecReports.[].ReportEntries' < b-2024-11-12_15:25:33.json
+//	jq '.[0].SpecReports.[].ReportEntries.[0].Name' < b-2024-11-12_15:25:33.json
+func loadDB(db string) (*Population, error) {
+	files, err := filepath.Glob(db + "/*.json")
+	if err != nil {
+		return nil, err
+	}
+
+	pop := &Population{}
+
+	for _, file := range files {
+		if s, err := loadSampleFile(file); err != nil {
+			return nil, err
+		} else if s != nil {
+			pop.Samples = append(pop.Samples, *s)
+		}
+	}
+
+	return pop, nil
+}
+
+// loadSampleFile loads a single ginkgo json report file
+// This loads any measurements from ginkgo measurement, but it has special
+// handling for MemDuring and measurements that use the Before/After suffixes.
+//
+// To generate such a report json run for example:
+//
+//	ginkgo run --json-report out.json ./benchmarks
+func loadSampleFile(file string) (*parser.Sample, error) {
+	data, err := os.ReadFile(file)
+	if err != nil {
+		return nil, err
+	}
+	reports := []types.Report{}
+	err = json.Unmarshal(data, &reports)
+	if err != nil {
+		fmt.Printf("error: %s\n", data)
+		return nil, err
+	}
+	if len(reports) < 1 {
+		return nil, nil
+	}
+
+	r := (reports)[0]
+	if len(r.SpecReports) < 1 {
+		return nil, nil
+	}
+	if r.SpecReports[0].State != types.SpecStatePassed {
+		return nil, nil
+	}
+
+	s := parser.Sample{
+		Experiments: map[string]parser.Experiment{},
+		Setup:       map[string]parser.Measurement{},
+	}
+
+	d, err := parser.NewSetup(r.SpecReports, s.Setup)
+	if err != nil {
+		return nil, err
+	}
+	s.Description = d
+
+	total, err := parser.NewExperiments(r.SpecReports, s.Experiments)
+	if err != nil {
+		return nil, err
+	}
+
+	s.Setup["TotalDuration"] = parser.Measurement{
+		Type:            gm.MeasurementTypeDuration,
+		Style:           "{{bold}}",
+		PrecisionBundle: gm.DefaultPrecisionBundle,
+		Value:           total,
+	}
+
+	return &s, nil
+}
diff --git a/benchmarks/cmd/parser/parser.go b/benchmarks/cmd/parser/parser.go
new file mode 100644
index 00000000..d9e7cf48
--- /dev/null
+++ b/benchmarks/cmd/parser/parser.go
@@ -0,0 +1,220 @@
+package parser
+
+import (
+	"encoding/json"
+	"fmt"
+	"strings"
+
+	"github.com/onsi/ginkgo/v2/types"
+	gm "github.com/onsi/gomega/gmeasure"
+	"gonum.org/v1/gonum/stat"
+)
+
+const (
+	BeforeSetup = "beforeSetup"
+	AfterSetup  = "afterSetup"
+)
+
+// Sample represents a single benchmark report
+type Sample struct {
+	Description string                 `json:"description,omitempty"`
+	Experiments map[string]Experiment  `json:"experiments,omitempty"`
+	Setup       map[string]Measurement `json:"setup,omitempty"`
+}
+
+// Experiment is a set of measurements, like from 50-gitrepo-1-bundle
+// Measurements from the report are one dimensional, as most experiments don't
+// use sampling
+// type Experiment map[string]float64
+type Experiment struct {
+	Measurements map[string]Measurement
+}
+
+type Measurement struct {
+	Value           float64            `json:"value,omitempty"`
+	Type            gm.MeasurementType `json:"type,omitempty"`
+	PrecisionBundle gm.PrecisionBundle `json:"precision_bundle,omitempty"`
+	Style           string             `json:"style,omitempty"`
+	Units           string             `json:"units,omitempty"`
+}
+
+func (r Measurement) String() string {
+	return fmt.Sprintf(r.PrecisionBundle.ValueFormat, r.Value)
+}
+
+func NewSetup(specReports types.SpecReports, result map[string]Measurement) (string, error) {
+	description := ""
+
+	for _, specReport := range specReports {
+		if len(specReport.ContainerHierarchyLabels) > 1 {
+			continue
+		}
+
+		for _, entry := range specReport.ReportEntries {
+			if entry.Name != "setup" {
+				continue
+			}
+
+			xp := gm.Experiment{}
+			data := entry.Value.AsJSON
+			err := json.Unmarshal([]byte(data), &xp)
+			if err != nil {
+				fmt.Printf("error: %s\n", data)
+				return "", err
+			}
+			// in report:
+			// raw := entry.GetRawValue()
+			// xp, ok := raw.(*gm.Experiment)
+			// if !ok {
+			// 	return nil, false
+			// }
+			//
+
+			if xp.Name != BeforeSetup && xp.Name != AfterSetup {
+				continue
+			}
+
+			for _, m := range xp.Measurements {
+				name, v := Extract(m)
+				if name != "" {
+					tmp, ok := result[name]
+					if ok {
+						tmp.Value += v
+					} else {
+						tmp = Measurement{
+							Value:           v,
+							Type:            m.Type,
+							PrecisionBundle: m.PrecisionBundle,
+							Style:           m.Style,
+							Units:           m.Units,
+						}
+					}
+					result[name] = tmp
+				} else if m.Type == gm.MeasurementTypeNote {
+					description += "\n"
+					lines := strings.Split(strings.Trim(m.Note, "\n"), "\n")
+					for i := range lines {
+						description += fmt.Sprintf("%s\n", lines[i])
+					}
+				}
+			}
+			description += "\n"
+		}
+		break
+	}
+
+	return description, nil
+}
+
+func NewExperiments(specReports types.SpecReports, result map[string]Experiment) (float64, error) {
+	var total float64
+	for _, specReport := range specReports {
+		if specReport.Failed() {
+			return total, nil
+		}
+
+		// handle values from actual experiments, all experiments have labels
+		if len(specReport.ContainerHierarchyLabels) <= 1 {
+			continue
+		}
+
+		// NOTE we need to normalize the measurements, eg. high is
+		// better or low is better, calculate difference from
+		// Before/After.
+		for _, entry := range specReport.ReportEntries {
+			e := Experiment{
+				Measurements: map[string]Measurement{},
+			}
+
+			xp := gm.Experiment{}
+			data := entry.Value.AsJSON
+			err := json.Unmarshal([]byte(data), &xp)
+			if err != nil {
+				fmt.Printf("error: %s\n", data)
+				return total, err
+			}
+			// raw := entry.GetRawValue()
+			// xp, ok := raw.(*gm.Experiment)
+			// if !ok {
+			// 	fmt.Printf("failed to access report: %#v\n", entry)
+			// 	continue
+			// }
+
+			for _, m := range xp.Measurements {
+				name, v := Extract(m)
+				if name == "" {
+					continue
+				}
+
+				if name == "TotalDuration" {
+					total += v
+				}
+
+				tmp, ok := e.Measurements[name]
+				if ok {
+					tmp.Value += v
+				} else {
+					tmp = Measurement{
+						Value:           v,
+						Type:            m.Type,
+						PrecisionBundle: m.PrecisionBundle,
+						Style:           m.Style,
+						Units:           m.Units,
+					}
+				}
+				e.Measurements[name] = tmp
+			}
+			result[entry.Name] = e
+		}
+	}
+
+	return total, nil
+}
+
+func Extract(m gm.Measurement) (string, float64) {
+	var v float64
+
+	switch m.Type {
+	case gm.MeasurementTypeValue:
+		if len(m.Values) < 1 {
+			return "", 0
+		}
+		v = m.Values[0]
+
+	case gm.MeasurementTypeDuration:
+		if len(m.Durations) < 1 {
+			return "", 0
+		}
+		v = m.Durations[0].Round(m.PrecisionBundle.Duration).Seconds()
+
+	default:
+		return "", 0
+	}
+
+	name := m.Name
+
+	// MemDuring is actually sampled, not a single value
+	if m.Name == "MemDuring" {
+		v = stat.Mean(m.Values, nil)
+	} else if beforeAfterName(name) {
+		if strings.HasSuffix(m.Name, "Before") {
+			name = strings.TrimSuffix(m.Name, "Before")
+			v = -v
+		} else {
+			name = strings.TrimSuffix(m.Name, "After")
+		}
+	}
+
+	return name, v
+}
+
+// special handling for Before/After suffixes
+func beforeAfterName(name string) bool {
+	if strings.HasSuffix(name, "Before") {
+		return true
+	}
+	if strings.HasSuffix(name, "After") {
+		return true
+	}
+	return false
+}
diff --git a/benchmarks/cmd/root.go b/benchmarks/cmd/root.go
new file mode 100644
index 00000000..8d985b66
--- /dev/null
+++ b/benchmarks/cmd/root.go
@@ -0,0 +1,355 @@
+package main
+
+import (
+	"encoding/csv"
+	"encoding/json"
+	"fmt"
+	"maps"
+	"os"
+	"regexp"
+	"slices"
+
+	"github.com/rancher/fleet/benchmarks/report"
+
+	"github.com/onsi/gomega/gmeasure/table"
+	"github.com/spf13/cobra"
+)
+
+var (
+	rootCmd = &cobra.Command{
+		Use:   "report",
+		Short: "report on a ginkgo json",
+		Long:  `This is used to analyze benchmark results.`,
+	}
+	input   string
+	db      string
+	verbose bool
+)
+
+func main() {
+	Execute()
+}
+
+func Execute() {
+	err := rootCmd.Execute()
+	if err != nil {
+		os.Exit(1)
+	}
+}
+
+func init() {
+	rootCmd.AddCommand(jsonCmd)
+	rootCmd.AddCommand(reportCmd)
+	rootCmd.AddCommand(csvCmd)
+	rootCmd.PersistentFlags().StringVarP(&input, "input", "i", "report.json", "input file")
+	rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "verbose output")
+
+	reportCmd.PersistentFlags().StringVarP(&db, "db", "d", "db/", "path to json file database dir")
+}
+
+var jsonCmd = &cobra.Command{
+	Use:   "json",
+	Short: "print report as JSON",
+	RunE: func(cmd *cobra.Command, args []string) error {
+		sample, err := loadSampleFile(input)
+		if err != nil {
+			return err
+		}
+
+		// clean up structs for export
+		type row struct {
+			Value string `json:"value,omitempty"`
+			Units string `json:"units,omitempty"`
+		}
+		type export struct {
+			Description string
+			Experiments map[string]map[string]row
+			Setup       map[string]row
+		}
+
+		s := export{
+			Experiments: map[string]map[string]row{},
+			Setup:       map[string]row{},
+			Description: sample.Description,
+		}
+		for name, e := range sample.Experiments {
+			if s.Experiments[name] == nil {
+				s.Experiments[name] = map[string]row{}
+			}
+			for n, r := range e.Measurements {
+				s.Experiments[name][n] = row{
+					Value: r.String(),
+					Units: r.Units,
+				}
+			}
+		}
+		for name, r := range sample.Setup {
+			s.Setup[name] = row{
+				Value: r.String(),
+				Units: r.Units,
+			}
+		}
+
+		fmt.Println(prettyPrint(s))
+
+		return nil
+	},
+}
+
+var csvCmd = &cobra.Command{
+	Use:   "csv",
+	Short: "print report setup as CSV",
+	RunE: func(cmd *cobra.Command, args []string) error {
+		sample, err := loadSampleFile(input)
+		if err != nil {
+			return err
+		}
+
+		description := sample.Description
+		re := regexp.MustCompile(`\{\{[^}]*\}\}`)
+		description = re.ReplaceAllString(description, "")
+		wre := regexp.MustCompile(`WARNING:.*`)
+		description = wre.ReplaceAllString(description, "")
+
+		records := [][]string{}
+
+		headers := []string{"File", "Description"}
+		values := []string{input, description}
+
+		keys := slices.Sorted(maps.Keys(sample.Setup))
+		for _, name := range keys {
+			m := sample.Setup[name]
+
+			headers = append(headers, name+" "+m.Units)
+			values = append(values, m.String())
+		}
+
+		// append experiments duration
+		keys = slices.Sorted(maps.Keys(sample.Experiments))
+		for _, name := range keys {
+			e := sample.Experiments[name]
+
+			headers = append(headers, name)
+			m := e.Measurements["TotalDuration"]
+			values = append(values, m.String())
+		}
+
+		records = append(records, headers)
+		records = append(records, values)
+
+		w := csv.NewWriter(os.Stdout)
+		w.Comma = ','
+
+		for _, record := range records {
+			if err := w.Write(record); err != nil {
+				return err
+			}
+		}
+		w.Flush()
+
+		if err := w.Error(); err != nil {
+			return err
+		}
+
+		return nil
+	},
+}
+
+var reportCmd = &cobra.Command{
+	Use:   "report",
+	Short: "report on a ginkgo json",
+	Long:  `This is used to compare benchmark results.`,
+	RunE: func(cmd *cobra.Command, args []string) error {
+		population, err := loadDB(db)
+		if err != nil {
+			return err
+		}
+		// fmt.Print(prettyPrint(&population))
+
+		sample, err := loadSampleFile(input)
+		if err != nil {
+			return err
+		}
+
+		dsPop := Dataset{}
+		for _, s := range population.Samples {
+			transformDataset(dsPop, s)
+		}
+
+		scores := scoresByXP{}
+
+		calculate(sample, dsPop, scores)
+
+		if verbose {
+			fmt.Println("# Population from DB")
+			fmt.Println("```")
+			fmt.Println(prettyPrint(dsPop))
+			fmt.Println("```")
+			fmt.Println()
+			fmt.Println("# Current Sample")
+			fmt.Println("```")
+			fmt.Println(prettyPrint(sample))
+			fmt.Println("```")
+			fmt.Println()
+		}
+
+		if sample.Description != "" {
+			fmt.Println("# Description of Setup")
+			fmt.Println()
+			fmt.Println(sample.Description)
+			fmt.Println()
+			fmt.Printf("Population size: %d\n", len(population.Samples))
+			fmt.Println()
+		}
+
+		t1 := report.NewTable(sample.Setup)
+		t1.TableStyle.EnableTextStyling = false
+		fmt.Println(t1.Render())
+
+		fmt.Println("# Individual Measurements")
+		fmt.Println()
+
+		mRows := map[string]map[string]MeasurementRow{}
+		for experiment, xp := range sample.Experiments {
+			mRows[experiment] = map[string]MeasurementRow{}
+			for measurement, m := range xp.Measurements {
+				row := MeasurementRow{
+					Experiment:  experiment,
+					Measurement: measurement,
+					Value:       m.String(),
+					Mean:        dsPop[experiment][measurement].Mean,
+					StdDev:      dsPop[experiment][measurement].StdDev,
+					ZScore:      dsPop[experiment][measurement].ZScore,
+				}
+				mRows[experiment][measurement] = row
+			}
+		}
+
+		mTable := newMeasurementTable(mRows)
+		fmt.Println(mTable.Render())
+
+		// table for experiments
+		fmt.Println("# Experiment Summary")
+		fmt.Println()
+
+		rows := map[string]Row{}
+		for experiment, xp := range sample.Experiments {
+			row := Row{
+				Experiment: experiment,
+				ZScore:     scores[experiment].MeanZScore,
+			}
+			row.Duration = xp.Measurements["TotalDuration"].Value
+			row.MeanDuration = dsPop[experiment]["TotalDuration"].Mean
+			row.StdDevDuration = dsPop[experiment]["TotalDuration"].StdDev
+			rows[experiment] = row
+		}
+
+		xpTable := newTable(rows)
+		fmt.Println(xpTable.Render())
+
+		// Final score
+		fmt.Println("# Total Score")
+		fmt.Printf("%s, %.02f\n", input, scores.AvgZScores())
+
+		return nil
+	},
+}
+
+func prettyPrint(i interface{}) string {
+	s, _ := json.MarshalIndent(i, "", "\t")
+	return string(s)
+}
+
+type MeasurementRow struct {
+	Experiment  string
+	Measurement string
+	Value       string
+	Mean        float64
+	StdDev      float64
+	ZScore      float64
+}
+
+type Row struct {
+	Experiment     string
+	Duration       float64
+	MeanDuration   float64
+	StdDevDuration float64
+	ZScore         float64
+}
+
+func newTable(rows map[string]Row) *table.Table {
+	t := table.NewTable()
+	t.TableStyle.EnableTextStyling = false
+	t.AppendRow(table.R(
+		table.C("Experiment"),
+		table.C("Duration"),
+		table.C("Population Mean Duration"),
+		table.C("Population StdDev Duration"),
+		table.C("ZScore"),
+		table.C(""),
+		table.Divider("="),
+		//"{{bold}}",
+	))
+
+	keys := slices.Sorted(maps.Keys(rows))
+	for _, k := range keys {
+		row := rows[k]
+
+		r := table.R()
+		t.AppendRow(r)
+		r.AppendCell(table.C(row.Experiment))
+		r.AppendCell(table.C(fmt.Sprintf("%.02fs", row.Duration)))
+		r.AppendCell(table.C(fmt.Sprintf("%.02fs", row.MeanDuration)))
+		r.AppendCell(table.C(fmt.Sprintf("%.02fs", row.StdDevDuration)))
+		r.AppendCell(table.C(fmt.Sprintf("%.02f", row.ZScore)))
+		if row.ZScore < 0 {
+			r.AppendCell(table.C("better"))
+		} else if row.ZScore > 0 {
+			r.AppendCell(table.C("worse"))
+		} else {
+			r.AppendCell(table.C(""))
+		}
+
+	}
+
+	return t
+}
+
+func newMeasurementTable(rows map[string]map[string]MeasurementRow) *table.Table {
+	t := table.NewTable()
+	t.TableStyle.EnableTextStyling = false
+	t.AppendRow(table.R(
+		table.C("Experiment"),
+		table.C("Measurement"),
+		table.C("Value"),
+		table.C("Population Mean"),
+		table.C("Population StdDev"),
+		table.C("ZScore"),
+		table.Divider("="),
+		//"{{bold}}",
+	))
+
+	keys := slices.Sorted(maps.Keys(rows))
+	for _, k := range keys {
+		rowGroup := rows[k]
+		keys := slices.Sorted(maps.Keys(rowGroup))
+		for _, k := range keys {
+			row := rowGroup[k]
+
+			r := table.R()
+			t.AppendRow(r)
+			r.AppendCell(table.C(row.Experiment))
+			r.AppendCell(table.C(row.Measurement))
+			r.AppendCell(table.C(row.Value))
+			r.AppendCell(table.C(fmt.Sprintf("%.02f", row.Mean)))
+			r.AppendCell(table.C(fmt.Sprintf("%.02f", row.StdDev)))
+			if skip(row.Measurement) {
+				r.AppendCell(table.C("-"))
+			} else {
+				r.AppendCell(table.C(fmt.Sprintf("%.02f", row.ZScore)))
+			}
+		}
+	}
+
+	return t
+}
diff --git a/benchmarks/record/record.go b/benchmarks/record/record.go
index a2be7e28..f3b06622 100644
--- a/benchmarks/record/record.go
+++ b/benchmarks/record/record.go
@@ -221,7 +221,11 @@ func Metrics(experiment *gm.Experiment, suffix string) {
 		case "NetworkRX", "NetworkTX":
 			experiment.RecordValue(n, v, gm.Precision(0), gm.Units("bytes"))
 		default:
-			experiment.RecordValue(n, v, gm.Precision(0))
+			if strings.HasPrefix(k, "RESTClient") {
+				experiment.RecordValue(n, v, gm.Precision(0), gm.Units("requests"))
+			} else {
+				experiment.RecordValue(n, v, gm.Precision(0))
+			}
 		}
 	}
 }
@@ -285,9 +289,37 @@ func extractFromMetricFamilies(res map[string]float64, controllers []string, mfs
 					res["ReconcileSuccess"] += v
 
 				}
+
+				break
 			}
 		}
 	}
+	// rest_client_requests_total{code="200",host="10.43.0.1:443",method="DELETE"} 791
+	// rest_client_requests_total{code="200",host="10.43.0.1:443",method="GET"} 1432
+	// rest_client_requests_total{code="200",host="10.43.0.1:443",method="PATCH"} 213
+	// rest_client_requests_total{code="200",host="10.43.0.1:443",method="PUT"} 15986
+	// rest_client_requests_total{code="201",host="10.43.0.1:443",method="POST"} 2088
+	// rest_client_requests_total{code="404",host="10.43.0.1:443",method="GET"} 1
+	// rest_client_requests_total{code="409",host="10.43.0.1:443",method="POST"} 6
+	// rest_client_requests_total{code="409",host="10.43.0.1:443",method="PUT"} 1392
+	mf = mfs["rest_client_requests_total"]
+	for _, m := range mf.Metric {
+		l := m.GetLabel()
+		v := m.Counter.GetValue()
+		code := ""
+		method := ""
+		for _, tmp := range l {
+			switch tmp.GetName() {
+			case "code":
+				code = tmp.GetValue()
+			case "method":
+				method = tmp.GetValue()
+			}
+		}
+		if code != "" && method != "" {
+			res["RESTClient"+method+code] = v
+		}
+	}
 
 	// Example input:
 	// controller_runtime_reconcile_time_seconds_sum{controller="gitrepo"} 185.52245399500018
diff --git a/benchmarks/report/report.go b/benchmarks/report/report.go
index 37241e57..77202c8f 100644
--- a/benchmarks/report/report.go
+++ b/benchmarks/report/report.go
@@ -5,34 +5,16 @@ import (
 	"fmt"
 	"maps"
 	"slices"
-	"strings"
+
+	"github.com/rancher/fleet/benchmarks/cmd/parser"
 
 	"github.com/onsi/ginkgo/v2"
 	gm "github.com/onsi/gomega/gmeasure"
 	"github.com/onsi/gomega/gmeasure/table"
-
-	"gonum.org/v1/gonum/stat"
 )
 
 type Summary struct {
-	Description string
-	Experiments map[string]Experiment
-	Setup       map[string]Row
-}
-
-type Row struct {
-	Value           float64
-	Type            gm.MeasurementType
-	PrecisionBundle gm.PrecisionBundle
-	Style           string
-	Units           string
-}
-
-// Experiment is a set of measurements, like from 50-gitrepo-1-bundle
-// Measurements from the report are one dimensional, as most experiments don't
-// use sampling. Each measurement will result in one row.
-type Experiment struct {
-	Rows map[string]Row
+	parser.Sample
 }
 
 // ColorableString for ReportEntry to use
@@ -42,14 +24,14 @@ func (s Summary) ColorableString() string {
 	for _, k := range keys {
 		v := s.Experiments[k]
 		sb += fmt.Sprintf("{{green}}%s{{/}}\n", k)
-		t2 := newTable(v.Rows)
+		t2 := NewTable(v.Measurements)
 		sb += t2.Render()
 		sb += "\n"
 	}
 	sb += "{{green}}Environment{{/}}\n"
 	sb += s.Description
 	sb += "\n"
-	t1 := newTable(s.Setup)
+	t1 := NewTable(s.Setup)
 	sb += t1.Render()
 	sb += "\n"
 	return sb
@@ -60,7 +42,7 @@ func (s Summary) String() string {
 	return fmt.Sprintf("%s\n%s\n%s\n", s.Description, prettyPrint(s.Setup), prettyPrint(s.Experiments))
 }
 
-func newTable(rows map[string]Row) *table.Table {
+func NewTable(rows map[string]parser.Measurement) *table.Table {
 	t := table.NewTable()
 	t.AppendRow(table.R(
 		table.C("Measurement"), table.C("Value"), table.C("Unit"),
@@ -75,7 +57,7 @@ func newTable(rows map[string]Row) *table.Table {
 		r := table.R(m.Style)
 		t.AppendRow(r)
 		r.AppendCell(table.C(k))
-		r.AppendCell(table.C(fmt.Sprintf(m.PrecisionBundle.ValueFormat, m.Value)))
+		r.AppendCell(table.C(m.String()))
 		r.AppendCell(table.C(m.Units))
 
 	}
@@ -86,170 +68,33 @@ func newTable(rows map[string]Row) *table.Table {
 // New builds a printable table from the ginkgo.Report.
 func New(r ginkgo.Report) (*Summary, bool) {
 	s := &Summary{
-		Experiments: map[string]Experiment{},
-		Setup:       map[string]Row{},
+		parser.Sample{
+			Experiments: map[string]parser.Experiment{},
+			Setup:       map[string]parser.Measurement{},
+		},
 	}
 
-	total := Row{
-		Type:            gm.MeasurementTypeDuration,
-		Style:           "{{bold}}",
-		PrecisionBundle: gm.DefaultPrecisionBundle,
+	d, err := parser.NewSetup(r.SpecReports, s.Setup)
+	if err != nil {
+		return nil, false
 	}
+	s.Description = d
 
-	for _, specReport := range r.SpecReports {
-		if specReport.Failed() {
-			return nil, false
-		}
-
-		// handle values from actual experiments, all experiments have labels
-		if len(specReport.ContainerHierarchyLabels) <= 1 {
-			continue
-		}
-
-		for _, entry := range specReport.ReportEntries {
-
-			e := Experiment{
-				Rows: map[string]Row{},
-			}
-
-			raw := entry.GetRawValue()
-			xp, ok := raw.(*gm.Experiment)
-			if !ok {
-				fmt.Printf("failed to access report: %#v\n", entry)
-				continue
-			}
-
-			for _, m := range xp.Measurements {
-				name, v := extract(m)
-				if name == "" {
-					continue
-				}
-
-				if name == "TotalDuration" {
-					total.Value += v
-				}
-
-				tmp, ok := e.Rows[name]
-				if ok {
-					tmp.Value += v
-				} else {
-					tmp = Row{
-						Value:           v,
-						Type:            m.Type,
-						PrecisionBundle: m.PrecisionBundle,
-						Style:           m.Style,
-						Units:           m.Units,
-					}
-				}
-				e.Rows[name] = tmp
-			}
-			s.Experiments[entry.Name] = e
-		}
+	total, err := parser.NewExperiments(r.SpecReports, s.Experiments)
+	if err != nil {
+		return nil, false
 	}
 
-	for _, specReport := range r.SpecReports {
-		if len(specReport.ContainerHierarchyLabels) > 1 {
-			continue
-		}
-
-		// handle setup entries
-		for _, entry := range specReport.ReportEntries {
-			if entry.Name != "setup" {
-				continue
-			}
-
-			raw := entry.GetRawValue()
-			xp, ok := raw.(*gm.Experiment)
-			if !ok {
-				return nil, false
-			}
-
-			if xp.Name != "beforeSetup" && xp.Name != "afterSetup" {
-				continue
-			}
-
-			for _, m := range xp.Measurements {
-				name, v := extract(m)
-				if name != "" {
-					tmp, ok := s.Setup[name]
-					if ok {
-						tmp.Value += v
-					} else {
-						tmp = Row{
-							Value:           v,
-							Type:            m.Type,
-							PrecisionBundle: m.PrecisionBundle,
-							Style:           m.Style,
-							Units:           m.Units,
-						}
-					}
-					s.Setup[name] = tmp
-				} else if m.Type == gm.MeasurementTypeNote {
-					s.Description += "\n"
-					lines := strings.Split(strings.Trim(m.Note, "\n"), "\n")
-					for i := range lines {
-						s.Description += fmt.Sprintf("%s\n", lines[i])
-					}
-				}
-			}
-			s.Description += "\n"
-		}
-		break
+	s.Setup["TotalDuration"] = parser.Measurement{
+		Type:            gm.MeasurementTypeDuration,
+		Style:           "{{bold}}",
+		PrecisionBundle: gm.DefaultPrecisionBundle,
+		Value:           total,
 	}
 
-	s.Setup["TotalDuration"] = total
-
 	return s, true
 }
 
-func extract(m gm.Measurement) (string, float64) {
-	var v float64
-
-	switch m.Type {
-	case gm.MeasurementTypeValue:
-		if len(m.Values) < 1 {
-			return "", 0
-		}
-		v = m.Values[0]
-
-	case gm.MeasurementTypeDuration:
-		if len(m.Durations) < 1 {
-			return "", 0
-		}
-		v = m.Durations[0].Seconds()
-
-	default:
-		return "", 0
-	}
-
-	name := m.Name
-
-	// MemDuring is actually sampled, not a single value
-	if m.Name == "MemDuring" {
-		v = stat.Mean(m.Values, nil)
-	} else if beforeAfterName(name) {
-		if strings.HasSuffix(m.Name, "Before") {
-			name = strings.TrimSuffix(m.Name, "Before")
-			v = -v
-		} else {
-			name = strings.TrimSuffix(m.Name, "After")
-		}
-	}
-
-	return name, v
-}
-
-// special handling for Before/After suffixes
-func beforeAfterName(name string) bool {
-	if strings.HasSuffix(name, "Before") {
-		return true
-	}
-	if strings.HasSuffix(name, "After") {
-		return true
-	}
-	return false
-}
-
 func prettyPrint(i interface{}) string {
 	s, _ := json.MarshalIndent(i, "", "\t")
 	return string(s)
diff --git a/dev/benchmarks.sh b/dev/benchmarks.sh
index 7d6b9876..84edfca1 100755
--- a/dev/benchmarks.sh
+++ b/dev/benchmarks.sh
@@ -23,3 +23,5 @@ if [ "$n" -eq 0 ]; then
 fi
 
 ginkgo run --fail-fast --seed 1731598958 --json-report "$FLEET_BENCH_OUTPUT" ./benchmarks
+
+go run ./benchmarks/cmd report -d benchmarks/db -i "$FLEET_BENCH_OUTPUT"
diff --git a/dev/create-secrets b/dev/create-secrets
new file mode 100755
index 00000000..af15c176
--- /dev/null
+++ b/dev/create-secrets
@@ -0,0 +1,3 @@
+#!/bin/bash
+
+./.github/scripts/create-secrets.sh "$@"
diff --git a/dev/setup-multi-cluster b/dev/setup-multi-cluster
index b929f9d0..50f53550 100755
--- a/dev/setup-multi-cluster
+++ b/dev/setup-multi-cluster
@@ -26,4 +26,5 @@ fi
 # needed for gitrepo tests
 ./dev/import-images-tests-k3d
 ./dev/create-zot-certs 'FleetCI-RootCA' # for OCI tests
+./dev/create-secrets 'FleetCI-RootCA'
 ./e2e/testenv/infra/infra setup
diff --git a/dev/setup-single-cluster b/dev/setup-single-cluster
index 23b454a8..913c1efa 100755
--- a/dev/setup-single-cluster
+++ b/dev/setup-single-cluster
@@ -39,4 +39,5 @@ fi
 # needed for gitrepo tests
 ./dev/import-images-tests-k3d
 ./dev/create-zot-certs 'FleetCI-RootCA' # for OCI tests
+./dev/create-secrets 'FleetCI-RootCA'
 ./e2e/testenv/infra/infra setup
diff --git a/go.mod b/go.mod
index 7e7a0870..2e895a26 100644
--- a/go.mod
+++ b/go.mod
@@ -66,7 +66,7 @@ require (
 	k8s.io/utils v0.0.0-20241210054802-24370beab758
 	oras.land/oras-go/v2 v2.5.0
 	sigs.k8s.io/cli-utils v0.37.2
-	sigs.k8s.io/controller-runtime v0.20.1
+	sigs.k8s.io/controller-runtime v0.20.2
 	sigs.k8s.io/controller-tools v0.17.2
 	sigs.k8s.io/kustomize/api v0.18.0
 	sigs.k8s.io/kustomize/kyaml v0.18.1
@@ -113,7 +113,7 @@ require (
 	github.com/docker/go-units v0.5.0 // indirect
 	github.com/emicklei/go-restful/v3 v3.12.1 // indirect
 	github.com/emirpasic/gods v1.18.1 // indirect
-	github.com/evanphx/json-patch/v5 v5.9.0 // indirect
+	github.com/evanphx/json-patch/v5 v5.9.11 // indirect
 	github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect
 	github.com/fatih/color v1.18.0 // indirect
 	github.com/felixge/httpsnoop v1.0.4 // indirect
diff --git a/go.sum b/go.sum
index 72e91e5a..fb5365d0 100644
--- a/go.sum
+++ b/go.sum
@@ -786,8 +786,8 @@ github.com/envoyproxy/protoc-gen-validate v0.9.1/go.mod h1:OKNgG7TCp5pF4d6XftA0+
 github.com/envoyproxy/protoc-gen-validate v0.10.1/go.mod h1:DRjgyB0I43LtJapqN6NiRwroiAU2PaFuvk/vjgh61ss=
 github.com/evanphx/json-patch v5.9.11+incompatible h1:ixHHqfcGvxhWkniF1tWxBHA0yb4Z+d1UQi45df52xW8=
 github.com/evanphx/json-patch v5.9.11+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
-github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg=
-github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ=
+github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU=
+github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM=
 github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f h1:Wl78ApPPB2Wvf/TIe2xdyJxTlb6obmF18d8QdkxNDu4=
 github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f/go.mod h1:OSYXu++VVOHnXeitef/D8n/6y4QV8uLHSFXX4NeXMGc=
 github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
@@ -2195,8 +2195,8 @@ rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
 rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
 sigs.k8s.io/cli-utils v0.37.2 h1:GOfKw5RV2HDQZDJlru5KkfLO1tbxqMoyn1IYUxqBpNg=
 sigs.k8s.io/cli-utils v0.37.2/go.mod h1:V+IZZr4UoGj7gMJXklWBg6t5xbdThFBcpj4MrZuCYco=
-sigs.k8s.io/controller-runtime v0.20.1 h1:JbGMAG/X94NeM3xvjenVUaBjy6Ui4Ogd/J5ZtjZnHaE=
-sigs.k8s.io/controller-runtime v0.20.1/go.mod h1:BrP3w158MwvB3ZbNpaAcIKkHQ7YGpYnzpoSTZ8E14WU=
+sigs.k8s.io/controller-runtime v0.20.2 h1:/439OZVxoEc02psi1h4QO3bHzTgu49bb347Xp4gW1pc=
+sigs.k8s.io/controller-runtime v0.20.2/go.mod h1:xg2XB0K5ShQzAgsoujxuKN4LNXR2LfwwHsPj7Iaw+XY=
 sigs.k8s.io/controller-tools v0.17.2 h1:jNFOKps8WnaRKZU2R+4vRCHnXyJanVmXBWqkuUPFyFg=
 sigs.k8s.io/controller-tools v0.17.2/go.mod h1:4q5tZG2JniS5M5bkiXY2/potOiXyhoZVw/U48vLkXk0=
 sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8=
diff --git a/internal/cmd/controller/gitops/reconciler/gitjob_controller.go b/internal/cmd/controller/gitops/reconciler/gitjob_controller.go
index c8d1d173..6309609d 100644
--- a/internal/cmd/controller/gitops/reconciler/gitjob_controller.go
+++ b/internal/cmd/controller/gitops/reconciler/gitjob_controller.go
@@ -22,6 +22,7 @@ import (
 	"github.com/rancher/fleet/internal/names"
 	"github.com/rancher/fleet/internal/ociwrapper"
 	v1alpha1 "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1"
+	"github.com/rancher/fleet/pkg/cert"
 	fleetevent "github.com/rancher/fleet/pkg/event"
 	"github.com/rancher/fleet/pkg/sharding"
 
@@ -396,30 +397,52 @@ func (r *GitJobReconciler) createTargetsConfigMap(ctx context.Context, gitrepo *
 	return err
 }
 
-func (r *GitJobReconciler) createCABundleSecret(ctx context.Context, gitrepo *v1alpha1.GitRepo) error {
-	if len(gitrepo.Spec.CABundle) == 0 {
-		return nil
+// createCABundleSecret creates a CA bundle secret, if the provided source contains data.
+// That provided source may be the CABundle field of the provided gitrepo (if the provided name matches the CA bundle
+// name expected for the gitrepo, and that CABundle field is non-empty), or Rancher-configured secrets in all other cases.
+// This returns a boolean indicating whether the secret has been successfully created (or updated, in case it already
+// existed), and an error.
+func (r *GitJobReconciler) createCABundleSecret(ctx context.Context, gitrepo *v1alpha1.GitRepo, name string) (bool, error) {
+	var caBundle []byte
+	fieldName := "cacerts"
+
+	if name == caBundleName(gitrepo) {
+		caBundle = gitrepo.Spec.CABundle
+		fieldName = bundleCAFile
+	}
+
+	if len(caBundle) == 0 {
+		cab, err := cert.GetRancherCABundle(ctx, r.Client)
+		if err != nil {
+			return false, err
+		}
+
+		if len(cab) == 0 {
+			return false, nil
+		}
+
+		caBundle = cab
 	}
 
 	secret := &corev1.Secret{
 		ObjectMeta: metav1.ObjectMeta{
 			Namespace: gitrepo.ObjectMeta.Namespace,
-			Name:      caBundleName(gitrepo),
+			Name:      name,
 		},
 		Data: map[string][]byte{
-			bundleCAFile: gitrepo.Spec.CABundle,
+			fieldName: caBundle,
 		},
 	}
 	if err := controllerutil.SetControllerReference(gitrepo, secret, r.Scheme); err != nil {
-		return err
+		return false, err
 	}
 	data := secret.StringData
-	_, err := controllerutil.CreateOrUpdate(ctx, r.Client, secret, func() error {
-		secret.StringData = data
+	res, err := controllerutil.CreateOrUpdate(ctx, r.Client, secret, func() error {
+		secret.StringData = data // Supports update case, if the secret already exists.
 		return nil
 	})
 
-	return err
+	return res == controllerutil.OperationResultCreated || res == controllerutil.OperationResultUpdated, err
 }
 
 func (r *GitJobReconciler) validateExternalSecretExist(ctx context.Context, gitrepo *v1alpha1.GitRepo) error {
@@ -454,7 +477,7 @@ func (r *GitJobReconciler) createJobAndResources(ctx context.Context, gitrepo *v
 	if err := r.createTargetsConfigMap(ctx, gitrepo); err != nil {
 		return fmt.Errorf("failed to create targets config map for git job: %w", err)
 	}
-	if err := r.createCABundleSecret(ctx, gitrepo); err != nil {
+	if _, err := r.createCABundleSecret(ctx, gitrepo, caBundleName(gitrepo)); err != nil {
 		return fmt.Errorf("failed to create cabundle secret for git job: %w", err)
 	}
 	if err := r.createJob(ctx, gitrepo); err != nil {
@@ -530,6 +553,10 @@ func caBundleName(obj *v1alpha1.GitRepo) string {
 	return fmt.Sprintf("%s-cabundle", obj.Name)
 }
 
+func rancherCABundleName(obj *v1alpha1.GitRepo) string {
+	return fmt.Sprintf("%s-rancher-cabundle", obj.Name)
+}
+
 func (r *GitJobReconciler) newGitJob(ctx context.Context, obj *v1alpha1.GitRepo) (*batchv1.Job, error) {
 	jobSpec, err := r.newJobSpec(ctx, obj)
 	if err != nil {
@@ -580,7 +607,17 @@ func (r *GitJobReconciler) newGitJob(ctx context.Context, obj *v1alpha1.GitRepo)
 		},
 	)
 
-	if obj.Spec.CABundle != nil {
+	// Look for a `--ca-bundle-file` arg to the git cloner. This applies to cases where the GitRepo's `Spec.CABundle` is
+	// specified, but also to cases where a CA bundle secret has been created instead, with data from Rancher
+	// secrets.
+	hasCABundleArg := false
+	for _, arg := range initContainer.Args {
+		if arg == "--ca-bundle-file" {
+			hasCABundleArg = true
+			break
+		}
+	}
+	if hasCABundleArg {
 		job.Spec.Template.Spec.Volumes = append(job.Spec.Template.Spec.Volumes, corev1.Volume{
 			Name: bundleCAVolumeName,
 			VolumeSource: corev1.VolumeSource{
@@ -622,6 +659,8 @@ func (r *GitJobReconciler) newGitJob(ctx context.Context, obj *v1alpha1.GitRepo)
 }
 
 func (r *GitJobReconciler) newJobSpec(ctx context.Context, gitrepo *v1alpha1.GitRepo) (*batchv1.JobSpec, error) {
+	var CACertsFilePathOverride string
+
 	paths := gitrepo.Spec.Paths
 	if len(paths) == 0 {
 		paths = []string{"."}
@@ -634,28 +673,66 @@ func (r *GitJobReconciler) newJobSpec(ctx context.Context, gitrepo *v1alpha1.Git
 	}
 
 	volumes, volumeMounts := volumes(configMap.Name)
+	var certVolCreated bool
 
 	if gitrepo.Spec.HelmSecretNameForPaths != "" {
-		vols, volMnts := volumesFromSecret(ctx, r.Client,
+		vols, volMnts, hasCertVol := volumesFromSecret(ctx, r.Client,
 			gitrepo.Namespace,
 			gitrepo.Spec.HelmSecretNameForPaths,
 			"helm-secret-by-path",
+			"",
 		)
 
+		certVolCreated = hasCertVol
+
 		volumes = append(volumes, vols...)
 		volumeMounts = append(volumeMounts, volMnts...)
 
 	} else if gitrepo.Spec.HelmSecretName != "" {
-		vols, volMnts := volumesFromSecret(ctx, r.Client,
+		vols, volMnts, hasCertVol := volumesFromSecret(ctx, r.Client,
 			gitrepo.Namespace,
 			gitrepo.Spec.HelmSecretName,
 			"helm-secret",
+			"",
 		)
 
+		certVolCreated = hasCertVol
+
 		volumes = append(volumes, vols...)
 		volumeMounts = append(volumeMounts, volMnts...)
 	}
 
+	// In case no Helm secret volume has been created, because Helm secrets don't exist or don't contain a CA
+	// bundle, mount a volume with a Rancher CA bundle.
+	if !certVolCreated {
+		// Fall back to Rancher-configured secrets
+		// We need to copy secret data from Rancher, because Rancher secrets live in a different namespace and
+		// can therefore not be used as sources for a volume.
+		secretName := rancherCABundleName(gitrepo)
+		res, err := r.createCABundleSecret(ctx, gitrepo, secretName)
+		if err != nil {
+			return nil, err
+		}
+
+		if res {
+			CACertsDirOverride := "/etc/rancher/certs"
+
+			// Override the volume name and mount path to prevent any conflict with an existing Helm secret
+			// providing username and password.
+			vols, volMnts, _ := volumesFromSecret(ctx, r.Client,
+				gitrepo.Namespace,
+				secretName,
+				"rancher-helm-secret",
+				CACertsDirOverride,
+			)
+
+			volumes = append(volumes, vols...)
+			volumeMounts = append(volumeMounts, volMnts...)
+
+			CACertsFilePathOverride = CACertsDirOverride + "/cacerts"
+		}
+	}
+
 	if ociwrapper.ExperimentalOCIIsEnabled() && gitrepo.Spec.OCIRegistry != nil && gitrepo.Spec.OCIRegistry.AuthSecretName != "" {
 		vol, volMnt, err := ociVolumeFromSecret(ctx, r.Client,
 			gitrepo.Namespace,
@@ -686,7 +763,7 @@ func (r *GitJobReconciler) newJobSpec(ctx context.Context, gitrepo *v1alpha1.Git
 
 	saName := names.SafeConcatName("git", gitrepo.Name)
 	logger := log.FromContext(ctx)
-	args, envs := argsAndEnvs(gitrepo, logger.V(1).Enabled())
+	args, envs := argsAndEnvs(gitrepo, logger.V(1).Enabled(), CACertsFilePathOverride)
 
 	return &batchv1.JobSpec{
 		BackoffLimit: &zero,
@@ -742,7 +819,7 @@ func (r *GitJobReconciler) newJobSpec(ctx context.Context, gitrepo *v1alpha1.Git
 	}, nil
 }
 
-func argsAndEnvs(gitrepo *v1alpha1.GitRepo, debug bool) ([]string, []corev1.EnvVar) {
+func argsAndEnvs(gitrepo *v1alpha1.GitRepo, debug bool, CACertsPathOverride string) ([]string, []corev1.EnvVar) {
 	args := []string{
 		"fleet",
 		"apply",
@@ -808,11 +885,17 @@ func argsAndEnvs(gitrepo *v1alpha1.GitRepo, debug bool) ([]string, []corev1.EnvV
 		helmArgs := []string{
 			"--password-file",
 			"/etc/fleet/helm/password",
-			"--cacerts-file",
-			"/etc/fleet/helm/cacerts",
 			"--ssh-privatekey-file",
 			"/etc/fleet/helm/ssh-privatekey",
 		}
+
+		if CACertsPathOverride == "" {
+			helmArgs = append(helmArgs,
+				"--cacerts-file",
+				"/etc/fleet/helm/cacerts",
+			)
+		}
+
 		if gitrepo.Spec.HelmRepoURLRegex != "" {
 			helmArgs = append(helmArgs, "--helm-repo-url-regex", gitrepo.Spec.HelmRepoURLRegex)
 		}
@@ -837,6 +920,23 @@ func argsAndEnvs(gitrepo *v1alpha1.GitRepo, debug bool) ([]string, []corev1.EnvV
 			})
 	}
 
+	if CACertsPathOverride != "" {
+		helmArgs := []string{
+			"--cacerts-file",
+			CACertsPathOverride,
+		}
+		if gitrepo.Spec.HelmRepoURLRegex != "" {
+			helmArgs = append(helmArgs, "--helm-repo-url-regex", gitrepo.Spec.HelmRepoURLRegex)
+		}
+		args = append(args, helmArgs...)
+		env = append(env,
+			// for ssh go-getter, make sure we always accept new host key
+			corev1.EnvVar{
+				Name:  "GIT_SSH_COMMAND",
+				Value: "ssh -o stricthostkeychecking=accept-new",
+			})
+	}
+
 	if ociwrapper.ExperimentalOCIIsEnabled() && gitrepo.Spec.OCIRegistry != nil && gitrepo.Spec.OCIRegistry.Reference != "" {
 		args = append(args, "--oci-reference", gitrepo.Spec.OCIRegistry.Reference)
 		if gitrepo.Spec.OCIRegistry.AuthSecretName != "" {
@@ -951,8 +1051,12 @@ func volumesFromSecret(
 	ctx context.Context,
 	c client.Client,
 	namespace string,
-	secretName, volumeName string,
-) ([]corev1.Volume, []corev1.VolumeMount) {
+	secretName, volumeName, mountPath string,
+) ([]corev1.Volume, []corev1.VolumeMount, bool) {
+	if mountPath == "" {
+		mountPath = "/etc/fleet/helm"
+	}
+
 	volumes := []corev1.Volume{
 		{
 			Name: volumeName,
@@ -966,19 +1070,18 @@ func volumesFromSecret(
 	volumeMounts := []corev1.VolumeMount{
 		{
 			Name:      volumeName,
-			MountPath: "/etc/fleet/helm",
+			MountPath: mountPath,
 		},
 	}
 
 	// Mount a CA certificate, if specified in the secret. This is necessary to support Helm registries with
 	// self-signed certificates.
 	secret := &corev1.Secret{}
+	var certVolCreated bool
 	_ = c.Get(ctx, types.NamespacedName{Namespace: namespace, Name: secretName}, secret)
 	if _, ok := secret.Data["cacerts"]; ok {
-		certVolumeName := fmt.Sprintf("%s-cert", volumeName)
-
 		volumes = append(volumes, corev1.Volume{
-			Name: certVolumeName,
+			Name: fmt.Sprintf("%s-cert", volumeName),
 			VolumeSource: corev1.VolumeSource{
 				Secret: &corev1.SecretVolumeSource{
 					SecretName: secretName,
@@ -992,12 +1095,14 @@ func volumesFromSecret(
 			},
 		})
 		volumeMounts = append(volumeMounts, corev1.VolumeMount{
-			Name:      certVolumeName,
+			Name:      fmt.Sprintf("%s-cert", volumeName),
 			MountPath: "/etc/ssl/certs",
 		})
+
+		certVolCreated = true
 	}
 
-	return volumes, volumeMounts
+	return volumes, volumeMounts, certVolCreated
 }
 
 func (r *GitJobReconciler) newGitCloner(ctx context.Context, obj *v1alpha1.GitRepo) (corev1.Container, error) {
@@ -1055,7 +1160,17 @@ func (r *GitJobReconciler) newGitCloner(ctx context.Context, obj *v1alpha1.GitRe
 	if obj.Spec.InsecureSkipTLSverify {
 		args = append(args, "--insecure-skip-tls")
 	}
-	if obj.Spec.CABundle != nil {
+
+	var CABundleSecret corev1.Secret
+	err := r.Get(ctx, types.NamespacedName{
+		Namespace: obj.Namespace,
+		Name:      caBundleName(obj),
+	}, &CABundleSecret)
+	if client.IgnoreNotFound(err) != nil {
+		return corev1.Container{}, err
+	}
+
+	if !errors.IsNotFound(err) {
 		volumeMounts = append(volumeMounts, corev1.VolumeMount{
 			Name:      bundleCAVolumeName,
 			MountPath: "/gitjob/cabundle",
diff --git a/pkg/cert/cabundle.go b/pkg/cert/cabundle.go
new file mode 100644
index 00000000..07cfe734
--- /dev/null
+++ b/pkg/cert/cabundle.go
@@ -0,0 +1,44 @@
+package cert
+
+import (
+	"context"
+	"fmt"
+
+	corev1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/api/errors"
+	"k8s.io/apimachinery/pkg/types"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+)
+
+const rancherNS = "cattle-system"
+
+func GetRancherCABundle(ctx context.Context, c client.Client) ([]byte, error) {
+	secret := &corev1.Secret{}
+
+	err := c.Get(ctx, types.NamespacedName{Namespace: rancherNS, Name: "tls-ca"}, secret)
+	if client.IgnoreNotFound(err) != nil {
+		return nil, err
+	}
+
+	caBundle, ok := secret.Data["cacerts.pem"] // TODO check that the path is right, with an actual Rancher install
+	if !errors.IsNotFound(err) && !ok {
+		return nil, fmt.Errorf("no field cacerts.pem found in secret tls-ca")
+	}
+
+	err = c.Get(ctx, types.NamespacedName{Namespace: rancherNS, Name: "tls-ca-additional"}, secret)
+	if err != nil {
+		if errors.IsNotFound(err) {
+			return caBundle, nil
+		}
+
+		return nil, err
+	}
+
+	field, ok := secret.Data["ca-additional.pem"] // TODO check that the path is right, with an actual Rancher install
+	if !ok {
+		return nil, fmt.Errorf("no field ca-additional.pem found in secret tls-ca-additional")
+	}
+	caBundle = append(caBundle, field...)
+
+	return caBundle, nil
+}
diff --git a/pkg/git/fetch.go b/pkg/git/fetch.go
index b1419735..da8c0960 100644
--- a/pkg/git/fetch.go
+++ b/pkg/git/fetch.go
@@ -5,6 +5,7 @@ import (
 
 	"github.com/rancher/fleet/internal/config"
 	v1alpha1 "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1"
+	"github.com/rancher/fleet/pkg/cert"
 
 	corev1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/api/errors"
@@ -43,8 +44,19 @@ func (f *Fetch) LatestCommit(ctx context.Context, gitrepo *v1alpha1.GitRepo, cli
 		branch = "master"
 	}
 
+	// Fall back to Rancher-configured CA bundles if no CA bundle is specified in the GitRepo
+	cabundle := gitrepo.Spec.CABundle
+	if len(cabundle) == 0 {
+		cab, err := cert.GetRancherCABundle(ctx, client)
+		if err != nil {
+			return "", err
+		}
+
+		cabundle = cab
+	}
+
 	r, err := NewRemote(gitrepo.Spec.Repo, &options{
-		CABundle:          gitrepo.Spec.CABundle,
+		CABundle:          cabundle,
 		Credential:        &secret,
 		InsecureTLSVerify: gitrepo.Spec.InsecureSkipTLSverify,
 		Timeout:           config.Get().GitClientTimeout.Duration,


Test patch:
diff --git a/.github/workflows/e2e-ci.yml b/.github/workflows/e2e-ci.yml
index 22bf4351..5f422236 100644
--- a/.github/workflows/e2e-ci.yml
+++ b/.github/workflows/e2e-ci.yml
@@ -107,6 +107,7 @@ jobs:
           FLEET_E2E_NS: fleet-local
         run: |
           ./.github/scripts/create-zot-certs.sh "FleetCI-RootCA"
+          ./.github/scripts/create-secrets.sh 'FleetCI-RootCA'
       -
         name: E2E Infra Tests
         if: ${{ matrix.test_type.name == 'infra-setup' }}
diff --git a/.github/workflows/e2e-nightly-ci.yml b/.github/workflows/e2e-nightly-ci.yml
index 0e211119..1624d0e2 100644
--- a/.github/workflows/e2e-nightly-ci.yml
+++ b/.github/workflows/e2e-nightly-ci.yml
@@ -82,6 +82,7 @@ jobs:
         run: |
           # Generate cert and key for TLS
           ./.github/scripts/create-zot-certs.sh "FleetCI-RootCA"
+          ./.github/scripts/create-secrets.sh 'FleetCI-RootCA'
       -
         name: E2E Tests
         if: ${{ matrix.test_type == 'e2e-nosecrets' }}
diff --git a/benchmarks/suite_test.go b/benchmarks/suite_test.go
index 2b36acf9..c7db9e82 100644
--- a/benchmarks/suite_test.go
+++ b/benchmarks/suite_test.go
@@ -11,6 +11,7 @@ import (
 	. "github.com/onsi/gomega"
 	gm "github.com/onsi/gomega/gmeasure"
 
+	"github.com/rancher/fleet/benchmarks/cmd/parser"
 	"github.com/rancher/fleet/benchmarks/record"
 	"github.com/rancher/fleet/benchmarks/report"
 	"github.com/rancher/fleet/e2e/testenv/kubectl"
@@ -75,7 +76,6 @@ var _ = JustBeforeEach(func() {
 	experiment = gm.NewExperiment(name)
 	AddReportEntry(experiment.Name, experiment, ReportEntryVisibilityNever)
 	experiment.RecordNote(record.Header("Info")+info, gm.Style("{{green}}"))
-	record.MemoryUsage(experiment, "MemBefore")
 	record.ResourceCount(ctx, experiment, "ResourceCountBefore")
 	if metrics {
 		record.Metrics(experiment, "Before")
@@ -129,7 +129,7 @@ var _ = BeforeSuite(func() {
 	record.Setup(workspace, k8sClient, k)
 
 	// describe the environment this suite is running against
-	e := gm.NewExperiment("beforeSetup")
+	e := gm.NewExperiment(parser.BeforeSetup)
 	record.MemoryUsage(e, "MemBefore")
 	record.ResourceCount(ctx, e, "ResourceCountBefore")
 	record.CRDCount(ctx, e, "CRDCount")
@@ -146,7 +146,7 @@ var _ = BeforeSuite(func() {
 })
 
 var _ = AfterSuite(func() {
-	e := gm.NewExperiment("afterSetup")
+	e := gm.NewExperiment(parser.AfterSetup)
 	record.MemoryUsage(e, "MemAfter")
 	record.ResourceCount(ctx, e, "ResourceCountAfter")
 	AddReportEntry("setup", e, ReportEntryVisibilityNever)
diff --git a/e2e/assets/gitrepo/nginx_deployment.yaml b/e2e/assets/gitrepo/nginx_deployment.yaml
index 0ab27d86..91faff54 100644
--- a/e2e/assets/gitrepo/nginx_deployment.yaml
+++ b/e2e/assets/gitrepo/nginx_deployment.yaml
@@ -14,9 +14,23 @@ spec:
       labels:
         app: git-server
     spec:
+      volumes:
+        - name: git-certs
+          secret:
+            secretName: git-server-certs
+            items:
+              - key: helm.crt
+                path: helm.crt
+              - key: helm.key
+                path: helm.key
       containers:
         - name: git-server
           image: nginx-git:test
           imagePullPolicy: IfNotPresent
           ports:
+          - containerPort: 4343
           - containerPort: 8080
+          volumeMounts:
+            - name: git-certs
+              mountPath: "/etc/ssl/certs"
+              readOnly: true
diff --git a/e2e/assets/gitrepo/nginx_git.conf b/e2e/assets/gitrepo/nginx_git.conf
index 94790c8a..48dde3d9 100644
--- a/e2e/assets/gitrepo/nginx_git.conf
+++ b/e2e/assets/gitrepo/nginx_git.conf
@@ -1,8 +1,12 @@
 events {}
 http {
     server {
+        error_log stderr info;
         listen       80;
+        listen       443 ssl;
         server_name localhost;
+        ssl_certificate /etc/ssl/certs/helm.crt;
+        ssl_certificate_key /etc/ssl/certs/helm.key;
 
         # This is where the repositories live on the server
         root /srv/git;
diff --git a/e2e/assets/gitrepo/nginx_service.yaml b/e2e/assets/gitrepo/nginx_service.yaml
index 60db11db..aa6b1be6 100644
--- a/e2e/assets/gitrepo/nginx_service.yaml
+++ b/e2e/assets/gitrepo/nginx_service.yaml
@@ -6,7 +6,12 @@ spec:
   selector:
     app: git-server
   ports:
-    - protocol: TCP
+    - name: http
+      protocol: TCP
       port: 8080
       targetPort: 80
+    - name: https
+      protocol: TCP
+      port: 4343
+      targetPort: 443
   type: LoadBalancer
diff --git a/e2e/single-cluster/gitrepo_test.go b/e2e/single-cluster/gitrepo_test.go
index 0ac10257..e9b894d2 100644
--- a/e2e/single-cluster/gitrepo_test.go
+++ b/e2e/single-cluster/gitrepo_test.go
@@ -26,8 +26,9 @@ import (
 )
 
 const (
-	port     = 8080
-	repoName = "repo"
+	port      = 8080
+	HTTPSPort = 4343
+	repoName  = "repo"
 )
 
 var _ = Describe("Monitoring Git repos via HTTP for change", Label("infra-setup"), func() {
@@ -44,329 +45,353 @@ var _ = Describe("Monitoring Git repos via HTTP for change", Label("infra-setup"
 		targetNamespace  string
 	)
 
-	BeforeEach(func() {
-		k = env.Kubectl.Namespace(env.Namespace)
-	})
+	cases := []struct {
+		name        string
+		port        int
+		protocol    string
+		withWebhook bool
+	}{
+		{
+			name:        "HTTP",
+			port:        port,
+			protocol:    "http",
+			withWebhook: true,
+		},
+		{
+			name:     "HTTPS with a custom CA",
+			port:     HTTPSPort,
+			protocol: "https",
+		},
+	}
+	for _, c := range cases {
+		BeforeEach(func() {
+			k = env.Kubectl.Namespace(env.Namespace)
+		})
 
-	JustBeforeEach(func() {
+		JustBeforeEach(func() {
 
-		// Build git repo URL reachable _within_ the cluster, for the GitRepo
-		host, err := githelper.BuildGitHostname(env.Namespace)
-		Expect(err).ToNot(HaveOccurred())
+			// Build git repo URL reachable _within_ the cluster, for the GitRepo
+			host, err := githelper.BuildGitHostname(env.Namespace)
+			Expect(err).ToNot(HaveOccurred())
 
-		addr, err := githelper.GetExternalRepoAddr(env, port, repoName)
-		Expect(err).ToNot(HaveOccurred())
-		gh = githelper.NewHTTP(addr)
+			addr, err := githelper.GetExternalRepoAddr(env, c.port, repoName)
+			Expect(err).ToNot(HaveOccurred())
+			addr = strings.Replace(addr, "http://", fmt.Sprintf("%s://", c.protocol), 1)
+			gh = githelper.NewHTTP(addr)
 
-		inClusterRepoURL = gh.GetInClusterURL(host, port, repoName)
+			inClusterRepoURL = gh.GetInClusterURL(host, c.port, repoName)
 
-		tmpDir, _ = os.MkdirTemp("", "fleet-")
-		clonedir = path.Join(tmpDir, repoName)
+			tmpDir, _ = os.MkdirTemp("", "fleet-")
+			clonedir = path.Join(tmpDir, repoName)
 
-		gitrepoName = testenv.RandomFilename("gitjob-test", r)
-	})
+			gitrepoName = testenv.RandomFilename("gitjob-test", r)
+		})
 
-	AfterEach(func() {
-		_ = os.RemoveAll(tmpDir)
+		AfterEach(func() {
+			_ = os.RemoveAll(tmpDir)
 
-		_, err := k.Delete("gitrepo", gitrepoName)
-		Expect(err).ToNot(HaveOccurred())
+			_, _ = k.Delete("gitrepo", gitrepoName)
 
-		// Check that the bundle deployment resource has been deleted
-		Eventually(func(g Gomega) {
-			out, _ := k.Get(
-				"bundledeployments",
-				"-A",
+			// Check that the bundle deployment resource has been deleted
+			Eventually(func(g Gomega) {
+				out, _ := k.Get(
+					"bundledeployments",
+					"-A",
+					"-l",
+					fmt.Sprintf("fleet.cattle.io/repo-name=%s", gitrepoName),
+				)
+				g.Expect(out).To(ContainSubstring("No resources found"))
+			}).Should(Succeed())
+
+			out, err := k.Namespace("cattle-fleet-system").Logs(
 				"-l",
-				fmt.Sprintf("fleet.cattle.io/repo-name=%s", gitrepoName),
+				"app=fleet-controller",
+				"-c",
+				"fleet-controller",
 			)
-			g.Expect(out).To(ContainSubstring("No resources found"))
-		}).Should(Succeed())
-
-		out, err := k.Namespace("cattle-fleet-system").Logs(
-			"-l",
-			"app=fleet-controller",
-			"-c",
-			"fleet-controller",
-		)
-		Expect(err).ToNot(HaveOccurred())
-
-		// Errors about resources other than bundles or bundle deployments not being found at deletion time
-		// should be ignored, as they may result from other test suites.
-		Expect(out).ToNot(MatchRegexp(
-			`ERROR.*Reconciler error.*Bundle(Deployment)?.fleet.cattle.io \\".*\\" not found`,
-		))
-
-		_, err = k.Delete("ns", targetNamespace, "--wait=false")
-		Expect(err).ToNot(HaveOccurred())
-	})
-
-	When("updating a git repository monitored via polling", func() {
-		BeforeEach(func() {
-			repoName = "repo"
-			targetNamespace = testenv.NewNamespaceName("target", r)
-		})
-
-		JustBeforeEach(func() {
-			err := testenv.ApplyTemplate(k, testenv.AssetPath("gitrepo/gitrepo.yaml"), struct {
-				Name            string
-				Repo            string
-				Branch          string
-				PollingInterval string
-				TargetNamespace string
-			}{
-				gitrepoName,
-				inClusterRepoURL,
-				gh.Branch,
-				"15s",           // default
-				targetNamespace, // to avoid conflicts with other tests
-			})
 			Expect(err).ToNot(HaveOccurred())
 
-			clone, err = gh.Create(clonedir, testenv.AssetPath("gitrepo/sleeper-chart"), "examples")
+			// Errors about resources other than bundles or bundle deployments not being found at deletion time
+			// should be ignored, as they may result from other test suites.
+			Expect(out).ToNot(MatchRegexp(
+				`ERROR.*Reconciler error.*Bundle(Deployment)?.fleet.cattle.io \\".*\\" not found`,
+			))
+
+			_, err = k.Delete("ns", targetNamespace, "--wait=false")
 			Expect(err).ToNot(HaveOccurred())
 		})
 
-		It("updates the deployment", func() {
-			By("checking the pod exists")
-			Eventually(func() string {
-				out, _ := k.Namespace(targetNamespace).Get("pods")
-				return out
-			}).Should(ContainSubstring("sleeper-"))
-
-			By("updating the git repository")
-			replace(path.Join(clonedir, "examples", "Chart.yaml"), "0.1.0", "0.2.0")
-			replace(path.Join(clonedir, "examples", "templates", "deployment.yaml"), "name: sleeper", "name: newsleep")
+		When(fmt.Sprintf("updating a git repository monitored via polling with %s for change", c.name), func() {
+			BeforeEach(func() {
+				repoName = "repo"
+				targetNamespace = testenv.NewNamespaceName("target", r)
+			})
 
-			commit, err := gh.Update(clone)
-			Expect(err).ToNot(HaveOccurred())
+			JustBeforeEach(func() {
+				err := testenv.ApplyTemplate(k, testenv.AssetPath("gitrepo/gitrepo.yaml"), struct {
+					Name            string
+					Repo            string
+					Branch          string
+					PollingInterval string
+					TargetNamespace string
+				}{
+					gitrepoName,
+					inClusterRepoURL,
+					gh.Branch,
+					"15s",           // default
+					targetNamespace, // to avoid conflicts with other tests
+				})
+				Expect(err).ToNot(HaveOccurred())
+
+				clone, err = gh.Create(clonedir, testenv.AssetPath("gitrepo/sleeper-chart"), "examples")
+				Expect(err).ToNot(HaveOccurred())
+			})
 
-			By("updating the gitrepo's status")
-			expectedStatus := fleet.GitRepoStatus{
-				Commit:       commit,
-				GitJobStatus: "Current",
-				StatusBase: fleet.StatusBase{
-					ReadyClusters:        1,
-					DesiredReadyClusters: 1,
-					Summary: fleet.BundleSummary{
-						NotReady:          0,
-						WaitApplied:       0,
-						ErrApplied:        0,
-						OutOfSync:         0,
-						Modified:          0,
-						Ready:             1,
-						Pending:           0,
-						DesiredReady:      1,
-						NonReadyResources: []fleet.NonReadyResource(nil),
-					},
-					Display: fleet.StatusDisplay{
-						ReadyBundleDeployments: "1/1",
-						// XXX: add state and message?
-					},
-					Conditions: []genericcondition.GenericCondition{
-						{
-							Type:   "Ready",
-							Status: "True",
+			It("updates the deployment", func() {
+				By("checking the pod exists")
+				Eventually(func() string {
+					out, _ := k.Namespace(targetNamespace).Get("pods")
+					return out
+				}).Should(ContainSubstring("sleeper-"))
+
+				By("updating the git repository")
+				replace(path.Join(clonedir, "examples", "Chart.yaml"), "0.1.0", "0.2.0")
+				replace(path.Join(clonedir, "examples", "templates", "deployment.yaml"), "name: sleeper", "name: newsleep")
+
+				commit, err := gh.Update(clone)
+				Expect(err).ToNot(HaveOccurred())
+
+				By("updating the gitrepo's status")
+				expectedStatus := fleet.GitRepoStatus{
+					Commit:       commit,
+					GitJobStatus: "Current",
+					StatusBase: fleet.StatusBase{
+						ReadyClusters:        1,
+						DesiredReadyClusters: 1,
+						Summary: fleet.BundleSummary{
+							NotReady:          0,
+							WaitApplied:       0,
+							ErrApplied:        0,
+							OutOfSync:         0,
+							Modified:          0,
+							Ready:             1,
+							Pending:           0,
+							DesiredReady:      1,
+							NonReadyResources: []fleet.NonReadyResource(nil),
 						},
-						{
-							Type:   "Accepted",
-							Status: "True",
+						Display: fleet.StatusDisplay{
+							ReadyBundleDeployments: "1/1",
+							// XXX: add state and message?
 						},
-						{
-							Type:   "Reconciling",
-							Status: "False",
+						Conditions: []genericcondition.GenericCondition{
+							{
+								Type:   "Ready",
+								Status: "True",
+							},
+							{
+								Type:   "Accepted",
+								Status: "True",
+							},
+							{
+								Type:   "Reconciling",
+								Status: "False",
+							},
+							{
+								Type:   "Stalled",
+								Status: "False",
+							},
 						},
-						{
-							Type:   "Stalled",
-							Status: "False",
+						ResourceCounts: fleet.ResourceCounts{
+							Ready:        1,
+							DesiredReady: 1,
+							WaitApplied:  0,
+							Modified:     0,
+							Orphaned:     0,
+							Missing:      0,
+							Unknown:      0,
+							NotReady:     0,
 						},
 					},
-					ResourceCounts: fleet.ResourceCounts{
-						Ready:        1,
-						DesiredReady: 1,
-						WaitApplied:  0,
-						Modified:     0,
-						Orphaned:     0,
-						Missing:      0,
-						Unknown:      0,
-						NotReady:     0,
-					},
-				},
-			}
-			Eventually(func(g Gomega) {
-				status := getGitRepoStatus(g, k, gitrepoName)
-				g.Expect(status).To(matchGitRepoStatus(expectedStatus))
-			}).Should(Succeed())
-
-			By("checking the deployment's new name")
-			Eventually(func() string {
-				out, _ := k.Namespace(targetNamespace).Get("deployments")
-				return out
-			}).Should(ContainSubstring("newsleep"))
-		})
-	})
-
-	When("updating a git repository monitored via webhook", func() {
-		BeforeEach(func() {
-			repoName = "webhook-test"
-			targetNamespace = testenv.NewNamespaceName("target", r)
-		})
-
-		JustBeforeEach(func() {
-			// Get git server pod name and create post-receive hook script from template
-			var (
-				out string
-				err error
-			)
-			Eventually(func() string {
-				out, err = k.Get("pod", "-l", "app=git-server", "-o", "name")
-				if err != nil {
-					fmt.Printf("%v\n", err)
-					return ""
 				}
-				return out
-			}).Should(ContainSubstring("pod/git-server-"))
-			Expect(err).ToNot(HaveOccurred(), out)
-
-			gitServerPod := strings.TrimPrefix(strings.TrimSpace(out), "pod/")
-
-			hookScript := path.Join(tmpDir, "hook_script")
-
-			err = testenv.Template(hookScript, testenv.AssetPath("gitrepo/post-receive.sh"), struct {
-				RepoURL string
-			}{
-				inClusterRepoURL,
-			})
-			Expect(err).ToNot(HaveOccurred())
-
-			// Create a git repo, erasing a previous repo with the same name if any
-			out, err = k.Run(
-				"exec",
-				gitServerPod,
-				"--",
-				"/bin/sh",
-				"-c",
-				fmt.Sprintf(
-					`dir=/srv/git/%s; rm -rf "$dir"; mkdir -p "$dir"; git init "$dir" --bare; GIT_DIR="$dir" git update-server-info`,
-					repoName,
-				),
-			)
-			Expect(err).ToNot(HaveOccurred(), out)
-
-			// Copy the script into the repo on the server pod
-			hookPathInRepo := fmt.Sprintf("/srv/git/%s/hooks/post-receive", repoName)
-
-			Eventually(func() error {
-				out, err = k.Run("cp", hookScript, fmt.Sprintf("%s:%s", gitServerPod, hookPathInRepo))
-				return err
-			}).Should(Not(HaveOccurred()), out)
-
-			// Make hook script executable
-			Eventually(func() error {
-				out, err = k.Run("exec", gitServerPod, "--", "chmod", "+x", hookPathInRepo)
-				return err
-			}).ShouldNot(HaveOccurred(), out)
-
-			// Clone previously created repo
-			clone, err = gh.Create(clonedir, testenv.AssetPath("gitrepo/sleeper-chart"), "examples")
-			Expect(err).ToNot(HaveOccurred())
-
-			err = testenv.ApplyTemplate(k, testenv.AssetPath("gitrepo/gitrepo.yaml"), struct {
-				Name            string
-				Repo            string
-				Branch          string
-				PollingInterval string
-				TargetNamespace string
-			}{
-				gitrepoName,
-				inClusterRepoURL,
-				gh.Branch,
-				"24h",           // prevent polling
-				targetNamespace, // to avoid conflicts with other tests
+				Eventually(func(g Gomega) {
+					status := getGitRepoStatus(g, k, gitrepoName)
+					g.Expect(status).To(matchGitRepoStatus(expectedStatus))
+				}).Should(Succeed())
+
+				By("checking the deployment's new name")
+				Eventually(func() string {
+					out, _ := k.Namespace(targetNamespace).Get("deployments")
+					return out
+				}).Should(ContainSubstring("newsleep"))
 			})
-			Expect(err).ToNot(HaveOccurred())
 		})
 
-		It("updates the deployment", func() {
-			By("checking the pod exists")
-			Eventually(func() string {
-				out, _ := k.Namespace(targetNamespace).Get("pods")
-				return out
-			}).Should(ContainSubstring("sleeper-"))
+		When("updating a git repository monitored via webhook", func() {
+			BeforeEach(func() {
+				repoName = "webhook-test"
+				targetNamespace = testenv.NewNamespaceName("target", r)
+			})
 
-			By("updating the git repository")
-			replace(path.Join(clonedir, "examples", "Chart.yaml"), "0.1.0", "0.2.0")
-			replace(path.Join(clonedir, "examples", "templates", "deployment.yaml"), "name: sleeper", "name: newsleep")
+			JustBeforeEach(func() {
+				// Get git server pod name and create post-receive hook script from template
+				var (
+					out string
+					err error
+				)
+				Eventually(func() string {
+					out, err = k.Get("pod", "-l", "app=git-server", "-o", "name")
+					if err != nil {
+						fmt.Printf("%v\n", err)
+						return ""
+					}
+					return out
+				}).Should(ContainSubstring("pod/git-server-"))
+				Expect(err).ToNot(HaveOccurred(), out)
+
+				gitServerPod := strings.TrimPrefix(strings.TrimSpace(out), "pod/")
+
+				hookScript := path.Join(tmpDir, "hook_script")
+
+				err = testenv.Template(hookScript, testenv.AssetPath("gitrepo/post-receive.sh"), struct {
+					RepoURL string
+				}{
+					inClusterRepoURL,
+				})
+				Expect(err).ToNot(HaveOccurred())
+
+				// Create a git repo, erasing a previous repo with the same name if any
+				out, err = k.Run(
+					"exec",
+					gitServerPod,
+					"--",
+					"/bin/sh",
+					"-c",
+					fmt.Sprintf(
+						`dir=/srv/git/%s; rm -rf "$dir"; mkdir -p "$dir"; git init "$dir" --bare; GIT_DIR="$dir" git update-server-info`,
+						repoName,
+					),
+				)
+				Expect(err).ToNot(HaveOccurred(), out)
+
+				// Copy the script into the repo on the server pod
+				hookPathInRepo := fmt.Sprintf("/srv/git/%s/hooks/post-receive", repoName)
+
+				Eventually(func() error {
+					out, err = k.Run("cp", hookScript, fmt.Sprintf("%s:%s", gitServerPod, hookPathInRepo))
+					return err
+				}).Should(Not(HaveOccurred()), out)
+
+				// Make hook script executable
+				Eventually(func() error {
+					out, err = k.Run("exec", gitServerPod, "--", "chmod", "+x", hookPathInRepo)
+					return err
+				}).ShouldNot(HaveOccurred(), out)
+
+				// Clone previously created repo
+				clone, err = gh.Create(clonedir, testenv.AssetPath("gitrepo/sleeper-chart"), "examples")
+				Expect(err).ToNot(HaveOccurred())
+
+				err = testenv.ApplyTemplate(k, testenv.AssetPath("gitrepo/gitrepo.yaml"), struct {
+					Name            string
+					Repo            string
+					Branch          string
+					PollingInterval string
+					TargetNamespace string
+				}{
+					gitrepoName,
+					inClusterRepoURL,
+					gh.Branch,
+					"24h",           // prevent polling
+					targetNamespace, // to avoid conflicts with other tests
+				})
+				Expect(err).ToNot(HaveOccurred())
+			})
 
-			commit, err := gh.Update(clone)
-			Expect(err).ToNot(HaveOccurred())
+			It("updates the deployment", func() {
+				if !c.withWebhook {
+					Skip(fmt.Sprintf("webhook tests skipped for %s", c.name))
+				}
 
-			By("updating the gitrepo's status")
-			expectedStatus := fleet.GitRepoStatus{
-				Commit:        commit,
-				WebhookCommit: commit,
-				GitJobStatus:  "Current",
-				StatusBase: fleet.StatusBase{
-					ReadyClusters:        1,
-					DesiredReadyClusters: 1,
-					Summary: fleet.BundleSummary{
-						NotReady:          0,
-						WaitApplied:       0,
-						ErrApplied:        0,
-						OutOfSync:         0,
-						Modified:          0,
-						Ready:             1,
-						Pending:           0,
-						DesiredReady:      1,
-						NonReadyResources: []fleet.NonReadyResource(nil),
-					},
-					Display: fleet.StatusDisplay{
-						ReadyBundleDeployments: "1/1",
-						// XXX: add state and message?
-					},
-					Conditions: []genericcondition.GenericCondition{
-						{
-							Type:   "Ready",
-							Status: "True",
+				By("checking the pod exists")
+				Eventually(func() string {
+					out, _ := k.Namespace(targetNamespace).Get("pods")
+					return out
+				}).Should(ContainSubstring("sleeper-"))
+
+				By("updating the git repository")
+				replace(path.Join(clonedir, "examples", "Chart.yaml"), "0.1.0", "0.2.0")
+				replace(path.Join(clonedir, "examples", "templates", "deployment.yaml"), "name: sleeper", "name: newsleep")
+
+				commit, err := gh.Update(clone)
+				Expect(err).ToNot(HaveOccurred())
+
+				By("updating the gitrepo's status")
+				expectedStatus := fleet.GitRepoStatus{
+					Commit:        commit,
+					WebhookCommit: commit,
+					GitJobStatus:  "Current",
+					StatusBase: fleet.StatusBase{
+						ReadyClusters:        1,
+						DesiredReadyClusters: 1,
+						Summary: fleet.BundleSummary{
+							NotReady:          0,
+							WaitApplied:       0,
+							ErrApplied:        0,
+							OutOfSync:         0,
+							Modified:          0,
+							Ready:             1,
+							Pending:           0,
+							DesiredReady:      1,
+							NonReadyResources: []fleet.NonReadyResource(nil),
 						},
-						{
-							Type:   "Accepted",
-							Status: "True",
+						Display: fleet.StatusDisplay{
+							ReadyBundleDeployments: "1/1",
+							// XXX: add state and message?
 						},
-						{
-							Type:   "Reconciling",
-							Status: "False",
+						Conditions: []genericcondition.GenericCondition{
+							{
+								Type:   "Ready",
+								Status: "True",
+							},
+							{
+								Type:   "Accepted",
+								Status: "True",
+							},
+							{
+								Type:   "Reconciling",
+								Status: "False",
+							},
+							{
+								Type:   "Stalled",
+								Status: "False",
+							},
 						},
-						{
-							Type:   "Stalled",
-							Status: "False",
+						ResourceCounts: fleet.ResourceCounts{
+							Ready:        1,
+							DesiredReady: 1,
+							WaitApplied:  0,
+							Modified:     0,
+							Orphaned:     0,
+							Missing:      0,
+							Unknown:      0,
+							NotReady:     0,
 						},
 					},
-					ResourceCounts: fleet.ResourceCounts{
-						Ready:        1,
-						DesiredReady: 1,
-						WaitApplied:  0,
-						Modified:     0,
-						Orphaned:     0,
-						Missing:      0,
-						Unknown:      0,
-						NotReady:     0,
-					},
-				},
-			}
-			Eventually(func(g Gomega) {
-				status := getGitRepoStatus(g, k, gitrepoName)
-				g.Expect(status).To(matchGitRepoStatus(expectedStatus))
-
-			}).Should(Succeed())
-
-			By("checking the deployment's new name")
-			Eventually(func() string {
-				out, _ := k.Namespace(targetNamespace).Get("deployments")
-				return out
-			}).Should(ContainSubstring("newsleep"))
-		}, Label("webhook"))
-	})
+				}
+				Eventually(func(g Gomega) {
+					status := getGitRepoStatus(g, k, gitrepoName)
+					g.Expect(status).To(matchGitRepoStatus(expectedStatus))
+
+				}).Should(Succeed())
+
+				By("checking the deployment's new name")
+				Eventually(func() string {
+					out, _ := k.Namespace(targetNamespace).Get("deployments")
+					return out
+				}).Should(ContainSubstring("newsleep"))
+			}, Label("webhook"))
+		})
+	}
 })
 
 // replace replaces string s with r in the file located at path. That file must exist and be writable.
diff --git a/e2e/single-cluster/helm_auth_test.go b/e2e/single-cluster/helm_auth_test.go
index 57550d6a..caff82f2 100644
--- a/e2e/single-cluster/helm_auth_test.go
+++ b/e2e/single-cluster/helm_auth_test.go
@@ -13,13 +13,14 @@ import (
 	. "github.com/onsi/gomega"
 )
 
-var _ = Describe("Single Cluster Examples", Label("infra-setup"), func() {
+var _ = Describe("GitRepo using Helm chart with auth", Label("infra-setup"), func() {
 	var (
 		gitRepoPath     string
 		tmpdir          string
 		k               kubectl.Command
 		gh              *githelper.Git
 		targetNamespace string
+		helmSecretName  string
 	)
 
 	JustBeforeEach(func() {
@@ -37,7 +38,7 @@ var _ = Describe("Single Cluster Examples", Label("infra-setup"), func() {
 		}{
 			inClusterRepoURL,
 			gitRepoPath,
-			"helm-secret",
+			helmSecretName,
 		})
 		Expect(err).ToNot(HaveOccurred())
 
@@ -49,6 +50,7 @@ var _ = Describe("Single Cluster Examples", Label("infra-setup"), func() {
 		Context("containing a private OCI-based helm chart", Label("oci-registry"), func() {
 			BeforeEach(func() {
 				gitRepoPath = "oci-with-auth"
+				helmSecretName = "helm-secret"
 				targetNamespace = fmt.Sprintf("fleet-helm-%s", gitRepoPath)
 				k = env.Kubectl.Namespace(env.Namespace)
 
@@ -62,9 +64,38 @@ var _ = Describe("Single Cluster Examples", Label("infra-setup"), func() {
 				}).Should(ContainSubstring("sleeper-"))
 			})
 		})
+		Context("containing a private HTTP-based helm chart with repo path and no CA bundle in secret", Label("helm-registry"), func() {
+			BeforeEach(func() {
+				gitRepoPath = "http-with-auth-repo-path"
+				helmSecretName = "helm-secret-no-ca"
+				targetNamespace = fmt.Sprintf("fleet-helm-%s", gitRepoPath)
+				k = env.Kubectl.Namespace(env.Namespace)
+
+				// No CA bundle in this secret
+				out, err := k.Create(
+					"secret", "generic", helmSecretName,
+					"--from-literal=username="+os.Getenv("CI_OCI_USERNAME"),
+					"--from-literal=password="+os.Getenv("CI_OCI_PASSWORD"),
+				)
+				Expect(err).ToNot(HaveOccurred(), out)
+				tmpdir, gh = setupGitRepo(gitRepoPath, repoName, port)
+
+				DeferCleanup(func() {
+					_, _ = k.Delete("secret", helmSecretName)
+				})
+			})
+
+			It("deploys the helm chart", func() {
+				Eventually(func() string {
+					out, _ := k.Namespace(targetNamespace).Get("pods", "--field-selector=status.phase==Running")
+					return out
+				}).Should(ContainSubstring("sleeper-"))
+			})
+		})
 		Context("containing a private HTTP-based helm chart with repo path", Label("helm-registry"), func() {
 			BeforeEach(func() {
 				gitRepoPath = "http-with-auth-repo-path"
+				helmSecretName = "helm-secret"
 				targetNamespace = fmt.Sprintf("fleet-helm-%s", gitRepoPath)
 				k = env.Kubectl.Namespace(env.Namespace)
 
@@ -81,6 +112,7 @@ var _ = Describe("Single Cluster Examples", Label("infra-setup"), func() {
 		Context("containing a private HTTP-based helm chart with chart path", Label("helm-registry"), func() {
 			BeforeEach(func() {
 				gitRepoPath = "http-with-auth-chart-path"
+				helmSecretName = "helm-secret"
 				targetNamespace = fmt.Sprintf("fleet-helm-%s", gitRepoPath)
 				k = env.Kubectl.Namespace(env.Namespace)
 
@@ -106,6 +138,7 @@ var _ = Describe("Single Cluster Examples", Label("infra-setup"), func() {
 
 // setupGitRepo creates a local clone with data from repoPath and pushes it to that same path on the git server,
 // within a common repository named `repo`.
+// nolint: unparam // repo and port are always fed with the same value - for now.
 func setupGitRepo(repoPath, repo string, port int) (tmpdir string, gh *githelper.Git) {
 	addr, err := githelper.GetExternalRepoAddr(env, port, repo)
 	Expect(err).ToNot(HaveOccurred())
diff --git a/e2e/testenv/githelper/git.go b/e2e/testenv/githelper/git.go
index a1c30419..7f7394cb 100644
--- a/e2e/testenv/githelper/git.go
+++ b/e2e/testenv/githelper/git.go
@@ -210,6 +210,9 @@ func (g *Git) Create(repodir string, from string, subdir string) (*git.Repositor
 		Progress: os.Stdout,
 		// Force push, so our initial state is deterministic
 		RefSpecs: []config.RefSpec{config.RefSpec("+refs/heads/master:refs/heads/" + g.Branch)},
+		// This prevents IP SANs from being needed on the git server cert; the TLS verification we are most
+		// interested in is the one happening in Fleet's controllers and jobs.
+		InsecureSkipTLS: true,
 	}
 	k, err := g.Auth.getKeys()
 	if err != nil {
@@ -246,6 +249,9 @@ func (g *Git) Update(repo *git.Repository) (string, error) {
 	po := git.PushOptions{
 		Progress: os.Stdout,
 		RefSpecs: []config.RefSpec{config.RefSpec("refs/heads/master:refs/heads/" + g.Branch)},
+		// This prevents IP SANs from being needed on the git server cert; the TLS verification we are most
+		// interested in is the one happening in Fleet's controllers and jobs.
+		InsecureSkipTLS: true,
 	}
 	k, err := g.Auth.getKeys()
 	if err != nil {
diff --git a/integrationtests/gitjob/controller/controller_test.go b/integrationtests/gitjob/controller/controller_test.go
index 7dc9614c..ae10828d 100644
--- a/integrationtests/gitjob/controller/controller_test.go
+++ b/integrationtests/gitjob/controller/controller_test.go
@@ -102,15 +102,15 @@ var _ = Describe("GitJob controller", func() {
 
 				var sa corev1.ServiceAccount
 				g.Expect(k8sClient.Get(ctx, ns, &sa)).ToNot(HaveOccurred())
-				Expect(sa.ObjectMeta).To(beOwnedBy(gitRepoOwnerRef))
+				g.Expect(sa.ObjectMeta).To(beOwnedBy(gitRepoOwnerRef))
 
 				var ro rbacv1.Role
 				g.Expect(k8sClient.Get(ctx, ns, &ro)).ToNot(HaveOccurred())
-				Expect(ro.ObjectMeta).To(beOwnedBy(gitRepoOwnerRef))
+				g.Expect(ro.ObjectMeta).To(beOwnedBy(gitRepoOwnerRef))
 
 				var rb rbacv1.RoleBinding
 				g.Expect(k8sClient.Get(ctx, ns, &rb)).ToNot(HaveOccurred())
-				Expect(rb.ObjectMeta).To(beOwnedBy(gitRepoOwnerRef))
+				g.Expect(rb.ObjectMeta).To(beOwnedBy(gitRepoOwnerRef))
 			}).Should(Succeed())
 		})
 
@@ -190,7 +190,8 @@ var _ = Describe("GitJob controller", func() {
 				caBundle = nil
 			})
 
-			It("does not create a secret for the CA bundle", func() {
+			It("does not create secrets", func() {
+				By("not creating a secret for the CA bundle")
 				secretName := fmt.Sprintf("%s-cabundle", gitRepoName)
 				ns := types.NamespacedName{Name: secretName, Namespace: gitRepo.Namespace}
 				var secret corev1.Secret
@@ -201,6 +202,137 @@ var _ = Describe("GitJob controller", func() {
 					g.Expect(err).To(HaveOccurred())
 					g.Expect(errors.IsNotFound(err)).To(BeTrue(), err)
 				}, time.Second*5, time.Second*1).Should(Succeed())
+
+				By("not creating a secret for the Helm client")
+				secretName = fmt.Sprintf("%s-rancher-cabundle", gitRepoName)
+				ns = types.NamespacedName{Name: secretName, Namespace: gitRepo.Namespace}
+
+				Consistently(func(g Gomega) {
+					err := k8sClient.Get(ctx, ns, &secret)
+
+					g.Expect(err).To(HaveOccurred())
+					g.Expect(errors.IsNotFound(err)).To(BeTrue(), err)
+				}, time.Second*5, time.Second*1).Should(Succeed())
+			})
+		})
+
+		When("a job is created without a specified CA bundle, but Rancher has secrets containing a CA bundle", func() {
+			BeforeEach(func() {
+				gitRepoName = "rancher-ca-bundle"
+				caBundle = nil
+
+				rancherSecret := corev1.Secret{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "tls-ca-additional",
+						Namespace: "cattle-system",
+					},
+					Data: map[string][]byte{
+						"ca-additional.pem": []byte("foo"),
+					},
+				}
+				err := k8sClient.Create(ctx, &rancherSecret)
+				Expect(err).ToNot(HaveOccurred())
+
+				DeferCleanup(func() {
+					_ = k8sClient.Delete(ctx, &rancherSecret)
+				})
+			})
+
+			It("creates secrets for the CA bundle", func() {
+				By("creating a CA bundle secret for the git cloner")
+				secretName := fmt.Sprintf("%s-cabundle", gitRepoName)
+				ns := types.NamespacedName{Name: secretName, Namespace: gitRepo.Namespace}
+				var secret corev1.Secret
+
+				gitRepoOwnerRef := metav1.OwnerReference{
+					Kind:       "GitRepo",
+					APIVersion: "fleet.cattle.io/v1alpha1",
+					Name:       gitRepoName,
+				}
+
+				Eventually(func(g Gomega) {
+					err := k8sClient.Get(ctx, ns, &secret)
+
+					g.Expect(err).ToNot(HaveOccurred())
+					data, ok := secret.Data["additional-ca.crt"]
+					g.Expect(ok).To(BeTrue())
+					g.Expect(data).To(Equal([]byte("foo")))
+					g.Expect(secret.ObjectMeta).To(beOwnedBy(gitRepoOwnerRef))
+				}, time.Second*5, time.Second*1).Should(Succeed())
+
+				By("creating a CA bundle secret for the Helm client")
+				secretName = fmt.Sprintf("%s-rancher-cabundle", gitRepoName)
+				ns = types.NamespacedName{Name: secretName, Namespace: gitRepo.Namespace}
+
+				Eventually(func(g Gomega) {
+					err := k8sClient.Get(ctx, ns, &secret)
+
+					g.Expect(err).ToNot(HaveOccurred())
+					data, ok := secret.Data["cacerts"]
+					g.Expect(ok).To(BeTrue())
+					g.Expect(data).To(Equal([]byte("foo")))
+					g.Expect(secret.ObjectMeta).To(beOwnedBy(gitRepoOwnerRef))
+				}, time.Second*5, time.Second*1).Should(Succeed())
+			})
+		})
+
+		When("a job is created with a CA bundle, without Helm secrets, and Rancher has secrets containing a CA bundle", func() {
+			BeforeEach(func() {
+				gitRepoName = "with-ca-bundle-and-rancher-secrets"
+				caBundle = []byte("LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tZm9vLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo=")
+
+				rancherSecret := corev1.Secret{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "tls-ca-additional",
+						Namespace: "cattle-system",
+					},
+					Data: map[string][]byte{
+						"ca-additional.pem": []byte("foo"),
+					},
+				}
+				err := k8sClient.Create(ctx, &rancherSecret)
+				Expect(err).ToNot(HaveOccurred())
+
+				DeferCleanup(func() {
+					_ = k8sClient.Delete(ctx, &rancherSecret)
+				})
+			})
+
+			It("creates secrets for the CA bundle", func() {
+				By("creating a secret for the gitrepo CA bundle")
+				gitRepoOwnerRef := metav1.OwnerReference{
+					Kind:       "GitRepo",
+					APIVersion: "fleet.cattle.io/v1alpha1",
+					Name:       gitRepoName,
+				}
+
+				secretName := fmt.Sprintf("%s-cabundle", gitRepoName)
+				ns := types.NamespacedName{Name: secretName, Namespace: gitRepo.Namespace}
+				var secret corev1.Secret
+
+				Eventually(func(g Gomega) {
+					err := k8sClient.Get(ctx, ns, &secret)
+					g.Expect(err).ToNot(HaveOccurred())
+					g.Expect(secret.ObjectMeta).To(beOwnedBy(gitRepoOwnerRef))
+
+					data, ok := secret.Data["additional-ca.crt"]
+					g.Expect(ok).To(BeTrue())
+					g.Expect(data).To(Equal(gitRepo.Spec.CABundle))
+				}, time.Second*5, time.Second*1).Should(Succeed())
+
+				By("creating a secret for the Helm client CA bundle")
+				secretName = fmt.Sprintf("%s-rancher-cabundle", gitRepoName)
+				ns = types.NamespacedName{Name: secretName, Namespace: gitRepo.Namespace}
+
+				Eventually(func(g Gomega) {
+					err := k8sClient.Get(ctx, ns, &secret)
+
+					g.Expect(err).ToNot(HaveOccurred())
+					data, ok := secret.Data["cacerts"]
+					g.Expect(ok).To(BeTrue())
+					g.Expect(data).To(Equal([]byte("foo")))
+					g.Expect(secret.ObjectMeta).To(beOwnedBy(gitRepoOwnerRef))
+				}, time.Second*5, time.Second*1).Should(Succeed())
 			})
 		})
 
@@ -224,7 +356,7 @@ var _ = Describe("GitJob controller", func() {
 				Eventually(func(g Gomega) {
 					err := k8sClient.Get(ctx, ns, &secret)
 					g.Expect(err).ToNot(HaveOccurred())
-					Expect(secret.ObjectMeta).To(beOwnedBy(gitRepoOwnerRef))
+					g.Expect(secret.ObjectMeta).To(beOwnedBy(gitRepoOwnerRef))
 
 					data, ok := secret.Data["additional-ca.crt"]
 					g.Expect(ok).To(BeTrue())
@@ -754,6 +886,211 @@ var _ = Describe("GitJob controller", func() {
 		})
 	})
 
+	When("creating a gitRepo that references a helm secret", func() {
+		var (
+			gitRepo        v1alpha1.GitRepo
+			gitRepoName    string
+			helmSecretName string
+		)
+
+		JustBeforeEach(func() {
+			expectedCommit = commit
+			gitRepo = createGitRepo(gitRepoName)
+			gitRepo.Spec.HelmSecretName = helmSecretName
+			// Create should not return an error
+			err := k8sClient.Create(ctx, &gitRepo)
+			Expect(err).ToNot(HaveOccurred())
+		})
+
+		AfterEach(func() {
+			// delete the gitrepo and wait until it is deleted
+			waitDeleteGitrepo(gitRepo)
+			// reset the logs buffer so we don't read logs from previous tests
+			logsBuffer.Reset()
+		})
+
+		Context("helmSecretName secret exists and so does a Rancher CA bundle secret", func() {
+			BeforeEach(func() {
+				helmSecretName = "my-helm-secret"
+				gitRepoName = "helm-secret-and-rancher-ca-bundle"
+			})
+
+			JustBeforeEach(func() {
+				helmSecret := corev1.Secret{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      helmSecretName,
+						Namespace: gitRepo.Namespace,
+					},
+					Data: map[string][]byte{
+						"cacerts": []byte("foo"),
+					},
+				}
+				err := k8sClient.Create(ctx, &helmSecret)
+				Expect(err).ToNot(HaveOccurred())
+
+				rancherSecret := corev1.Secret{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "tls-ca-additional",
+						Namespace: "cattle-system",
+					},
+					Data: map[string][]byte{
+						"ca-additional.pem": []byte("foo"),
+					},
+				}
+				err = k8sClient.Create(ctx, &rancherSecret)
+				Expect(err).ToNot(HaveOccurred())
+
+				DeferCleanup(func() {
+					_ = k8sClient.Delete(ctx, &rancherSecret)
+					_ = k8sClient.Delete(ctx, &helmSecret)
+				})
+			})
+
+			It("creates RBAC resources", func() {
+				gitRepoOwnerRef := metav1.OwnerReference{
+					Kind:       "GitRepo",
+					APIVersion: "fleet.cattle.io/v1alpha1",
+					Name:       gitRepoName,
+				}
+
+				Eventually(func(g Gomega) {
+					saName := names.SafeConcatName("git", gitRepo.Name)
+					ns := types.NamespacedName{Name: saName, Namespace: gitRepo.Namespace}
+
+					var sa corev1.ServiceAccount
+					g.Expect(k8sClient.Get(ctx, ns, &sa)).ToNot(HaveOccurred())
+					g.Expect(sa.ObjectMeta).To(beOwnedBy(gitRepoOwnerRef))
+
+					var ro rbacv1.Role
+					g.Expect(k8sClient.Get(ctx, ns, &ro)).ToNot(HaveOccurred())
+					g.Expect(ro.ObjectMeta).To(beOwnedBy(gitRepoOwnerRef))
+
+					var rb rbacv1.RoleBinding
+					g.Expect(k8sClient.Get(ctx, ns, &rb)).ToNot(HaveOccurred())
+					g.Expect(rb.ObjectMeta).To(beOwnedBy(gitRepoOwnerRef))
+				}).Should(Succeed())
+			})
+
+			It("creates the job", func() {
+				var job batchv1.Job
+				Eventually(func() error {
+					var gitRepoFromCluster v1alpha1.GitRepo
+					err := k8sClient.Get(ctx, types.NamespacedName{Name: gitRepo.Name, Namespace: gitRepo.Namespace}, &gitRepoFromCluster)
+					if err != nil {
+						return err
+					}
+					jobName := names.SafeConcatName(gitRepoName, names.Hex(repo+commit, 5))
+					return k8sClient.Get(ctx, types.NamespacedName{Name: jobName, Namespace: gitRepoNamespace}, &job)
+				}).Should(Not(HaveOccurred()))
+			})
+
+			It("does not create a CA bundle secret for the Helm client", func() {
+				secretName := fmt.Sprintf("%s-rancher-cabundle", gitRepoName)
+				ns := types.NamespacedName{Name: secretName, Namespace: gitRepo.Namespace}
+
+				var secret corev1.Secret
+
+				Consistently(func(g Gomega) {
+					err := k8sClient.Get(ctx, ns, &secret)
+
+					g.Expect(err).To(HaveOccurred())
+					g.Expect(errors.IsNotFound(err)).To(BeTrue(), err)
+				}, time.Second*5, time.Second*1).Should(Succeed())
+			})
+		})
+
+		Context("helmSecretName secret exists, without a CA bundle, and a Rancher CA bundle secret exists", func() {
+			BeforeEach(func() {
+				helmSecretName = "my-helm-secret"
+				gitRepoName = "helm-secret-no-cacert-and-rancher-ca-bundle"
+			})
+
+			JustBeforeEach(func() {
+				helmSecret := corev1.Secret{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      helmSecretName,
+						Namespace: gitRepo.Namespace,
+					},
+					Data: map[string][]byte{
+						"username": []byte("foo"),
+						"password": []byte("bar"),
+						// no CA cert here
+					},
+				}
+				err := k8sClient.Create(ctx, &helmSecret)
+				Expect(err).ToNot(HaveOccurred())
+
+				rancherSecret := corev1.Secret{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "tls-ca-additional",
+						Namespace: "cattle-system",
+					},
+					Data: map[string][]byte{
+						"ca-additional.pem": []byte("foo"),
+					},
+				}
+				err = k8sClient.Create(ctx, &rancherSecret)
+				Expect(err).ToNot(HaveOccurred())
+
+				DeferCleanup(func() {
+					_ = k8sClient.Delete(ctx, &rancherSecret)
+					_ = k8sClient.Delete(ctx, &helmSecret)
+				})
+			})
+
+			It("creates RBAC resources", func() {
+				gitRepoOwnerRef := metav1.OwnerReference{
+					Kind:       "GitRepo",
+					APIVersion: "fleet.cattle.io/v1alpha1",
+					Name:       gitRepoName,
+				}
+
+				Eventually(func(g Gomega) {
+					saName := names.SafeConcatName("git", gitRepo.Name)
+					ns := types.NamespacedName{Name: saName, Namespace: gitRepo.Namespace}
+
+					var sa corev1.ServiceAccount
+					g.Expect(k8sClient.Get(ctx, ns, &sa)).ToNot(HaveOccurred())
+					g.Expect(sa.ObjectMeta).To(beOwnedBy(gitRepoOwnerRef))
+
+					var ro rbacv1.Role
+					g.Expect(k8sClient.Get(ctx, ns, &ro)).ToNot(HaveOccurred())
+					g.Expect(ro.ObjectMeta).To(beOwnedBy(gitRepoOwnerRef))
+
+					var rb rbacv1.RoleBinding
+					g.Expect(k8sClient.Get(ctx, ns, &rb)).ToNot(HaveOccurred())
+					g.Expect(rb.ObjectMeta).To(beOwnedBy(gitRepoOwnerRef))
+				}).Should(Succeed())
+			})
+
+			It("creates the job", func() {
+				var job batchv1.Job
+				Eventually(func() error {
+					var gitRepoFromCluster v1alpha1.GitRepo
+					err := k8sClient.Get(ctx, types.NamespacedName{Name: gitRepo.Name, Namespace: gitRepo.Namespace}, &gitRepoFromCluster)
+					if err != nil {
+						return err
+					}
+					jobName := names.SafeConcatName(gitRepoName, names.Hex(repo+commit, 5))
+					return k8sClient.Get(ctx, types.NamespacedName{Name: jobName, Namespace: gitRepoNamespace}, &job)
+				}).Should(Not(HaveOccurred()))
+			})
+
+			It("creates a CA bundle secret for the Helm client", func() {
+				secretName := fmt.Sprintf("%s-rancher-cabundle", gitRepoName)
+				ns := types.NamespacedName{Name: secretName, Namespace: gitRepo.Namespace}
+
+				var secret corev1.Secret
+
+				Eventually(func(g Gomega) {
+					err := k8sClient.Get(ctx, ns, &secret)
+
+					g.Expect(err).ToNot(HaveOccurred())
+				}, time.Second*5, time.Second*1).Should(Succeed())
+			})
+		})
+	})
+
 	When("creating a gitRepo that references a nonexistent helm secret", func() {
 		var (
 			gitRepo                v1alpha1.GitRepo
@@ -768,7 +1105,7 @@ var _ = Describe("GitJob controller", func() {
 			gitRepo = createGitRepo(gitRepoName)
 			gitRepo.Spec.HelmSecretNameForPaths = helmSecretNameForPaths
 			gitRepo.Spec.HelmSecretName = helmSecretName
-			// Create should return an error
+			// Create should not return an error
 			err := k8sClient.Create(ctx, &gitRepo)
 			Expect(err).ToNot(HaveOccurred())
 		})
diff --git a/integrationtests/gitjob/controller/suite_test.go b/integrationtests/gitjob/controller/suite_test.go
index baeeadab..12221df9 100644
--- a/integrationtests/gitjob/controller/suite_test.go
+++ b/integrationtests/gitjob/controller/suite_test.go
@@ -23,6 +23,7 @@ import (
 	v1alpha1 "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1"
 	"github.com/rancher/fleet/pkg/git/mocks"
 
+	corev1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/types"
@@ -141,6 +142,21 @@ var _ = BeforeSuite(func() {
 		err = mgr.Start(ctx)
 		Expect(err).ToNot(HaveOccurred(), "failed to run manager")
 	}()
+
+	err = k8sClient.Create(ctx, &corev1.Namespace{
+		ObjectMeta: metav1.ObjectMeta{
+			Name: "cattle-system",
+		},
+	})
+	Expect(err).ToNot(HaveOccurred())
+
+	DeferCleanup(func() {
+		_ = k8sClient.Delete(ctx, &corev1.Namespace{
+			ObjectMeta: metav1.ObjectMeta{
+				Name: "cattle-system",
+			},
+		})
+	})
 })
 
 var _ = AfterSuite(func() {
diff --git a/integrationtests/gitjob/git/git_test.go b/integrationtests/gitjob/git/git_test.go
index 628f206f..3fa89d2a 100644
--- a/integrationtests/gitjob/git/git_test.go
+++ b/integrationtests/gitjob/git/git_test.go
@@ -117,7 +117,9 @@ func TestLatestCommit_NoAuth(t *testing.T) {
 		t.Run(name, func(t *testing.T) {
 			f := git.Fetch{}
 			client := mocks.NewMockClient(ctlr)
-			client.EXPECT().Get(ctx, gomock.Any(), gomock.Any()).Return(kerrors.NewNotFound(schema.GroupResource{}, "notfound"))
+			// May be called multiple times, including calls to get Rancher CA bundle secrets
+			client.EXPECT().Get(ctx, gomock.Any(), gomock.Any()).
+				Return(kerrors.NewNotFound(schema.GroupResource{}, "notfound")).AnyTimes()
 			latestCommit, err := f.LatestCommit(ctx, test.gitrepo, client)
 			if test.expectedErr == nil {
 				require.NoError(t, err)
@@ -343,7 +345,9 @@ func TestLatestCommit_Revision(t *testing.T) {
 		t.Run(name, func(t *testing.T) {
 			f := git.Fetch{}
 			client := mocks.NewMockClient(ctlr)
-			client.EXPECT().Get(ctx, gomock.Any(), gomock.Any()).Return(kerrors.NewNotFound(schema.GroupResource{}, "notfound"))
+			// May be called multiple times, including calls to get Rancher CA bundle secrets
+			client.EXPECT().Get(ctx, gomock.Any(), gomock.Any()).
+				Return(kerrors.NewNotFound(schema.GroupResource{}, "notfound")).AnyTimes()
 			latestCommit, err := f.LatestCommit(ctx, test.gitrepo, client)
 			if test.expectedCommit != latestCommit {
 				t.Errorf("latestCommit doesn't match. got %s, expected %s", latestCommit, test.expectedCommit)
diff --git a/internal/cmd/controller/gitops/reconciler/gitjob_test.go b/internal/cmd/controller/gitops/reconciler/gitjob_test.go
index d4dcf720..9eb47933 100644
--- a/internal/cmd/controller/gitops/reconciler/gitjob_test.go
+++ b/internal/cmd/controller/gitops/reconciler/gitjob_test.go
@@ -675,12 +675,14 @@ func TestNewJob(t *testing.T) { // nolint:funlen
 	defer mockCtrl.Finish()
 	scheme := runtime.NewScheme()
 	utilruntime.Must(batchv1.AddToScheme(scheme))
+	utilruntime.Must(fleetv1.AddToScheme(scheme))
 	ctx := context.TODO()
 
 	tests := map[string]struct {
 		gitrepo                *fleetv1.GitRepo
 		client                 client.Client
 		expectedInitContainers []corev1.Container
+		expectedContainers     []corev1.Container
 		expectedVolumes        []corev1.Volume
 		expectedErr            error
 	}{
@@ -945,6 +947,10 @@ func TestNewJob(t *testing.T) { // nolint:funlen
 		},
 		"custom CA": {
 			gitrepo: &fleetv1.GitRepo{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "test-custom-ca",
+					Namespace: "test-ns",
+				},
 				Spec: fleetv1.GitRepoSpec{
 					CABundle: []byte("ca"),
 					Repo:     "repo",
@@ -1000,11 +1006,136 @@ func TestNewJob(t *testing.T) { // nolint:funlen
 					Name: bundleCAVolumeName,
 					VolumeSource: corev1.VolumeSource{
 						Secret: &corev1.SecretVolumeSource{
-							SecretName: "-cabundle",
+							SecretName: "test-custom-ca-cabundle",
 						},
 					},
 				},
 			},
+			client: fake.NewFakeClient(&corev1.Secret{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "test-custom-ca-cabundle",
+					Namespace: "test-ns",
+				},
+				Data: map[string][]byte{
+					"cacerts": []byte("foo"),
+				},
+				Type: corev1.SecretTypeSSHAuth,
+			}),
+		},
+		"no custom CA but Rancher CA secret exists": {
+			gitrepo: &fleetv1.GitRepo{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "test-rancher-custom-ca",
+					Namespace: "test-ns",
+				},
+				Spec: fleetv1.GitRepoSpec{
+					Repo: "repo",
+				},
+			},
+			expectedInitContainers: []corev1.Container{
+				{
+					Command: []string{
+						"fleet",
+					},
+					Args: []string{
+						"gitcloner",
+						"repo",
+						"/workspace",
+						"--branch",
+						"master",
+						"--ca-bundle-file",
+						"/gitjob/cabundle/" + bundleCAFile,
+					},
+					Image: "test",
+					Name:  "gitcloner-initializer",
+					VolumeMounts: []corev1.VolumeMount{
+						{
+							Name:      gitClonerVolumeName,
+							MountPath: "/workspace",
+						},
+						{
+							Name:      emptyDirVolumeName,
+							MountPath: "/tmp",
+						},
+						{
+							Name:      bundleCAVolumeName,
+							MountPath: "/gitjob/cabundle",
+						},
+					},
+					SecurityContext: securityContext,
+				},
+			},
+			expectedVolumes: []corev1.Volume{
+				{
+					Name: gitClonerVolumeName,
+					VolumeSource: corev1.VolumeSource{
+						EmptyDir: &corev1.EmptyDirVolumeSource{},
+					},
+				},
+				{
+					Name: emptyDirVolumeName,
+					VolumeSource: corev1.VolumeSource{
+						EmptyDir: &corev1.EmptyDirVolumeSource{},
+					},
+				},
+				{
+					Name: bundleCAVolumeName,
+					VolumeSource: corev1.VolumeSource{
+						Secret: &corev1.SecretVolumeSource{
+							SecretName: "test-rancher-custom-ca-cabundle",
+						},
+					},
+				},
+				{
+					Name: "rancher-helm-secret-cert",
+					VolumeSource: corev1.VolumeSource{
+						Secret: &corev1.SecretVolumeSource{
+							SecretName: "test-rancher-custom-ca-rancher-cabundle",
+							Items: []corev1.KeyToPath{
+								{
+									Key:  "cacerts",
+									Path: "cacert.crt",
+								},
+							},
+						},
+					},
+				},
+			},
+			expectedContainers: []corev1.Container{
+				{
+					Args: []string{
+						"--cacerts-file",
+						"/etc/rancher/certs/cacerts",
+					},
+					Name: "fleet",
+					VolumeMounts: []corev1.VolumeMount{
+						{
+							Name:      "rancher-helm-secret-cert",
+							MountPath: "/etc/ssl/certs",
+						},
+					},
+				},
+			},
+			client: fake.NewFakeClient(
+				&corev1.Secret{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "tls-ca-additional",
+						Namespace: "cattle-system",
+					},
+					Data: map[string][]byte{
+						"ca-additional.pem": []byte("foo"),
+					},
+				},
+				&corev1.Secret{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "test-rancher-custom-ca-cabundle",
+						Namespace: "test-ns",
+					},
+					Data: map[string][]byte{
+						"additional-ca.crt": []byte("foo"),
+					},
+				},
+			),
 		},
 		"skip tls": {
 			gitrepo: &fleetv1.GitRepo{
@@ -1055,6 +1186,7 @@ func TestNewJob(t *testing.T) { // nolint:funlen
 					},
 				},
 			},
+			client: fake.NewFakeClient(),
 		},
 	}
 
@@ -1070,8 +1202,50 @@ func TestNewJob(t *testing.T) { // nolint:funlen
 			if err != nil {
 				t.Fatalf("unexpected error: %v", err)
 			}
+
 			if !cmp.Equal(job.Spec.Template.Spec.InitContainers, test.expectedInitContainers) {
-				t.Fatalf("expected initContainers: %v, got: %v", test.expectedInitContainers, job.Spec.Template.Spec.InitContainers)
+				t.Fatalf("expected initContainers:\n\t%v,\n got:\n\t%v", test.expectedInitContainers, job.Spec.Template.Spec.InitContainers)
+			}
+
+			for _, eCont := range test.expectedContainers {
+				found := false
+				for _, cont := range job.Spec.Template.Spec.Containers {
+					if cont.Name == eCont.Name {
+						found = true
+
+						for _, eArg := range eCont.Args {
+							argFound := false
+							for _, arg := range cont.Args {
+								if arg == eArg {
+									argFound = true
+									break
+								}
+							}
+							if !argFound {
+								t.Fatalf("expected arg %q not found in container %s with args %#v", eArg, eCont.Name, cont.Args)
+							}
+						}
+
+						for _, eVM := range eCont.VolumeMounts {
+							vmFound := false
+							for _, vm := range cont.VolumeMounts {
+								if vm.Name != eVM.Name {
+									continue
+								}
+								vmFound = true
+								if vm != eVM {
+									t.Fatalf("expected volume mount %v in container %s, got %v", eVM, eCont.Name, vm)
+								}
+							}
+							if !vmFound {
+								t.Fatalf("expected volume mount %v not found in container %s", eVM, eCont.Name)
+							}
+						}
+					}
+				}
+				if !found {
+					t.Fatalf("expected container %s not found", eCont.Name)
+				}
 			}
 
 			for _, evol := range test.expectedVolumes {
@@ -1083,7 +1257,7 @@ func TestNewJob(t *testing.T) { // nolint:funlen
 					}
 				}
 				if !found {
-					t.Fatalf("volume %v not found in %v", evol, job.Spec.Template.Spec.Volumes)
+					t.Fatalf("volume %v not found in \n\t%v", evol, job.Spec.Template.Spec.Volumes)
 				}
 			}
 		})
diff --git a/pkg/cert/cabundle_test.go b/pkg/cert/cabundle_test.go
new file mode 100644
index 00000000..207abfbe
--- /dev/null
+++ b/pkg/cert/cabundle_test.go
@@ -0,0 +1,221 @@
+package cert_test
+
+import (
+	"context"
+	"fmt"
+	"testing"
+
+	"go.uber.org/mock/gomock"
+	corev1 "k8s.io/api/core/v1"
+	k8serrors "k8s.io/apimachinery/pkg/api/errors"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/types"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+
+	"github.com/rancher/fleet/internal/mocks"
+	"github.com/rancher/fleet/pkg/cert"
+)
+
+func TestGetRancherCABundle(t *testing.T) {
+	notFoundErr := &k8serrors.StatusError{ErrStatus: metav1.Status{Reason: metav1.StatusReasonNotFound}}
+	testCases := []struct {
+		name           string
+		secretGets     func(c *mocks.MockClient)
+		expectedBundle []byte
+		expectedErr    error
+	}{
+		{
+			name: "no secrets found",
+			secretGets: func(c *mocks.MockClient) {
+				c.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(
+					func(ctx context.Context, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error {
+						// nothing done: object stays nil
+						return notFoundErr
+					}).Times(2)
+			},
+			expectedBundle: nil,
+			expectedErr:    nil,
+		},
+		{
+			name: "tls-ca exists but tls-ca-additional does not",
+			secretGets: func(c *mocks.MockClient) {
+				c.EXPECT().Get(
+					gomock.Any(),
+					types.NamespacedName{Namespace: "cattle-system", Name: "tls-ca"},
+					gomock.Any(),
+				).DoAndReturn(
+					func(ctx context.Context, key client.ObjectKey, secret *corev1.Secret, opts ...client.GetOption) error {
+						secret.Data = map[string][]byte{"cacerts.pem": []byte("foo")}
+						return nil
+					})
+
+				c.EXPECT().Get(
+					gomock.Any(),
+					types.NamespacedName{Namespace: "cattle-system", Name: "tls-ca-additional"},
+					gomock.Any(),
+				).DoAndReturn(
+					func(ctx context.Context, key client.ObjectKey, secret *corev1.Secret, opts ...client.GetOption) error {
+						return notFoundErr
+					})
+			},
+			expectedBundle: []byte("foo"),
+			expectedErr:    nil,
+		},
+		{
+			name: "tls-ca does not exist but tls-ca-additional does",
+			secretGets: func(c *mocks.MockClient) {
+				c.EXPECT().Get(
+					gomock.Any(),
+					types.NamespacedName{Namespace: "cattle-system", Name: "tls-ca"},
+					gomock.Any(),
+				).DoAndReturn(
+					func(ctx context.Context, key client.ObjectKey, secret *corev1.Secret, opts ...client.GetOption) error {
+						return notFoundErr
+					})
+
+				c.EXPECT().Get(
+					gomock.Any(),
+					types.NamespacedName{Namespace: "cattle-system", Name: "tls-ca-additional"},
+					gomock.Any(),
+				).DoAndReturn(
+					func(ctx context.Context, key client.ObjectKey, secret *corev1.Secret, opts ...client.GetOption) error {
+						secret.Data = map[string][]byte{"ca-additional.pem": []byte("foo")}
+						return nil
+					})
+			},
+			expectedBundle: []byte("foo"),
+			expectedErr:    nil,
+		},
+		{
+			name: "tls-ca and tls-ca-additional both exist",
+			secretGets: func(c *mocks.MockClient) {
+				c.EXPECT().Get(
+					gomock.Any(),
+					types.NamespacedName{Namespace: "cattle-system", Name: "tls-ca"},
+					gomock.Any(),
+				).DoAndReturn(
+					func(ctx context.Context, key client.ObjectKey, secret *corev1.Secret, opts ...client.GetOption) error {
+						secret.Data = map[string][]byte{"cacerts.pem": []byte("foo")}
+						return nil
+					})
+
+				c.EXPECT().Get(
+					gomock.Any(),
+					types.NamespacedName{Namespace: "cattle-system", Name: "tls-ca-additional"},
+					gomock.Any(),
+				).DoAndReturn(
+					func(ctx context.Context, key client.ObjectKey, secret *corev1.Secret, opts ...client.GetOption) error {
+						secret.Data = map[string][]byte{"ca-additional.pem": []byte("bar")}
+						return nil
+					})
+			},
+			expectedBundle: []byte("foobar"),
+			expectedErr:    nil,
+		},
+		{
+			name: "tls-ca is malformed",
+			secretGets: func(c *mocks.MockClient) {
+				c.EXPECT().Get(
+					gomock.Any(),
+					types.NamespacedName{Namespace: "cattle-system", Name: "tls-ca"},
+					gomock.Any(),
+				).DoAndReturn(
+					func(ctx context.Context, key client.ObjectKey, secret *corev1.Secret, opts ...client.GetOption) error {
+						secret.Data = map[string][]byte{"certs.pem": []byte("foo")}
+						return nil
+					})
+			},
+			expectedBundle: nil,
+			expectedErr:    fmt.Errorf("no field cacerts.pem found in secret tls-ca"),
+		},
+		{
+			name: "tls-ca and tls-ca-additional both exist, but tls-ca-additional is malformed",
+			secretGets: func(c *mocks.MockClient) {
+				c.EXPECT().Get(
+					gomock.Any(),
+					types.NamespacedName{Namespace: "cattle-system", Name: "tls-ca"},
+					gomock.Any(),
+				).DoAndReturn(
+					func(ctx context.Context, key client.ObjectKey, secret *corev1.Secret, opts ...client.GetOption) error {
+						secret.Data = map[string][]byte{"cacerts.pem": []byte("foo")}
+						return nil
+					})
+
+				c.EXPECT().Get(
+					gomock.Any(),
+					types.NamespacedName{Namespace: "cattle-system", Name: "tls-ca-additional"},
+					gomock.Any(),
+				).DoAndReturn(
+					func(ctx context.Context, key client.ObjectKey, secret *corev1.Secret, opts ...client.GetOption) error {
+						secret.Data = map[string][]byte{"moar-ca.pem": []byte("bar")}
+						return nil
+					})
+			},
+			expectedBundle: nil,
+			expectedErr:    fmt.Errorf("no field ca-additional.pem found in secret tls-ca-additional"),
+		},
+		{
+			name: "client fails to get tls-ca secret",
+			secretGets: func(c *mocks.MockClient) {
+				c.EXPECT().Get(
+					gomock.Any(),
+					types.NamespacedName{Namespace: "cattle-system", Name: "tls-ca"},
+					gomock.Any(),
+				).DoAndReturn(
+					func(ctx context.Context, key client.ObjectKey, secret *corev1.Secret, opts ...client.GetOption) error {
+						return fmt.Errorf("something happened")
+					})
+			},
+			expectedBundle: nil,
+			expectedErr:    fmt.Errorf("something happened"),
+		},
+		{
+			name: "client fails to get tls-ca-additional secret",
+			secretGets: func(c *mocks.MockClient) {
+				c.EXPECT().Get(
+					gomock.Any(),
+					types.NamespacedName{Namespace: "cattle-system", Name: "tls-ca"},
+					gomock.Any(),
+				).DoAndReturn(
+					func(ctx context.Context, key client.ObjectKey, secret *corev1.Secret, opts ...client.GetOption) error {
+						secret.Data = map[string][]byte{"cacerts.pem": []byte("foo")}
+						return nil
+					})
+
+				c.EXPECT().Get(
+					gomock.Any(),
+					types.NamespacedName{Namespace: "cattle-system", Name: "tls-ca-additional"},
+					gomock.Any(),
+				).DoAndReturn(
+					func(ctx context.Context, key client.ObjectKey, secret *corev1.Secret, opts ...client.GetOption) error {
+						return fmt.Errorf("something happened")
+					})
+			},
+			expectedBundle: nil,
+			expectedErr:    fmt.Errorf("something happened"),
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+
+			ctlr := gomock.NewController(t)
+			mockClient := mocks.NewMockClient(ctlr)
+
+			tc.secretGets(mockClient)
+
+			bundle, err := cert.GetRancherCABundle(context.Background(), mockClient)
+			if tc.expectedErr == nil && err != nil {
+				t.Errorf("expected nil error, got %q", err.Error())
+			} else if tc.expectedErr != nil && err == nil {
+				t.Errorf("expected error %q, got nil", tc.expectedErr.Error())
+			} else if err != nil && tc.expectedErr != nil && err.Error() != tc.expectedErr.Error() {
+				t.Errorf("expected %q, got %q", tc.expectedErr.Error(), err.Error())
+			}
+
+			if string(bundle) != string(tc.expectedBundle) { // XXX: should we compare byte by byte instead?
+				t.Errorf("expected %q, got %q", tc.expectedBundle, bundle)
+			}
+		})
+	}
+}
diff --git a/pkg/git/fetch_test.go b/pkg/git/fetch_test.go
index f9341529..cb04cf4f 100644
--- a/pkg/git/fetch_test.go
+++ b/pkg/git/fetch_test.go
@@ -1,10 +1,21 @@
 package git_test
 
 import (
+	"bytes"
 	"context"
+	"crypto/rand"
+	"crypto/rsa"
+	"crypto/tls"
+	"crypto/x509"
+	"crypto/x509/pkix"
+	"encoding/base64"
+	"encoding/pem"
 	"fmt"
+	"math/big"
+	"net"
 	"net/http"
 	"net/http/httptest"
+	"time"
 
 	. "github.com/onsi/ginkgo/v2"
 	. "github.com/onsi/gomega"
@@ -30,7 +41,7 @@ func newTestClient(objs ...client.Object) client.Client {
 		Build()
 }
 
-func newTestGithubServer(refs []string) *httptest.Server {
+func newTestGithubServer(refs []string, TLSCfg *tls.Config) *httptest.Server {
 	// fake response from github with capabilities
 	header := "001e# service=git-upload-pack\n01552ada7cca738877df8459b3a34839a15e5683edaa HEAD\x00"
 	header += "multi_ack thin-pack side-band side-band-64k ofs-delta shallow deepen-since deepen-not deepen-relative no-progress include-tag multi_ack_detailed allow-tip-sha1-in-want allow-reachable-sha1-in-want no-done symref=HEAD:refs/heads/master filter object-format=sha1 agent=git/github-f133c3a1d7e6\n"
@@ -49,7 +60,13 @@ func newTestGithubServer(refs []string) *httptest.Server {
 	})
 
 	ts := httptest.NewUnstartedServer(mux)
-	ts.Start()
+	if TLSCfg != nil {
+		ts.TLS = TLSCfg
+		ts.StartTLS()
+	} else {
+		ts.Start()
+	}
+
 	return ts
 }
 
@@ -59,13 +76,6 @@ var _ = Describe("git fetch's LatestCommit tests", func() {
 			fakeGithub *httptest.Server
 			refs       []string
 		)
-		JustBeforeEach(func() {
-			fakeGithub = newTestGithubServer(refs)
-		})
-
-		AfterEach(func() {
-			fakeGithub.Close()
-		})
 
 		BeforeEach(func() {
 			refs = []string{
@@ -92,6 +102,9 @@ var _ = Describe("git fetch's LatestCommit tests", func() {
 				},
 			}
 
+			fakeGithub = newTestGithubServer(refs, nil)
+			defer fakeGithub.Close()
+
 			gr := &fleetv1.GitRepo{
 				ObjectMeta: metav1.ObjectMeta{
 					Name:      "test-gitrepo",
@@ -106,6 +119,7 @@ var _ = Describe("git fetch's LatestCommit tests", func() {
 					Commit: "",
 				},
 			}
+
 			c := newTestClient(secret)
 			f := git.Fetch{}
 			commit, err := f.LatestCommit(context.Background(), gr, c)
@@ -126,6 +140,9 @@ var _ = Describe("git fetch's LatestCommit tests", func() {
 				},
 			}
 
+			fakeGithub = newTestGithubServer(refs, nil)
+			defer fakeGithub.Close()
+
 			gr := &fleetv1.GitRepo{
 				ObjectMeta: metav1.ObjectMeta{
 					Name:      "test-gitrepo",
@@ -140,6 +157,7 @@ var _ = Describe("git fetch's LatestCommit tests", func() {
 					Commit: "",
 				},
 			}
+
 			c := newTestClient(secret)
 			f := git.Fetch{}
 			commit, err := f.LatestCommit(context.Background(), gr, c)
@@ -160,6 +178,9 @@ var _ = Describe("git fetch's LatestCommit tests", func() {
 				},
 			}
 
+			fakeGithub = newTestGithubServer(refs, nil)
+			defer fakeGithub.Close()
+
 			gr := &fleetv1.GitRepo{
 				ObjectMeta: metav1.ObjectMeta{
 					Name:      "test-gitrepo",
@@ -174,6 +195,7 @@ var _ = Describe("git fetch's LatestCommit tests", func() {
 					Commit: "",
 				},
 			}
+
 			c := newTestClient(secret)
 			f := git.Fetch{}
 			commit, err := f.LatestCommit(context.Background(), gr, c)
@@ -181,5 +203,145 @@ var _ = Describe("git fetch's LatestCommit tests", func() {
 			Expect(commit).To(BeEmpty())
 			Expect(err.Error()).To(Equal("ssh: no key found"))
 		})
+
+		It("uses a Rancher CA bundle if configured", func() {
+			cfg, ca, err := setupCerts()
+			Expect(err).ToNot(HaveOccurred())
+
+			buf := make([]byte, base64.StdEncoding.EncodedLen(len(ca)))
+			base64.StdEncoding.Encode(buf, ca)
+
+			fakeGithub = newTestGithubServer(refs, cfg)
+			defer fakeGithub.Close()
+
+			config.Set(&config.Config{
+				GitClientTimeout: metav1.Duration{Duration: 0},
+			})
+
+			secret := &corev1.Secret{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "tls-ca-additional",
+					Namespace: "cattle-system",
+				},
+				Data: map[string][]byte{
+					"ca-additional.pem": ca,
+				},
+			}
+
+			gr := &fleetv1.GitRepo{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "test-gitrepo",
+					Namespace: "test-ns",
+				},
+				Spec: fleetv1.GitRepoSpec{
+					Repo:   fakeGithub.URL,
+					Branch: "master",
+				},
+				Status: fleetv1.GitRepoStatus{
+					Commit: "",
+				},
+			}
+
+			c := newTestClient(secret)
+			f := git.Fetch{}
+			commit, err := f.LatestCommit(context.Background(), gr, c)
+			Expect(err).ToNot(HaveOccurred())
+			Expect(commit).To(Equal("2ada7cca738877df8459b3a34839a15e5683edaa"))
+
+			// Try again without the secret, and check that fetching the latest commit fails
+			c = newTestClient()
+			commit, err = f.LatestCommit(context.Background(), gr, c)
+			Expect(err).To(HaveOccurred())
+			Expect(err.Error()).To(ContainSubstring("signed by unknown authority"))
+			Expect(commit).To(BeEmpty())
+		})
 	})
 })
+
+// setupCerts creates a CA certificate, encodes it in PEM format, and creates server certificates signed with the
+// previously generated CA cert.
+// It returns server TLS config used to set up a test server, along with PEM data for the CA cert and an error, if any
+// (in which case the other 2 returned values will be nil).
+// Heavily inspired by https://shaneutt.com/blog/golang-ca-and-signed-cert-go/
+func setupCerts() (serverTLSConf *tls.Config, caPEMData []byte, err error) {
+	subject := pkix.Name{
+		Organization:  []string{"Testing Fleet, Inc."},
+		Country:       []string{"DE"},
+		Locality:      []string{"Fleet City"},
+		StreetAddress: []string{"Continuous Deployment Street"},
+		PostalCode:    []string{"4242"},
+	}
+
+	// set up CA certificate
+	ca := &x509.Certificate{
+		SerialNumber:          big.NewInt(2025),
+		Subject:               subject,
+		NotBefore:             time.Now(),
+		NotAfter:              time.Now().AddDate(10, 0, 0),
+		IsCA:                  true,
+		ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
+		KeyUsage:              x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
+		BasicConstraintsValid: true,
+	}
+
+	caPrivKey, err := rsa.GenerateKey(rand.Reader, 4096)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	caBytes, err := x509.CreateCertificate(rand.Reader, ca, ca, &caPrivKey.PublicKey, caPrivKey)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	caPEM := new(bytes.Buffer)
+	_ = pem.Encode(caPEM, &pem.Block{
+		Type:  "CERTIFICATE",
+		Bytes: caBytes,
+	})
+
+	// set up server certificate
+	cert := &x509.Certificate{
+		SerialNumber: big.NewInt(2019),
+		Subject:      subject,
+		IPAddresses:  []net.IP{net.IPv4(127, 0, 0, 1), net.IPv6loopback},
+		NotBefore:    time.Now(),
+		NotAfter:     time.Now().AddDate(10, 0, 0),
+		SubjectKeyId: []byte{1, 2, 3, 4, 6},
+		ExtKeyUsage:  []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
+		KeyUsage:     x509.KeyUsageDigitalSignature,
+	}
+
+	certPrivKey, err := rsa.GenerateKey(rand.Reader, 4096)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	certBytes, err := x509.CreateCertificate(rand.Reader, cert, ca, &certPrivKey.PublicKey, caPrivKey)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	certPEM := new(bytes.Buffer)
+	_ = pem.Encode(certPEM, &pem.Block{
+		Type:  "CERTIFICATE",
+		Bytes: certBytes,
+	})
+
+	certPrivKeyPEM := new(bytes.Buffer)
+	_ = pem.Encode(certPrivKeyPEM, &pem.Block{
+		Type:  "RSA PRIVATE KEY",
+		Bytes: x509.MarshalPKCS1PrivateKey(certPrivKey),
+	})
+
+	serverCert, err := tls.X509KeyPair(certPEM.Bytes(), certPrivKeyPEM.Bytes())
+	if err != nil {
+		return nil, nil, err
+	}
+
+	serverTLSConf = &tls.Config{
+		Certificates: []tls.Certificate{serverCert},
+	}
+
+	return serverTLSConf, caPEM.Bytes(), nil
+}