From 2b0e35559c945a112e3461ff1c37051045b3eb8c Mon Sep 17 00:00:00 2001 From: Soule BA Date: Wed, 19 Oct 2022 02:42:38 +0200 Subject: [PATCH 1/2] Add cosign verification to the chart Template If implemented users can reconcile charts with cosign verification enabled. Signed-off-by: Soule BA --- api/v2beta1/helmrelease_types.go | 21 +++++ api/v2beta1/zz_generated.deepcopy.go | 25 ++++++ .../helm.toolkit.fluxcd.io_helmreleases.yaml | 28 +++++++ controllers/helmrelease_controller_chart.go | 15 ++++ .../helmrelease_controller_chart_test.go | 48 +++++++++++ docs/api/helmrelease.md | 83 +++++++++++++++++++ go.mod | 6 +- go.sum | 12 +-- 8 files changed, 229 insertions(+), 9 deletions(-) diff --git a/api/v2beta1/helmrelease_types.go b/api/v2beta1/helmrelease_types.go index b9cfc31e1..170c98e8a 100644 --- a/api/v2beta1/helmrelease_types.go +++ b/api/v2beta1/helmrelease_types.go @@ -286,6 +286,14 @@ type HelmChartTemplateSpec struct { // +optional // +deprecated ValuesFile string `json:"valuesFile,omitempty"` + + // Verify contains the secret name containing the trusted public keys + // used to verify the signature and specifies which provider to use to check + // whether OCI image is authentic. + // This field is only supported for OCI sources. + // Chart dependencies, which are not bundled in the umbrella chart artifact, are not verified. + // +optional + Verify *HelmChartTemplateVerification `json:"verify,omitempty"` } // GetInterval returns the configured interval for the v1beta2.HelmChart, @@ -306,6 +314,19 @@ func (in HelmChartTemplate) GetNamespace(defaultNamespace string) string { return in.Spec.SourceRef.Namespace } +// HelmChartTemplateVerification verifies the authenticity of an OCI Helm chart. +type HelmChartTemplateVerification struct { + // Provider specifies the technology used to sign the OCI Helm chart. + // +kubebuilder:validation:Enum=cosign + // +kubebuilder:default:=cosign + Provider string `json:"provider"` + + // SecretRef specifies the Kubernetes Secret containing the + // trusted public keys. + // +optional + SecretRef *meta.LocalObjectReference `json:"secretRef,omitempty"` +} + // DeploymentAction defines a consistent interface for Install and Upgrade. // +kubebuilder:object:generate=false type DeploymentAction interface { diff --git a/api/v2beta1/zz_generated.deepcopy.go b/api/v2beta1/zz_generated.deepcopy.go index 7f8ba2992..6c3f4c541 100644 --- a/api/v2beta1/zz_generated.deepcopy.go +++ b/api/v2beta1/zz_generated.deepcopy.go @@ -74,6 +74,11 @@ func (in *HelmChartTemplateSpec) DeepCopyInto(out *HelmChartTemplateSpec) { *out = make([]string, len(*in)) copy(*out, *in) } + if in.Verify != nil { + in, out := &in.Verify, &out.Verify + *out = new(HelmChartTemplateVerification) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HelmChartTemplateSpec. @@ -86,6 +91,26 @@ func (in *HelmChartTemplateSpec) DeepCopy() *HelmChartTemplateSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HelmChartTemplateVerification) DeepCopyInto(out *HelmChartTemplateVerification) { + *out = *in + if in.SecretRef != nil { + in, out := &in.SecretRef, &out.SecretRef + *out = new(meta.LocalObjectReference) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HelmChartTemplateVerification. +func (in *HelmChartTemplateVerification) DeepCopy() *HelmChartTemplateVerification { + if in == nil { + return nil + } + out := new(HelmChartTemplateVerification) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *HelmRelease) DeepCopyInto(out *HelmRelease) { *out = *in diff --git a/config/crd/bases/helm.toolkit.fluxcd.io_helmreleases.yaml b/config/crd/bases/helm.toolkit.fluxcd.io_helmreleases.yaml index bd385332e..3e0021fd2 100644 --- a/config/crd/bases/helm.toolkit.fluxcd.io_helmreleases.yaml +++ b/config/crd/bases/helm.toolkit.fluxcd.io_helmreleases.yaml @@ -117,6 +117,34 @@ spec: items: type: string type: array + verify: + description: Verify contains the secret name containing the + trusted public keys used to verify the signature and specifies + which provider to use to check whether OCI image is authentic. + This field is only supported for OCI sources. Chart dependencies, + which are not bundled in the umbrella chart artifact, are + not verified. + properties: + provider: + default: cosign + description: Provider specifies the technology used to + sign the OCI Helm chart. + enum: + - cosign + type: string + secretRef: + description: SecretRef specifies the Kubernetes Secret + containing the trusted public keys. + properties: + name: + description: Name of the referent. + type: string + required: + - name + type: object + required: + - provider + type: object version: default: '*' description: Version semver expression, ignored for charts diff --git a/controllers/helmrelease_controller_chart.go b/controllers/helmrelease_controller_chart.go index 60e369411..912a7b8e4 100644 --- a/controllers/helmrelease_controller_chart.go +++ b/controllers/helmrelease_controller_chart.go @@ -211,6 +211,7 @@ func buildHelmChartFromTemplate(hr *v2.HelmRelease) *sourcev1.HelmChart { ReconcileStrategy: template.Spec.ReconcileStrategy, ValuesFiles: template.Spec.ValuesFiles, ValuesFile: template.Spec.ValuesFile, + Verify: templateVerificationToSourceVerification(template.Spec.Verify), }, } } @@ -239,7 +240,21 @@ func helmChartRequiresUpdate(hr *v2.HelmRelease, chart *sourcev1.HelmChart) bool return true case template.Spec.ValuesFile != chart.Spec.ValuesFile: return true + case !reflect.DeepEqual(templateVerificationToSourceVerification(template.Spec.Verify), chart.Spec.Verify): + return true default: return false } } + +// templateVerificationToSourceVerification converts the HelmChartTemplateVerification to the OCIRepositoryVerification. +func templateVerificationToSourceVerification(template *v2.HelmChartTemplateVerification) *sourcev1.OCIRepositoryVerification { + if template == nil { + return nil + } + + return &sourcev1.OCIRepositoryVerification{ + Provider: template.Provider, + SecretRef: template.SecretRef, + } +} diff --git a/controllers/helmrelease_controller_chart_test.go b/controllers/helmrelease_controller_chart_test.go index 6dcf1c815..43001e5d4 100644 --- a/controllers/helmrelease_controller_chart_test.go +++ b/controllers/helmrelease_controller_chart_test.go @@ -18,9 +18,11 @@ package controllers import ( "context" + "fmt" "testing" "time" + "github.com/fluxcd/pkg/apis/meta" sourcev1 "github.com/fluxcd/source-controller/api/v1beta2" "github.com/go-logr/logr" . "github.com/onsi/gomega" @@ -371,6 +373,39 @@ func Test_buildHelmChartFromTemplate(t *testing.T) { }, }, }, + { + name: "take cosign verification into account", + modify: func(hr *v2.HelmRelease) { + hr.Spec.Chart.Spec.Verify = &v2.HelmChartTemplateVerification{ + Provider: "cosign", + SecretRef: &meta.LocalObjectReference{ + Name: "cosign-key", + }, + } + }, + want: &sourcev1.HelmChart{ + ObjectMeta: metav1.ObjectMeta{ + Name: "default-test-release", + Namespace: "default", + }, + Spec: sourcev1.HelmChartSpec{ + Chart: "chart", + Version: "1.0.0", + SourceRef: sourcev1.LocalHelmChartSourceReference{ + Name: "test-repository", + Kind: "HelmRepository", + }, + Interval: metav1.Duration{Duration: 2 * time.Minute}, + ValuesFiles: []string{"values.yaml"}, + Verify: &sourcev1.OCIRepositoryVerification{ + Provider: "cosign", + SecretRef: &meta.LocalObjectReference{ + Name: "cosign-key", + }, + }, + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -398,6 +433,9 @@ func Test_helmChartRequiresUpdate(t *testing.T) { Kind: "HelmRepository", }, Interval: &metav1.Duration{Duration: 2 * time.Minute}, + Verify: &v2.HelmChartTemplateVerification{ + Provider: "cosign", + }, }, }, }, @@ -469,6 +507,13 @@ func Test_helmChartRequiresUpdate(t *testing.T) { }, want: true, }, + { + name: "detects verify change", + modify: func(hr *v2.HelmRelease, hc *sourcev1.HelmChart) { + hr.Spec.Chart.Spec.Verify.Provider = "foo-bar" + }, + want: true, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -476,9 +521,12 @@ func Test_helmChartRequiresUpdate(t *testing.T) { hr := hrWithChartTemplate.DeepCopy() hc := buildHelmChartFromTemplate(hr) + // second copy to avoid modifying the original + hr = hrWithChartTemplate.DeepCopy() g.Expect(helmChartRequiresUpdate(hr, hc)).To(Equal(false)) tt.modify(hr, hc) + fmt.Println("verify", hr.Spec.Chart.Spec.Verify.Provider, hc.Spec.Verify.Provider) g.Expect(helmChartRequiresUpdate(hr, hc)).To(Equal(tt.want)) }) } diff --git a/docs/api/helmrelease.md b/docs/api/helmrelease.md index 6b72ba5c6..b53cf1962 100644 --- a/docs/api/helmrelease.md +++ b/docs/api/helmrelease.md @@ -566,6 +566,24 @@ for backwards compatibility the file defined here is merged before the ValuesFiles items. Ignored when omitted.

+ + +verify
+ + +HelmChartTemplateVerification + + + + +(Optional) +

Verify contains the secret name containing the trusted public keys +used to verify the signature and specifies which provider to use to check +whether OCI image is authentic. +This field is only supported for OCI sources. +Chart dependencies, which are not bundled in the umbrella chart artifact, are not verified.

+ + @@ -688,6 +706,71 @@ for backwards compatibility the file defined here is merged before the ValuesFiles items. Ignored when omitted.

+ + +verify
+ + +HelmChartTemplateVerification + + + + +(Optional) +

Verify contains the secret name containing the trusted public keys +used to verify the signature and specifies which provider to use to check +whether OCI image is authentic. +This field is only supported for OCI sources. +Chart dependencies, which are not bundled in the umbrella chart artifact, are not verified.

+ + + + + + +

HelmChartTemplateVerification +

+

+(Appears on: +HelmChartTemplateSpec) +

+

HelmChartTemplateVerification verifies the authenticity of an OCI Helm chart.

+
+
+ + + + + + + + + + + + + + + +
FieldDescription
+provider
+ +string + +
+

Provider specifies the technology used to sign the OCI Helm chart.

+
+secretRef
+ + +github.com/fluxcd/pkg/apis/meta.LocalObjectReference + + +
+(Optional) +

SecretRef specifies the Kubernetes Secret containing the +trusted public keys.

+
diff --git a/go.mod b/go.mod index e0f1fa2d0..669f71271 100644 --- a/go.mod +++ b/go.mod @@ -8,9 +8,9 @@ require ( github.com/fluxcd/helm-controller/api v0.25.0 github.com/fluxcd/pkg/apis/acl v0.1.0 github.com/fluxcd/pkg/apis/kustomize v0.6.0 - github.com/fluxcd/pkg/apis/meta v0.16.0 + github.com/fluxcd/pkg/apis/meta v0.17.0 github.com/fluxcd/pkg/runtime v0.19.0 - github.com/fluxcd/source-controller/api v0.30.0 + github.com/fluxcd/source-controller/api v0.31.0 github.com/go-logr/logr v1.2.3 github.com/hashicorp/go-retryablehttp v0.7.1 github.com/onsi/gomega v1.20.2 @@ -18,7 +18,7 @@ require ( helm.sh/helm/v3 v3.10.0 k8s.io/api v0.25.2 k8s.io/apiextensions-apiserver v0.25.2 - k8s.io/apimachinery v0.25.2 + k8s.io/apimachinery v0.25.3 k8s.io/cli-runtime v0.25.2 k8s.io/client-go v0.25.2 k8s.io/utils v0.0.0-20220922133306-665eaaec4324 diff --git a/go.sum b/go.sum index c9bf39183..d795e13af 100644 --- a/go.sum +++ b/go.sum @@ -180,12 +180,12 @@ github.com/fluxcd/pkg/apis/acl v0.1.0 h1:EoAl377hDQYL3WqanWCdifauXqXbMyFuK82NnX6 github.com/fluxcd/pkg/apis/acl v0.1.0/go.mod h1:zfEZzz169Oap034EsDhmCAGgnWlcWmIObZjYMusoXS8= github.com/fluxcd/pkg/apis/kustomize v0.6.0 h1:Afxv3Uv+xiuettzqm3sP0ceWikDZTfHdHtLv6u2nFM8= github.com/fluxcd/pkg/apis/kustomize v0.6.0/go.mod h1:iY0zSpK6eUiPfNt/yR6g0q/wQP+wH+Ax/L7KBOx5x2M= -github.com/fluxcd/pkg/apis/meta v0.16.0 h1:6Mj9rB0TtvCeTe3IlQDc1i2DH75Oosea9yUqS7XafVg= -github.com/fluxcd/pkg/apis/meta v0.16.0/go.mod h1:GrOVzWXiu22XjLNgLLe2EBYhQPqZetes5SIADb4bmHE= +github.com/fluxcd/pkg/apis/meta v0.17.0 h1:Y2dfo1syHZDb9Mexjr2SWdcj1FnxnRXm015hEnhl6wU= +github.com/fluxcd/pkg/apis/meta v0.17.0/go.mod h1:GrOVzWXiu22XjLNgLLe2EBYhQPqZetes5SIADb4bmHE= github.com/fluxcd/pkg/runtime v0.19.0 h1:4lRlnZfJFhWvuaNWgNsAkPQg09633xCRCf9d0SgXIWk= github.com/fluxcd/pkg/runtime v0.19.0/go.mod h1:9Kh46LjwQeUu6o1DUQulLGyo5e5wfQxeFf4ONNobT3U= -github.com/fluxcd/source-controller/api v0.30.0 h1:rPVPpwXcYG2n0DTRcRagfGDiccvCib5S09K5iMjlpRU= -github.com/fluxcd/source-controller/api v0.30.0/go.mod h1:UkjAqQ6QAXNNesNQDTArTeiTp+UuhOUIA+JyFhGP/+Q= +github.com/fluxcd/source-controller/api v0.31.0 h1:4PZQt2XILTUZ/2JOVGzAIpNDXjx8n10skAhuBHa9tVw= +github.com/fluxcd/source-controller/api v0.31.0/go.mod h1:XOf8hJB7jFcAKiOb8HVZcegkBeNSb4g0nxqnNjeVufg= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= @@ -1094,8 +1094,8 @@ k8s.io/api v0.25.2 h1:v6G8RyFcwf0HR5jQGIAYlvtRNrxMJQG1xJzaSeVnIS8= k8s.io/api v0.25.2/go.mod h1:qP1Rn4sCVFwx/xIhe+we2cwBLTXNcheRyYXwajonhy0= k8s.io/apiextensions-apiserver v0.25.2 h1:8uOQX17RE7XL02ngtnh3TgifY7EhekpK+/piwzQNnBo= k8s.io/apiextensions-apiserver v0.25.2/go.mod h1:iRwwRDlWPfaHhuBfQ0WMa5skdQfrE18QXJaJvIDLvE8= -k8s.io/apimachinery v0.25.2 h1:WbxfAjCx+AeN8Ilp9joWnyJ6xu9OMeS/fsfjK/5zaQs= -k8s.io/apimachinery v0.25.2/go.mod h1:hqqA1X0bsgsxI6dXsJ4HnNTBOmJNxyPp8dw3u2fSHwA= +k8s.io/apimachinery v0.25.3 h1:7o9ium4uyUOM76t6aunP0nZuex7gDf8VGwkR5RcJnQc= +k8s.io/apimachinery v0.25.3/go.mod h1:jaF9C/iPNM1FuLl7Zuy5b9v+n35HGSh6AQ4HYRkCqwo= k8s.io/apiserver v0.25.2 h1:YePimobk187IMIdnmsMxsfIbC5p4eX3WSOrS9x6FEYw= k8s.io/apiserver v0.25.2/go.mod h1:30r7xyQTREWCkG2uSjgjhQcKVvAAlqoD+YyrqR6Cn+I= k8s.io/cli-runtime v0.25.2 h1:XOx+SKRjBpYMLY/J292BHTkmyDffl/qOx3YSuFZkTuc= From e5f7b8ccb449f903ab2cb18c3372d9b27c548b50 Mon Sep 17 00:00:00 2001 From: Soule BA Date: Fri, 21 Oct 2022 16:47:43 +0200 Subject: [PATCH 2/2] Add an e2e test for keyless verification Signed-off-by: Soule BA --- .github/workflows/e2e.yaml | 1 + config/default/kustomization.yaml | 4 ++-- config/testdata/podinfo/helmrelease-oci.yaml | 21 +++++++++++++++++++ config/testdata/sources/helmrepository.yaml | 2 +- .../testdata/sources/helmrepository_oci.yaml | 8 +++++++ 5 files changed, 33 insertions(+), 3 deletions(-) create mode 100644 config/testdata/podinfo/helmrelease-oci.yaml create mode 100644 config/testdata/sources/helmrepository_oci.yaml diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index 80a8dd90a..c87323d20 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -110,6 +110,7 @@ jobs: kubectl -n helm-system apply -f config/testdata/podinfo kubectl -n helm-system wait helmreleases/podinfo --for=condition=ready --timeout=4m kubectl -n helm-system wait helmreleases/podinfo-git --for=condition=ready --timeout=4m + kubectl -n helm-system wait helmreleases/podinfo-oci --for=condition=ready --timeout=4m kubectl -n helm-system delete -f config/testdata/podinfo - name: Run dependency tests run: | diff --git a/config/default/kustomization.yaml b/config/default/kustomization.yaml index eaef9e214..4e7fcf0c0 100644 --- a/config/default/kustomization.yaml +++ b/config/default/kustomization.yaml @@ -2,8 +2,8 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization namespace: helm-system resources: -- https://github.com/fluxcd/source-controller/releases/download/v0.25.3/source-controller.crds.yaml -- https://github.com/fluxcd/source-controller/releases/download/v0.25.3/source-controller.deployment.yaml +- https://github.com/fluxcd/source-controller/releases/download/v0.31.0/source-controller.crds.yaml +- https://github.com/fluxcd/source-controller/releases/download/v0.31.0/source-controller.deployment.yaml - ../crd - ../rbac - ../manager diff --git a/config/testdata/podinfo/helmrelease-oci.yaml b/config/testdata/podinfo/helmrelease-oci.yaml new file mode 100644 index 000000000..10e078bee --- /dev/null +++ b/config/testdata/podinfo/helmrelease-oci.yaml @@ -0,0 +1,21 @@ +apiVersion: helm.toolkit.fluxcd.io/v2beta1 +kind: HelmRelease +metadata: + name: podinfo-oci +spec: + interval: 5m + chart: + spec: + chart: podinfo + version: '6.2.1' + sourceRef: + kind: HelmRepository + name: podinfo-oci + interval: 1m + verify: + provider: cosign + values: + resources: + requests: + cpu: 100m + memory: 64Mi diff --git a/config/testdata/sources/helmrepository.yaml b/config/testdata/sources/helmrepository.yaml index 79008e7b8..c83ef482b 100644 --- a/config/testdata/sources/helmrepository.yaml +++ b/config/testdata/sources/helmrepository.yaml @@ -1,4 +1,4 @@ -apiVersion: source.toolkit.fluxcd.io/v1beta1 +apiVersion: source.toolkit.fluxcd.io/v1beta2 kind: HelmRepository metadata: name: podinfo diff --git a/config/testdata/sources/helmrepository_oci.yaml b/config/testdata/sources/helmrepository_oci.yaml new file mode 100644 index 000000000..f0648c7a5 --- /dev/null +++ b/config/testdata/sources/helmrepository_oci.yaml @@ -0,0 +1,8 @@ +apiVersion: source.toolkit.fluxcd.io/v1beta2 +kind: HelmRepository +metadata: + name: podinfo-oci +spec: + interval: 1m + url: oci://ghcr.io/stefanprodan/charts + type: "oci"