Skip to content

Commit 0e97547

Browse files
committed
implement Cosign verification for HelmCharts
If implemented, users will be able to enable chart verification for OCI based helm charts. Signed-off-by: Soule BA <[email protected]>
1 parent 55dd799 commit 0e97547

File tree

16 files changed

+522
-18
lines changed

16 files changed

+522
-18
lines changed

api/v1beta2/helmchart_types.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,14 @@ type HelmChartSpec struct {
8686
// NOTE: Not implemented, provisional as of https://github.com/fluxcd/flux2/pull/2092
8787
// +optional
8888
AccessFrom *acl.AccessFrom `json:"accessFrom,omitempty"`
89+
90+
// Verify contains the secret name containing the trusted public keys
91+
// used to verify the signature and specifies which provider to use to check
92+
// whether OCI image is authentic.
93+
// This field is only supported for OCI sources.
94+
// Chart dependencies, which are not bundled in the umbrella chart artifact, are not verified.
95+
// +optional
96+
Verify *OCIRepositoryVerification `json:"verify,omitempty"`
8997
}
9098

9199
const (

api/v1beta2/zz_generated.deepcopy.go

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

config/crd/bases/source.toolkit.fluxcd.io_helmcharts.yaml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,33 @@ spec:
403403
items:
404404
type: string
405405
type: array
406+
verify:
407+
description: Verify contains the secret name containing the trusted
408+
public keys used to verify the signature and specifies which provider
409+
to use to check whether OCI image is authentic. This field is only
410+
supported for OCI sources. Chart dependencies, which are not bundled
411+
in the umbrella chart artifact, are not verified.
412+
properties:
413+
provider:
414+
default: cosign
415+
description: Provider specifies the technology used to sign the
416+
OCI Artifact.
417+
enum:
418+
- cosign
419+
type: string
420+
secretRef:
421+
description: SecretRef specifies the Kubernetes Secret containing
422+
the trusted public keys.
423+
properties:
424+
name:
425+
description: Name of the referent.
426+
type: string
427+
required:
428+
- name
429+
type: object
430+
required:
431+
- provider
432+
type: object
406433
version:
407434
default: '*'
408435
description: Version is the chart version semver expression, ignored

config/testdata/helmchart-from-oci/source.yaml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,17 @@ spec:
1919
name: podinfo
2020
version: '6.1.*'
2121
interval: 1m
22+
---
23+
apiVersion: source.toolkit.fluxcd.io/v1beta2
24+
kind: HelmChart
25+
metadata:
26+
name: podinfo-keyless
27+
spec:
28+
chart: podinfo
29+
sourceRef:
30+
kind: HelmRepository
31+
name: podinfo
32+
version: '6.2.1'
33+
interval: 1m
34+
verify:
35+
provider: cosign

controllers/helmchart_controller.go

Lines changed: 102 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import (
2828
"strings"
2929
"time"
3030

31+
soci "github.com/fluxcd/source-controller/internal/oci"
3132
helmgetter "helm.sh/helm/v3/pkg/getter"
3233
helmreg "helm.sh/helm/v3/pkg/registry"
3334
corev1 "k8s.io/api/core/v1"
@@ -57,6 +58,7 @@ import (
5758
"github.com/fluxcd/pkg/runtime/predicates"
5859
"github.com/fluxcd/pkg/untar"
5960
"github.com/google/go-containerregistry/pkg/authn"
61+
"github.com/google/go-containerregistry/pkg/v1/remote"
6062

6163
sourcev1 "github.com/fluxcd/source-controller/api/v1beta2"
6264
"github.com/fluxcd/source-controller/internal/cache"
@@ -80,6 +82,7 @@ var helmChartReadyCondition = summarize.Conditions{
8082
sourcev1.BuildFailedCondition,
8183
sourcev1.ArtifactOutdatedCondition,
8284
sourcev1.ArtifactInStorageCondition,
85+
sourcev1.SourceVerifiedCondition,
8386
meta.ReadyCondition,
8487
meta.ReconcilingCondition,
8588
meta.StalledCondition,
@@ -90,6 +93,7 @@ var helmChartReadyCondition = summarize.Conditions{
9093
sourcev1.BuildFailedCondition,
9194
sourcev1.ArtifactOutdatedCondition,
9295
sourcev1.ArtifactInStorageCondition,
96+
sourcev1.SourceVerifiedCondition,
9397
meta.StalledCondition,
9498
meta.ReconcilingCondition,
9599
},
@@ -564,17 +568,38 @@ func (r *HelmChartReconciler) buildFromHelmRepository(ctx context.Context, obj *
564568
}()
565569
}
566570

571+
var verifiers []soci.Verifier
572+
if obj.Spec.Verify != nil {
573+
provider := obj.Spec.Verify.Provider
574+
verifiers, err = r.makeVerifiers(ctx, obj, authenticator, keychain)
575+
if err != nil {
576+
if obj.Spec.Verify.SecretRef == nil {
577+
provider = fmt.Sprintf("%s keyless", provider)
578+
}
579+
e := serror.NewGeneric(
580+
fmt.Errorf("failed to verify the signature using provider '%s': %w", provider, err),
581+
sourcev1.VerificationError,
582+
)
583+
conditions.MarkFalse(obj, sourcev1.SourceVerifiedCondition, e.Reason, e.Err.Error())
584+
return sreconcile.ResultEmpty, e
585+
}
586+
}
587+
567588
// Tell the chart repository to use the OCI client with the configured getter
568589
clientOpts = append(clientOpts, helmgetter.WithRegistryClient(registryClient))
569-
ociChartRepo, err := repository.NewOCIChartRepository(normalizedURL, repository.WithOCIGetter(r.Getters), repository.WithOCIGetterOptions(clientOpts), repository.WithOCIRegistryClient(registryClient))
590+
ociChartRepo, err := repository.NewOCIChartRepository(normalizedURL,
591+
repository.WithOCIGetter(r.Getters),
592+
repository.WithOCIGetterOptions(clientOpts),
593+
repository.WithOCIRegistryClient(registryClient),
594+
repository.WithVerifiers(verifiers))
570595
if err != nil {
571596
return chartRepoConfigErrorReturn(err, obj)
572597
}
573598
chartRepo = ociChartRepo
574599

575600
// If login options are configured, use them to login to the registry
576601
// The OCIGetter will later retrieve the stored credentials to pull the chart
577-
if keychain != nil {
602+
if loginOpt != nil {
578603
err = ociChartRepo.Login(loginOpt)
579604
if err != nil {
580605
e := &serror.Event{
@@ -622,6 +647,17 @@ func (r *HelmChartReconciler) buildFromHelmRepository(ctx context.Context, obj *
622647
opts := chart.BuildOptions{
623648
ValuesFiles: obj.GetValuesFiles(),
624649
Force: obj.Generation != obj.Status.ObservedGeneration,
650+
// The remote builder will not attempt to download the chart if
651+
// an artifact exist with the same name and version and the force is false.
652+
// It will try to verify the chart if:
653+
// - we are on the first reconciliation
654+
// - the HelmChart spec has changed (generation drift)
655+
// - the previous reconciliation resulted in a failed artifact verification
656+
// - there is no artifact in storage
657+
Verify: obj.Spec.Verify != nil && (obj.Generation <= 0 ||
658+
conditions.GetObservedGeneration(obj, sourcev1.SourceVerifiedCondition) != obj.Generation ||
659+
conditions.IsFalse(obj, sourcev1.SourceVerifiedCondition) ||
660+
obj.GetArtifact() == nil),
625661
}
626662
if artifact := obj.GetArtifact(); artifact != nil {
627663
opts.CachedChart = r.Storage.LocalPath(*artifact)
@@ -1030,7 +1066,7 @@ func (r *HelmChartReconciler) namespacedChartRepositoryCallback(ctx context.Cont
10301066

10311067
// If login options are configured, use them to login to the registry
10321068
// The OCIGetter will later retrieve the stored credentials to pull the chart
1033-
if keychain != nil {
1069+
if loginOpt != nil {
10341070
err = ociChartRepo.Login(loginOpt)
10351071
if err != nil {
10361072
errs = append(errs, fmt.Errorf("failed to login to OCI chart repository for HelmRepository '%s': %w", repo.Name, err))
@@ -1239,6 +1275,11 @@ func observeChartBuild(obj *sourcev1.HelmChart, build *chart.Build, err error) {
12391275
if build.Complete() {
12401276
conditions.Delete(obj, sourcev1.FetchFailedCondition)
12411277
conditions.Delete(obj, sourcev1.BuildFailedCondition)
1278+
conditions.MarkTrue(obj, sourcev1.SourceVerifiedCondition, meta.SucceededReason, fmt.Sprintf("verified signature of version %s", build.Version))
1279+
}
1280+
1281+
if obj.Spec.Verify == nil {
1282+
conditions.Delete(obj, sourcev1.SourceVerifiedCondition)
12421283
}
12431284

12441285
if err != nil {
@@ -1251,7 +1292,7 @@ func observeChartBuild(obj *sourcev1.HelmChart, build *chart.Build, err error) {
12511292
}
12521293

12531294
switch buildErr.Reason {
1254-
case chart.ErrChartMetadataPatch, chart.ErrValuesFilesMerge, chart.ErrDependencyBuild, chart.ErrChartPackage:
1295+
case chart.ErrChartMetadataPatch, chart.ErrValuesFilesMerge, chart.ErrDependencyBuild, chart.ErrChartPackage, chart.ErrChartVerification:
12551296
conditions.Delete(obj, sourcev1.FetchFailedCondition)
12561297
conditions.MarkTrue(obj, sourcev1.BuildFailedCondition, buildErr.Reason.Reason, buildErr.Error())
12571298
default:
@@ -1290,3 +1331,60 @@ func chartRepoConfigErrorReturn(err error, obj *sourcev1.HelmChart) (sreconcile.
12901331
return sreconcile.ResultEmpty, e
12911332
}
12921333
}
1334+
1335+
// makeVerifiers returns a list of verifiers for the given chart.
1336+
func (r *HelmChartReconciler) makeVerifiers(ctx context.Context, obj *sourcev1.HelmChart, auth authn.Authenticator, keychain authn.Keychain) ([]soci.Verifier, error) {
1337+
var verifiers []soci.Verifier
1338+
verifyOpts := []remote.Option{}
1339+
if auth != nil {
1340+
verifyOpts = append(verifyOpts, remote.WithAuth(auth))
1341+
} else {
1342+
verifyOpts = append(verifyOpts, remote.WithAuthFromKeychain(keychain))
1343+
}
1344+
1345+
switch obj.Spec.Verify.Provider {
1346+
case "cosign":
1347+
defaultCosignOciOpts := []soci.Options{
1348+
soci.WithRemoteOptions(verifyOpts...),
1349+
}
1350+
1351+
// get the public keys from the given secret
1352+
if secretRef := obj.Spec.Verify.SecretRef; secretRef != nil {
1353+
certSecretName := types.NamespacedName{
1354+
Namespace: obj.Namespace,
1355+
Name: secretRef.Name,
1356+
}
1357+
1358+
var pubSecret corev1.Secret
1359+
if err := r.Get(ctx, certSecretName, &pubSecret); err != nil {
1360+
return nil, err
1361+
}
1362+
1363+
for k, data := range pubSecret.Data {
1364+
// search for public keys in the secret
1365+
if strings.HasSuffix(k, ".pub") {
1366+
verifier, err := soci.NewCosignVerifier(ctx, append(defaultCosignOciOpts, soci.WithPublicKey(data))...)
1367+
if err != nil {
1368+
return nil, err
1369+
}
1370+
verifiers = append(verifiers, verifier)
1371+
}
1372+
}
1373+
1374+
if len(verifiers) == 0 {
1375+
return nil, fmt.Errorf("no public keys found in secret '%s'", certSecretName)
1376+
}
1377+
return verifiers, nil
1378+
}
1379+
1380+
// if no secret is provided, add a keyless verifier
1381+
verifier, err := soci.NewCosignVerifier(ctx, defaultCosignOciOpts...)
1382+
if err != nil {
1383+
return nil, err
1384+
}
1385+
verifiers = append(verifiers, verifier)
1386+
return verifiers, nil
1387+
default:
1388+
return nil, fmt.Errorf("unsupported verification provider: %s", obj.Spec.Verify.Provider)
1389+
}
1390+
}

0 commit comments

Comments
 (0)