diff --git a/controllers/helmrelease_controller.go b/controllers/helmrelease_controller.go index 4b284a145..7a50e204d 100644 --- a/controllers/helmrelease_controller.go +++ b/controllers/helmrelease_controller.go @@ -48,7 +48,9 @@ import ( "sigs.k8s.io/controller-runtime/pkg/reconcile" "sigs.k8s.io/controller-runtime/pkg/source" + apiacl "github.com/fluxcd/pkg/apis/acl" "github.com/fluxcd/pkg/apis/meta" + "github.com/fluxcd/pkg/runtime/acl" "github.com/fluxcd/pkg/runtime/events" "github.com/fluxcd/pkg/runtime/metrics" "github.com/fluxcd/pkg/runtime/predicates" @@ -78,6 +80,7 @@ type HelmReleaseReconciler struct { EventRecorder kuberecorder.EventRecorder ExternalEventRecorder *events.Recorder MetricsRecorder *metrics.Recorder + NoCrossNamespaceRef bool } func (r *HelmReleaseReconciler) SetupWithManager(mgr ctrl.Manager, opts HelmReleaseReconcilerOptions) error { @@ -213,9 +216,15 @@ func (r *HelmReleaseReconciler) reconcile(ctx context.Context, hr v2.HelmRelease // Reconcile chart based on the HelmChartTemplate hc, reconcileErr := r.reconcileChart(ctx, &hr) if reconcileErr != nil { + reason := v2.ArtifactFailedReason + if acl.IsAccessDenied(reconcileErr) { + reason = apiacl.AccessDeniedReason + log.Error(reconcileErr, "access denied to cross-namespace source") + } + msg := fmt.Sprintf("chart reconciliation failed: %s", reconcileErr.Error()) r.event(ctx, hr, hr.Status.LastAttemptedRevision, events.EventSeverityError, msg) - return v2.HelmReleaseNotReady(hr, v2.ArtifactFailedReason, msg), ctrl.Result{Requeue: true}, reconcileErr + return v2.HelmReleaseNotReady(hr, reason, msg), ctrl.Result{Requeue: true}, reconcileErr } // Check chart readiness diff --git a/controllers/helmrelease_controller_chart.go b/controllers/helmrelease_controller_chart.go index 5bd0204bf..46023d7eb 100644 --- a/controllers/helmrelease_controller_chart.go +++ b/controllers/helmrelease_controller_chart.go @@ -28,6 +28,7 @@ import ( "reflect" "strings" + "github.com/fluxcd/pkg/runtime/acl" "github.com/hashicorp/go-retryablehttp" "helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v3/pkg/chart/loader" @@ -46,6 +47,11 @@ func (r *HelmReleaseReconciler) reconcileChart(ctx context.Context, hr *v2.HelmR Namespace: hr.Spec.Chart.GetNamespace(hr.Namespace), Name: hr.GetHelmChartName(), } + + if r.NoCrossNamespaceRef && chartName.Namespace != hr.Namespace { + return nil, acl.AccessDeniedError(fmt.Sprintf("can't access 'HelmChart/%s', cross-namespace references have beem blocked", + chartName)) + } // Garbage collect the previous HelmChart if the namespace named changed. if hr.Status.HelmChart != "" && hr.Status.HelmChart != chartName.String() { diff --git a/controllers/helmrelease_controller_chart_test.go b/controllers/helmrelease_controller_chart_test.go index 499b22d6a..082a1a525 100644 --- a/controllers/helmrelease_controller_chart_test.go +++ b/controllers/helmrelease_controller_chart_test.go @@ -41,6 +41,7 @@ func TestHelmReleaseReconciler_reconcileChart(t *testing.T) { expectHelmChartStatus string expectGC bool expectErr bool + noCrossNamspaceRef bool }{ { name: "new HelmChart", @@ -140,6 +141,33 @@ func TestHelmReleaseReconciler_reconcileChart(t *testing.T) { expectHelmChartStatus: "cross/default-test-release", expectGC: true, }, + { + name: "block cross namespace access when flag is set", + hr: &v2.HelmRelease{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-release", + Namespace: "default", + }, + Spec: v2.HelmReleaseSpec{ + Interval: metav1.Duration{Duration: time.Minute}, + Chart: v2.HelmChartTemplate{ + Spec: v2.HelmChartTemplateSpec{ + Chart: "chart", + SourceRef: v2.CrossNamespaceObjectReference{ + Name: "test-repository", + Kind: "HelmRepository", + Namespace: "cross", + }, + }, + }, + }, + Status: v2.HelmReleaseStatus{ + HelmChart: "", + }, + }, + noCrossNamspaceRef: true, + expectErr: true, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -156,7 +184,8 @@ func TestHelmReleaseReconciler_reconcileChart(t *testing.T) { } r := &HelmReleaseReconciler{ - Client: c, + Client: c, + NoCrossNamespaceRef: tt.noCrossNamspaceRef, } hc, err := r.reconcileChart(logr.NewContext(context.TODO(), logr.Discard()), tt.hr) diff --git a/docs/spec/v2beta1/helmreleases.md b/docs/spec/v2beta1/helmreleases.md index b33436bb6..15671133e 100644 --- a/docs/spec/v2beta1/helmreleases.md +++ b/docs/spec/v2beta1/helmreleases.md @@ -678,6 +678,15 @@ The `spec.chart.spec.sourceRef` is a reference to an object managed by [revision](https://github.com/fluxcd/source-controller/blob/main/docs/spec/v1beta1/common.md#source-status) changes, it generates a Kubernetes event that triggers a new release. +> **Note** that on multi-tenant clusters, platform admins can disable cross-namespace references +> with the `--no-cross-namespace-refs=true` flag. When this flag is set, the helmrelease can only +> refer to sources in the same namespace as the helmrelease object. + +> **Note** that on multi-tenant clusters, platform admins can disable cross-namespace references +> with the `--no-cross-namespace-refs=true` flag. When this flag is set, alerts can only refer to +> event sources in the same namespace as the alert object, +> preventing tenants from subscribing to another tenant's events. + Supported source types: - [HelmRepository](https://github.com/fluxcd/source-controller/blob/main/docs/spec/v1beta1/helmrepositories.md) diff --git a/go.mod b/go.mod index 3ab710c9e..d8baccaeb 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/fluxcd/helm-controller/api v0.15.0 github.com/fluxcd/pkg/apis/kustomize v0.3.1 github.com/fluxcd/pkg/apis/meta v0.10.2 - github.com/fluxcd/pkg/runtime v0.12.3 + github.com/fluxcd/pkg/runtime v0.12.4 github.com/fluxcd/source-controller/api v0.20.1 github.com/garyburd/redigo v1.6.3 // indirect github.com/go-logr/logr v1.2.2 @@ -31,6 +31,8 @@ require ( sigs.k8s.io/yaml v1.3.0 ) +require github.com/fluxcd/pkg/apis/acl v0.0.3 + require ( cloud.google.com/go v0.81.0 // indirect github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect @@ -61,7 +63,6 @@ require ( github.com/evanphx/json-patch v4.12.0+incompatible // indirect github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // indirect github.com/fatih/color v1.7.0 // indirect - github.com/fluxcd/pkg/apis/acl v0.0.3 // indirect github.com/fsnotify/fsnotify v1.5.1 // indirect github.com/go-errors/errors v1.0.1 // indirect github.com/go-logr/zapr v1.2.0 // indirect diff --git a/go.sum b/go.sum index 293ae9504..81952ef1e 100644 --- a/go.sum +++ b/go.sum @@ -278,6 +278,8 @@ github.com/fluxcd/pkg/apis/meta v0.10.2 h1:pnDBBEvfs4HaKiVAYgz+e/AQ8dLvcgmVfSeBr github.com/fluxcd/pkg/apis/meta v0.10.2/go.mod h1:KQ2er9xa6koy7uoPMZjIjNudB5p4tXs+w0GO6fRcy7I= github.com/fluxcd/pkg/runtime v0.12.3 h1:h21AZ3YG5MAP7DxFF9hfKrP+vFzys2L7CkUbPFjbP/0= github.com/fluxcd/pkg/runtime v0.12.3/go.mod h1:imJ2xYy/d4PbSinX2IefmZk+iS2c1P5fY0js8mCE4SM= +github.com/fluxcd/pkg/runtime v0.12.4 h1:gA27RG/+adN2/7Qe03PB46Iwmye/MusPCpuS4zQ2fW0= +github.com/fluxcd/pkg/runtime v0.12.4/go.mod h1:gspNvhAqodZgSmK1ZhMtvARBf/NGAlxmaZaIOHkJYsc= github.com/fluxcd/source-controller/api v0.20.1 h1:BfYw1gNHykiCVFNtDz3atcf3Vph+arfuveKmouI98wE= github.com/fluxcd/source-controller/api v0.20.1/go.mod h1:Ab2qDmAUz6ZCp8UaHYLYzxyFrC1FQqEqjxiROb/Rdiw= github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= diff --git a/main.go b/main.go index 1a59bf5a3..87fc51b54 100644 --- a/main.go +++ b/main.go @@ -30,6 +30,7 @@ import ( ctrl "sigs.k8s.io/controller-runtime" crtlmetrics "sigs.k8s.io/controller-runtime/pkg/metrics" + "github.com/fluxcd/pkg/runtime/acl" "github.com/fluxcd/pkg/runtime/client" "github.com/fluxcd/pkg/runtime/events" "github.com/fluxcd/pkg/runtime/leaderelection" @@ -70,6 +71,7 @@ func main() { httpRetry int clientOptions client.Options logOptions logger.Options + aclOptions acl.Options leaderElectionOptions leaderelection.Options ) @@ -83,6 +85,7 @@ func main() { flag.IntVar(&httpRetry, "http-retry", 9, "The maximum number of retries when failing to fetch artifacts over HTTP.") clientOptions.BindFlags(flag.CommandLine) logOptions.BindFlags(flag.CommandLine) + aclOptions.BindFlags(flag.CommandLine) leaderElectionOptions.BindFlags(flag.CommandLine) flag.Parse() @@ -139,6 +142,7 @@ func main() { EventRecorder: mgr.GetEventRecorderFor(controllerName), ExternalEventRecorder: eventRecorder, MetricsRecorder: metricsRecorder, + NoCrossNamespaceRef: aclOptions.NoCrossNamespaceRefs, }).SetupWithManager(mgr, controllers.HelmReleaseReconcilerOptions{ MaxConcurrentReconciles: concurrent, DependencyRequeueInterval: requeueDependency,