Skip to content

Commit 697f885

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 <soule@weave.works>
1 parent b443e10 commit 697f885

14 files changed

+494
-17
lines changed

api/v1beta2/helmchart_types.go

+8
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+
// Optional dependencies e.g. umbrella chart are not verified.
95+
// +optional
96+
Verify *OCIRepositoryVerification `json:"verify,omitempty"`
8997
}
9098

9199
const (

api/v1beta2/zz_generated.deepcopy.go

+5
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

+25
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,31 @@ 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.
410+
properties:
411+
provider:
412+
default: cosign
413+
description: Provider specifies the technology used to sign the
414+
OCI Artifact.
415+
enum:
416+
- cosign
417+
type: string
418+
secretRef:
419+
description: SecretRef specifies the Kubernetes Secret containing
420+
the trusted public keys.
421+
properties:
422+
name:
423+
description: Name of the referent.
424+
type: string
425+
required:
426+
- name
427+
type: object
428+
required:
429+
- provider
430+
type: object
406431
version:
407432
default: '*'
408433
description: Version is the chart version semver expression, ignored

controllers/helmchart_controller.go

+98-3
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"
@@ -70,6 +72,8 @@ import (
7072
"github.com/fluxcd/source-controller/internal/util"
7173
)
7274

75+
const cosignSignatureKey = "cosign.pub"
76+
7377
// helmChartReadyCondition contains all the conditions information
7478
// needed for HelmChart Ready status conditions summary calculation.
7579
var helmChartReadyCondition = summarize.Conditions{
@@ -80,6 +84,7 @@ var helmChartReadyCondition = summarize.Conditions{
8084
sourcev1.BuildFailedCondition,
8185
sourcev1.ArtifactOutdatedCondition,
8286
sourcev1.ArtifactInStorageCondition,
87+
sourcev1.SourceVerifiedCondition,
8388
meta.ReadyCondition,
8489
meta.ReconcilingCondition,
8590
meta.StalledCondition,
@@ -90,6 +95,7 @@ var helmChartReadyCondition = summarize.Conditions{
9095
sourcev1.BuildFailedCondition,
9196
sourcev1.ArtifactOutdatedCondition,
9297
sourcev1.ArtifactInStorageCondition,
98+
sourcev1.SourceVerifiedCondition,
9399
meta.StalledCondition,
94100
meta.ReconcilingCondition,
95101
},
@@ -563,17 +569,38 @@ func (r *HelmChartReconciler) buildFromHelmRepository(ctx context.Context, obj *
563569
}()
564570
}
565571

572+
var verifier soci.Verifier
573+
if obj.Spec.Verify != nil {
574+
provider := obj.Spec.Verify.Provider
575+
verifier, err = r.makeVerifier(ctx, obj, authenticator, keychain)
576+
if err != nil {
577+
if obj.Spec.Verify.SecretRef == nil {
578+
provider = fmt.Sprintf("%s keyless", provider)
579+
}
580+
e := serror.NewGeneric(
581+
fmt.Errorf("failed to verify the signature using provider '%s': %w", provider, err),
582+
sourcev1.VerificationError,
583+
)
584+
conditions.MarkFalse(obj, sourcev1.SourceVerifiedCondition, e.Reason, e.Err.Error())
585+
return sreconcile.ResultEmpty, e
586+
}
587+
}
588+
566589
// Tell the chart repository to use the OCI client with the configured getter
567590
clientOpts = append(clientOpts, helmgetter.WithRegistryClient(registryClient))
568-
ociChartRepo, err := repository.NewOCIChartRepository(normalizedURL, repository.WithOCIGetter(r.Getters), repository.WithOCIGetterOptions(clientOpts), repository.WithOCIRegistryClient(registryClient))
591+
ociChartRepo, err := repository.NewOCIChartRepository(normalizedURL,
592+
repository.WithOCIGetter(r.Getters),
593+
repository.WithOCIGetterOptions(clientOpts),
594+
repository.WithOCIRegistryClient(registryClient),
595+
repository.WithVerifier(verifier))
569596
if err != nil {
570597
return chartRepoConfigErrorReturn(err, obj)
571598
}
572599
chartRepo = ociChartRepo
573600

574601
// If login options are configured, use them to login to the registry
575602
// The OCIGetter will later retrieve the stored credentials to pull the chart
576-
if keychain != nil {
603+
if loginOpt != nil {
577604
err = ociChartRepo.Login(loginOpt)
578605
if err != nil {
579606
e := &serror.Event{
@@ -621,6 +648,17 @@ func (r *HelmChartReconciler) buildFromHelmRepository(ctx context.Context, obj *
621648
opts := chart.BuildOptions{
622649
ValuesFiles: obj.GetValuesFiles(),
623650
Force: obj.Generation != obj.Status.ObservedGeneration,
651+
// The remote builder will not attempt to download the chart if
652+
// an artifact exist with the same name and version and the force is false.
653+
// It will try to verify the chart if:
654+
// - we are on the first reconciliation
655+
// - the HelmChart spec has changed (generation drift)
656+
// - the previous reconciliation resulted in a failed artifact verification
657+
// - there is no artifact in storage
658+
Verify: obj.Spec.Verify != nil && (obj.Generation <= 0 ||
659+
conditions.GetObservedGeneration(obj, sourcev1.SourceVerifiedCondition) != obj.Generation ||
660+
conditions.IsFalse(obj, sourcev1.SourceVerifiedCondition) ||
661+
obj.GetArtifact() == nil),
624662
}
625663
if artifact := obj.GetArtifact(); artifact != nil {
626664
opts.CachedChart = r.Storage.LocalPath(*artifact)
@@ -1029,7 +1067,7 @@ func (r *HelmChartReconciler) namespacedChartRepositoryCallback(ctx context.Cont
10291067

10301068
// If login options are configured, use them to login to the registry
10311069
// The OCIGetter will later retrieve the stored credentials to pull the chart
1032-
if keychain != nil {
1070+
if loginOpt != nil {
10331071
err = ociChartRepo.Login(loginOpt)
10341072
if err != nil {
10351073
errs = append(errs, fmt.Errorf("failed to login to OCI chart repository for HelmRepository '%s': %w", repo.Name, err))
@@ -1238,6 +1276,11 @@ func observeChartBuild(obj *sourcev1.HelmChart, build *chart.Build, err error) {
12381276
if build.Complete() {
12391277
conditions.Delete(obj, sourcev1.FetchFailedCondition)
12401278
conditions.Delete(obj, sourcev1.BuildFailedCondition)
1279+
conditions.MarkTrue(obj, sourcev1.SourceVerifiedCondition, meta.SucceededReason, fmt.Sprintf("verified signature of version %s", build.Version))
1280+
}
1281+
1282+
if obj.Spec.Verify == nil {
1283+
conditions.Delete(obj, sourcev1.SourceVerifiedCondition)
12411284
}
12421285

12431286
if err != nil {
@@ -1253,6 +1296,10 @@ func observeChartBuild(obj *sourcev1.HelmChart, build *chart.Build, err error) {
12531296
case chart.ErrChartMetadataPatch, chart.ErrValuesFilesMerge, chart.ErrDependencyBuild, chart.ErrChartPackage:
12541297
conditions.Delete(obj, sourcev1.FetchFailedCondition)
12551298
conditions.MarkTrue(obj, sourcev1.BuildFailedCondition, buildErr.Reason.Reason, buildErr.Error())
1299+
case chart.ErrChartVerification:
1300+
conditions.Delete(obj, sourcev1.FetchFailedCondition)
1301+
conditions.MarkTrue(obj, sourcev1.BuildFailedCondition, buildErr.Reason.Reason, buildErr.Error())
1302+
conditions.MarkFalse(obj, sourcev1.SourceVerifiedCondition, buildErr.Reason.Reason, buildErr.Error())
12561303
default:
12571304
conditions.Delete(obj, sourcev1.BuildFailedCondition)
12581305
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, buildErr.Reason.Reason, buildErr.Error())
@@ -1289,3 +1336,51 @@ func chartRepoConfigErrorReturn(err error, obj *sourcev1.HelmChart) (sreconcile.
12891336
return sreconcile.ResultEmpty, e
12901337
}
12911338
}
1339+
1340+
// getVerifyOptions returns the verify options for the given chart.
1341+
func (r *HelmChartReconciler) makeVerifier(ctx context.Context, obj *sourcev1.HelmChart, auth authn.Authenticator, keychain authn.Keychain) (soci.Verifier, error) {
1342+
var publicKey []byte
1343+
verifyOpts := []remote.Option{}
1344+
if auth != nil {
1345+
verifyOpts = append(verifyOpts, remote.WithAuth(auth))
1346+
} else {
1347+
verifyOpts = append(verifyOpts, remote.WithAuthFromKeychain(keychain))
1348+
}
1349+
1350+
// get the public keys from the given secret
1351+
if secretRef := obj.Spec.Verify.SecretRef; secretRef != nil {
1352+
certSecretName := types.NamespacedName{
1353+
Namespace: obj.Namespace,
1354+
Name: secretRef.Name,
1355+
}
1356+
1357+
var pubSecret corev1.Secret
1358+
if err := r.Get(ctx, certSecretName, &pubSecret); err != nil {
1359+
return nil, err
1360+
}
1361+
1362+
switch obj.Spec.Verify.Provider {
1363+
case "cosign":
1364+
// we expect to find this key in the secret for cosign verification. We want to avoid
1365+
// having to look for a random public key.
1366+
if key, ok := pubSecret.Data[cosignSignatureKey]; ok {
1367+
publicKey = key
1368+
}
1369+
default:
1370+
}
1371+
}
1372+
1373+
switch obj.Spec.Verify.Provider {
1374+
case "cosign":
1375+
defaultCosignOciOpts := []soci.Options{
1376+
soci.WithRemoteOptions(verifyOpts...),
1377+
}
1378+
verifier, err := soci.NewCosignVerifier(ctx, append(defaultCosignOciOpts, soci.WithPublicKey(publicKey))...)
1379+
if err != nil {
1380+
return nil, err
1381+
}
1382+
return verifier, nil
1383+
default:
1384+
return nil, fmt.Errorf("unsupported verification provider: %s", obj.Spec.Verify.Provider)
1385+
}
1386+
}

0 commit comments

Comments
 (0)