Skip to content

Commit 6b9cde6

Browse files
committed
Fall back to Rancher CA bundles for HelmOps
Resolve the CA bundle in the HelmOps controller and store it in BundleHelmOptions.CABundle so the agent can use it without needing access to cattle-system secrets. The agent service account only has access to its own namespace. Also restart helmops pods in dev/update-controller-k3d so that redeployments pick up the new controller binary.
1 parent 3a0f39a commit 6b9cde6

12 files changed

Lines changed: 355 additions & 7 deletions

File tree

charts/fleet-crd/templates/crds.yaml

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,20 @@ spec:
194194
195195
should be downloaded from a helm chart'
196196
properties:
197+
helmOpCABundle:
198+
description: 'CABundle is a PEM encoded CA bundle used to validate
199+
TLS connections to
200+
201+
the Helm registry. It is resolved by the controller (which
202+
has access to
203+
204+
Rancher''s cattle-system CA secrets) and stored here so the
205+
agent can use
206+
207+
it without requiring access to those secrets.'
208+
format: byte
209+
nullable: true
210+
type: string
197211
helmOpInsecureSkipTLSVerify:
198212
description: InsecureSkipTLSverify will use insecure HTTPS to
199213
clone the helm app resource.
@@ -2067,6 +2081,20 @@ spec:
20672081
not a git repository.'
20682082
nullable: true
20692083
properties:
2084+
helmOpCABundle:
2085+
description: 'CABundle is a PEM encoded CA bundle used to validate
2086+
TLS connections to
2087+
2088+
the Helm registry. It is resolved by the controller (which
2089+
has access to
2090+
2091+
Rancher''s cattle-system CA secrets) and stored here so the
2092+
agent can use
2093+
2094+
it without requiring access to those secrets.'
2095+
format: byte
2096+
nullable: true
2097+
type: string
20702098
helmOpInsecureSkipTLSVerify:
20712099
description: InsecureSkipTLSverify will use insecure HTTPS to
20722100
clone the helm app resource.
@@ -8269,6 +8297,20 @@ spec:
82698297
not a git repository.'
82708298
nullable: true
82718299
properties:
8300+
helmOpCABundle:
8301+
description: 'CABundle is a PEM encoded CA bundle used to validate
8302+
TLS connections to
8303+
8304+
the Helm registry. It is resolved by the controller (which
8305+
has access to
8306+
8307+
Rancher''s cattle-system CA secrets) and stored here so the
8308+
agent can use
8309+
8310+
it without requiring access to those secrets.'
8311+
format: byte
8312+
nullable: true
8313+
type: string
82728314
helmOpInsecureSkipTLSVerify:
82738315
description: InsecureSkipTLSverify will use insecure HTTPS to
82748316
clone the helm app resource.

dev/update-controller-k3d

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,4 @@ fleet_ctx=$(kubectl config current-context)
2020
k3d image import rancher/fleet:dev -m direct -c "${fleet_ctx#k3d-}"
2121
kubectl delete pod -l app=fleet-controller -n cattle-fleet-system
2222
kubectl delete pod -l app=gitjob -n cattle-fleet-system
23+
kubectl delete pod -l app=helmops -n cattle-fleet-system

e2e/single-cluster/helmop_test.go

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -520,6 +520,91 @@ var _ = Describe("HelmOp resource tests with tarball source", Label("infra-setup
520520
})
521521
})
522522

523+
var _ = Describe("HelmOp resource falls back to Rancher CA bundle", Label("infra-setup", "helm-registry"), Ordered, func() {
524+
// This test mirrors the GitOps E2E test "should succeed when not configuring any CA"
525+
// in go_getter_custom_ca_test.go. The dev/create-secrets script places the root CA
526+
// into cattle-system/tls-ca-additional. ChartMuseum is served with a cert signed by
527+
// that root CA. A HelmOp with a credentials-only secret (no cacerts) and
528+
// InsecureSkipTLSVerify=false must therefore succeed via the Rancher CA fallback.
529+
const (
530+
name = "rancher-ca-fallback"
531+
secretName = "helmop-rancher-ca-creds"
532+
)
533+
534+
var (
535+
namespace string
536+
k kubectl.Command
537+
)
538+
539+
BeforeAll(func() {
540+
k = env.Kubectl.Namespace(env.Namespace)
541+
out, err := k.Create(
542+
"secret", "generic", secretName,
543+
"--from-literal=username="+os.Getenv("CI_OCI_USERNAME"),
544+
"--from-literal=password="+os.Getenv("CI_OCI_PASSWORD"),
545+
// no cacerts — TLS trust must come from cattle-system/tls-ca-additional
546+
)
547+
if strings.Contains(out, "already exists") {
548+
err = nil
549+
}
550+
Expect(err).ToNot(HaveOccurred(), out)
551+
})
552+
553+
JustBeforeEach(func() {
554+
namespace = testenv.NewNamespaceName(
555+
name,
556+
rand.New(rand.NewSource(time.Now().UnixNano())),
557+
)
558+
559+
// URL without embedded credentials so the secret is the only auth source.
560+
repo := fmt.Sprintf("https://chartmuseum-service.%s.svc.cluster.local:8081", cmd.InfraNamespace)
561+
err := testenv.ApplyTemplate(k, testenv.AssetPath("helmop/helmop.yaml"), struct {
562+
Name string
563+
Namespace string
564+
Repo string
565+
Chart string
566+
PollingInterval time.Duration
567+
HelmSecretName string
568+
InsecureSkipTLSVerify bool
569+
Version string
570+
}{
571+
name,
572+
namespace,
573+
repo,
574+
"sleeper-chart",
575+
5 * time.Second,
576+
secretName,
577+
false, // strict TLS — relies on Rancher CA bundle fallback
578+
"0.1.0",
579+
})
580+
Expect(err).ToNot(HaveOccurred())
581+
})
582+
583+
AfterAll(func() {
584+
out, err := k.Delete("helmop", name)
585+
Expect(err).ToNot(HaveOccurred(), out)
586+
out, err = k.Delete("secret", secretName)
587+
Expect(err).ToNot(HaveOccurred(), out)
588+
})
589+
590+
It("deploys the chart using the Rancher CA bundle from cattle-system", func() {
591+
Eventually(func(g Gomega) {
592+
outPods, _ := k.Namespace(namespace).Get("pods")
593+
g.Expect(outPods).To(ContainSubstring("sleeper-"))
594+
}).Should(Succeed())
595+
Eventually(func(g Gomega) {
596+
outDeployments, _ := k.Namespace(namespace).Get("deployments")
597+
g.Expect(outDeployments).To(ContainSubstring("sleeper"))
598+
}).Should(Succeed())
599+
600+
By("setting the expected version in the helmop Status")
601+
Eventually(func() string {
602+
out, _ := k.Get("helmop", name, "-o=jsonpath={.status.version}")
603+
return out
604+
}).Should(Equal("0.1.0"))
605+
})
606+
})
607+
523608
// getExternalHelmAddr retrieves the external URL where our local Helm registry can be reached.
524609
func getExternalHelmAddr(k kubectl.Command) (string, error) {
525610
if v := os.Getenv("external_ip"); v != "" {

integrationtests/helmops/controller/controller_test.go

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"crypto/sha256"
55
"crypto/subtle"
66
"crypto/tls"
7+
"encoding/pem"
78
"fmt"
89
"io"
910
"log"
@@ -230,6 +231,26 @@ func checkBundleIsAsExpected(g Gomega, bundle fleet.Bundle, helmop fleet.HelmOp,
230231
g.Expect(controllerutil.ContainsFinalizer(&bundle, finalize.BundleFinalizer)).To(BeTrue())
231232
}
232233

234+
// createRancherCASecret creates a secret in cattle-system using the
235+
// certificate from svr and registers a DeferCleanup to delete it.
236+
func createRancherCASecret(svr *httptest.Server, secretName, dataKey string) {
237+
certPEM := pem.EncodeToMemory(&pem.Block{
238+
Type: "CERTIFICATE",
239+
Bytes: svr.TLS.Certificates[0].Certificate[0],
240+
})
241+
secret := &v1.Secret{
242+
ObjectMeta: metav1.ObjectMeta{
243+
Name: secretName,
244+
Namespace: "cattle-system",
245+
},
246+
Data: map[string][]byte{dataKey: certPEM},
247+
}
248+
Expect(k8sClient.Create(ctx, secret)).ToNot(HaveOccurred())
249+
DeferCleanup(func() {
250+
_ = k8sClient.Delete(ctx, secret)
251+
})
252+
}
253+
233254
func updateHelmOp(helmop fleet.HelmOp) error {
234255
backoff := retry.DefaultBackoff
235256
backoff.Steps = 10
@@ -1267,5 +1288,73 @@ var _ = Describe("HelmOps controller", func() {
12671288
}).Should(Succeed())
12681289
})
12691290
})
1291+
1292+
When("connecting to a https server with a CA bundle from Rancher tls-ca secret", func() {
1293+
BeforeEach(func() {
1294+
targets = []fleet.BundleTarget{}
1295+
helmop = getRandomHelmOpWithTargets("test-rancher-tlsca", targets)
1296+
helmop.Spec.Helm.Version = ""
1297+
helmop.Spec.HelmSecretName = ""
1298+
1299+
svr := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
1300+
w.WriteHeader(http.StatusOK)
1301+
fmt.Fprint(w, helmRepoIndex)
1302+
}))
1303+
DeferCleanup(svr.Close)
1304+
1305+
helmop.Spec.Helm.Repo = svr.URL
1306+
helmop.Spec.Helm.Chart = "alpine"
1307+
helmop.Spec.InsecureSkipTLSverify = false
1308+
doAfterNamespaceCreated = func() {
1309+
createRancherCASecret(svr, "tls-ca", "cacerts.pem")
1310+
}
1311+
})
1312+
1313+
It("creates a bundle with the latest version it got from the index", func() {
1314+
Eventually(func(g Gomega) {
1315+
bundle := &fleet.Bundle{}
1316+
ns := types.NamespacedName{Name: helmop.Name, Namespace: helmop.Namespace}
1317+
err := k8sClient.Get(ctx, ns, bundle)
1318+
g.Expect(err).ToNot(HaveOccurred())
1319+
t := []fleet.BundleTarget{{Name: "default", ClusterGroup: "default"}}
1320+
helmop.Spec.Helm.Version = "0.2.0"
1321+
checkBundleIsAsExpected(g, *bundle, helmop, t)
1322+
}).Should(Succeed())
1323+
})
1324+
})
1325+
1326+
When("connecting to a https server with a CA bundle from Rancher tls-ca-additional secret", func() {
1327+
BeforeEach(func() {
1328+
targets = []fleet.BundleTarget{}
1329+
helmop = getRandomHelmOpWithTargets("test-rancher-tlsca-additional", targets)
1330+
helmop.Spec.Helm.Version = ""
1331+
helmop.Spec.HelmSecretName = ""
1332+
1333+
svr := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
1334+
w.WriteHeader(http.StatusOK)
1335+
fmt.Fprint(w, helmRepoIndex)
1336+
}))
1337+
DeferCleanup(svr.Close)
1338+
1339+
helmop.Spec.Helm.Repo = svr.URL
1340+
helmop.Spec.Helm.Chart = "alpine"
1341+
helmop.Spec.InsecureSkipTLSverify = false
1342+
doAfterNamespaceCreated = func() {
1343+
createRancherCASecret(svr, "tls-ca-additional", "ca-additional.pem")
1344+
}
1345+
})
1346+
1347+
It("creates a bundle with the latest version it got from the index", func() {
1348+
Eventually(func(g Gomega) {
1349+
bundle := &fleet.Bundle{}
1350+
ns := types.NamespacedName{Name: helmop.Name, Namespace: helmop.Namespace}
1351+
err := k8sClient.Get(ctx, ns, bundle)
1352+
g.Expect(err).ToNot(HaveOccurred())
1353+
t := []fleet.BundleTarget{{Name: "default", ClusterGroup: "default"}}
1354+
helmop.Spec.Helm.Version = "0.2.0"
1355+
checkBundleIsAsExpected(g, *bundle, helmop, t)
1356+
}).Should(Succeed())
1357+
})
1358+
})
12701359
})
12711360
})

integrationtests/helmops/controller/suite_test.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import (
1919
"github.com/rancher/fleet/internal/manifest"
2020
v1alpha1 "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1"
2121

22+
corev1 "k8s.io/api/core/v1"
23+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2224
"k8s.io/client-go/kubernetes"
2325
"k8s.io/client-go/kubernetes/scheme"
2426
"k8s.io/client-go/rest"
@@ -128,6 +130,22 @@ var _ = BeforeSuite(func() {
128130
err = mgr.Start(ctx)
129131
Expect(err).ToNot(HaveOccurred(), "failed to run manager")
130132
}()
133+
134+
// Create Rancher-like namespace for CA bundle secrets
135+
err = k8sClient.Create(ctx, &corev1.Namespace{
136+
ObjectMeta: metav1.ObjectMeta{
137+
Name: "cattle-system",
138+
},
139+
})
140+
Expect(err).ToNot(HaveOccurred())
141+
142+
DeferCleanup(func() {
143+
_ = k8sClient.Delete(ctx, &corev1.Namespace{
144+
ObjectMeta: metav1.ObjectMeta{
145+
Name: "cattle-system",
146+
},
147+
})
148+
})
131149
})
132150

133151
var _ = AfterSuite(func() {

internal/bundlereader/helm.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,13 @@ func GetManifestFromHelmChart(ctx context.Context, c client.Reader, bd *fleet.Bu
3232
}
3333
auth.InsecureSkipVerify = bd.Spec.HelmChartOptions.InsecureSkipTLSverify
3434

35+
// Use the Rancher CA bundle that was pre-resolved by the controller and stored in
36+
// HelmChartOptions.CABundle. The agent service account cannot read cattle-system
37+
// secrets directly, so the controller must pass the bundle through.
38+
if len(auth.CABundle) == 0 {
39+
auth.CABundle = bd.Spec.HelmChartOptions.CABundle
40+
}
41+
3542
chartURL, err := ChartURL(ctx, *helm, auth, true)
3643
if err != nil {
3744
return nil, err

0 commit comments

Comments
 (0)