Skip to content

Commit

Permalink
implement Cosign verification for HelmCharts
Browse files Browse the repository at this point in the history
If implemented, users will be able to enable chart verification for OCI
based helm charts.

Signed-off-by: Soule BA <soule@weave.works>
  • Loading branch information
souleb committed Oct 4, 2022
1 parent b443e10 commit 697f885
Show file tree
Hide file tree
Showing 14 changed files with 494 additions and 17 deletions.
8 changes: 8 additions & 0 deletions api/v1beta2/helmchart_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,14 @@ type HelmChartSpec struct {
// NOTE: Not implemented, provisional as of https://github.com/fluxcd/flux2/pull/2092
// +optional
AccessFrom *acl.AccessFrom `json:"accessFrom,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.
// Optional dependencies e.g. umbrella chart are not verified.
// +optional
Verify *OCIRepositoryVerification `json:"verify,omitempty"`
}

const (
Expand Down
5 changes: 5 additions & 0 deletions api/v1beta2/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

25 changes: 25 additions & 0 deletions config/crd/bases/source.toolkit.fluxcd.io_helmcharts.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,31 @@ 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.
properties:
provider:
default: cosign
description: Provider specifies the technology used to sign the
OCI Artifact.
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 is the chart version semver expression, ignored
Expand Down
101 changes: 98 additions & 3 deletions controllers/helmchart_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
"strings"
"time"

soci "github.com/fluxcd/source-controller/internal/oci"
helmgetter "helm.sh/helm/v3/pkg/getter"
helmreg "helm.sh/helm/v3/pkg/registry"
corev1 "k8s.io/api/core/v1"
Expand Down Expand Up @@ -57,6 +58,7 @@ import (
"github.com/fluxcd/pkg/runtime/predicates"
"github.com/fluxcd/pkg/untar"
"github.com/google/go-containerregistry/pkg/authn"
"github.com/google/go-containerregistry/pkg/v1/remote"

sourcev1 "github.com/fluxcd/source-controller/api/v1beta2"
"github.com/fluxcd/source-controller/internal/cache"
Expand All @@ -70,6 +72,8 @@ import (
"github.com/fluxcd/source-controller/internal/util"
)

const cosignSignatureKey = "cosign.pub"

// helmChartReadyCondition contains all the conditions information
// needed for HelmChart Ready status conditions summary calculation.
var helmChartReadyCondition = summarize.Conditions{
Expand All @@ -80,6 +84,7 @@ var helmChartReadyCondition = summarize.Conditions{
sourcev1.BuildFailedCondition,
sourcev1.ArtifactOutdatedCondition,
sourcev1.ArtifactInStorageCondition,
sourcev1.SourceVerifiedCondition,
meta.ReadyCondition,
meta.ReconcilingCondition,
meta.StalledCondition,
Expand All @@ -90,6 +95,7 @@ var helmChartReadyCondition = summarize.Conditions{
sourcev1.BuildFailedCondition,
sourcev1.ArtifactOutdatedCondition,
sourcev1.ArtifactInStorageCondition,
sourcev1.SourceVerifiedCondition,
meta.StalledCondition,
meta.ReconcilingCondition,
},
Expand Down Expand Up @@ -563,17 +569,38 @@ func (r *HelmChartReconciler) buildFromHelmRepository(ctx context.Context, obj *
}()
}

var verifier soci.Verifier
if obj.Spec.Verify != nil {
provider := obj.Spec.Verify.Provider
verifier, err = r.makeVerifier(ctx, obj, authenticator, keychain)
if err != nil {
if obj.Spec.Verify.SecretRef == nil {
provider = fmt.Sprintf("%s keyless", provider)
}
e := serror.NewGeneric(
fmt.Errorf("failed to verify the signature using provider '%s': %w", provider, err),
sourcev1.VerificationError,
)
conditions.MarkFalse(obj, sourcev1.SourceVerifiedCondition, e.Reason, e.Err.Error())
return sreconcile.ResultEmpty, e
}
}

// Tell the chart repository to use the OCI client with the configured getter
clientOpts = append(clientOpts, helmgetter.WithRegistryClient(registryClient))
ociChartRepo, err := repository.NewOCIChartRepository(normalizedURL, repository.WithOCIGetter(r.Getters), repository.WithOCIGetterOptions(clientOpts), repository.WithOCIRegistryClient(registryClient))
ociChartRepo, err := repository.NewOCIChartRepository(normalizedURL,
repository.WithOCIGetter(r.Getters),
repository.WithOCIGetterOptions(clientOpts),
repository.WithOCIRegistryClient(registryClient),
repository.WithVerifier(verifier))
if err != nil {
return chartRepoConfigErrorReturn(err, obj)
}
chartRepo = ociChartRepo

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

// If login options are configured, use them to login to the registry
// The OCIGetter will later retrieve the stored credentials to pull the chart
if keychain != nil {
if loginOpt != nil {
err = ociChartRepo.Login(loginOpt)
if err != nil {
errs = append(errs, fmt.Errorf("failed to login to OCI chart repository for HelmRepository '%s': %w", repo.Name, err))
Expand Down Expand Up @@ -1238,6 +1276,11 @@ func observeChartBuild(obj *sourcev1.HelmChart, build *chart.Build, err error) {
if build.Complete() {
conditions.Delete(obj, sourcev1.FetchFailedCondition)
conditions.Delete(obj, sourcev1.BuildFailedCondition)
conditions.MarkTrue(obj, sourcev1.SourceVerifiedCondition, meta.SucceededReason, fmt.Sprintf("verified signature of version %s", build.Version))
}

if obj.Spec.Verify == nil {
conditions.Delete(obj, sourcev1.SourceVerifiedCondition)
}

if err != nil {
Expand All @@ -1253,6 +1296,10 @@ func observeChartBuild(obj *sourcev1.HelmChart, build *chart.Build, err error) {
case chart.ErrChartMetadataPatch, chart.ErrValuesFilesMerge, chart.ErrDependencyBuild, chart.ErrChartPackage:
conditions.Delete(obj, sourcev1.FetchFailedCondition)
conditions.MarkTrue(obj, sourcev1.BuildFailedCondition, buildErr.Reason.Reason, buildErr.Error())
case chart.ErrChartVerification:
conditions.Delete(obj, sourcev1.FetchFailedCondition)
conditions.MarkTrue(obj, sourcev1.BuildFailedCondition, buildErr.Reason.Reason, buildErr.Error())
conditions.MarkFalse(obj, sourcev1.SourceVerifiedCondition, buildErr.Reason.Reason, buildErr.Error())
default:
conditions.Delete(obj, sourcev1.BuildFailedCondition)
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, buildErr.Reason.Reason, buildErr.Error())
Expand Down Expand Up @@ -1289,3 +1336,51 @@ func chartRepoConfigErrorReturn(err error, obj *sourcev1.HelmChart) (sreconcile.
return sreconcile.ResultEmpty, e
}
}

// getVerifyOptions returns the verify options for the given chart.
func (r *HelmChartReconciler) makeVerifier(ctx context.Context, obj *sourcev1.HelmChart, auth authn.Authenticator, keychain authn.Keychain) (soci.Verifier, error) {
var publicKey []byte
verifyOpts := []remote.Option{}
if auth != nil {
verifyOpts = append(verifyOpts, remote.WithAuth(auth))
} else {
verifyOpts = append(verifyOpts, remote.WithAuthFromKeychain(keychain))
}

// get the public keys from the given secret
if secretRef := obj.Spec.Verify.SecretRef; secretRef != nil {
certSecretName := types.NamespacedName{
Namespace: obj.Namespace,
Name: secretRef.Name,
}

var pubSecret corev1.Secret
if err := r.Get(ctx, certSecretName, &pubSecret); err != nil {
return nil, err
}

switch obj.Spec.Verify.Provider {
case "cosign":
// we expect to find this key in the secret for cosign verification. We want to avoid
// having to look for a random public key.
if key, ok := pubSecret.Data[cosignSignatureKey]; ok {
publicKey = key
}
default:
}
}

switch obj.Spec.Verify.Provider {
case "cosign":
defaultCosignOciOpts := []soci.Options{
soci.WithRemoteOptions(verifyOpts...),
}
verifier, err := soci.NewCosignVerifier(ctx, append(defaultCosignOciOpts, soci.WithPublicKey(publicKey))...)
if err != nil {
return nil, err
}
return verifier, nil
default:
return nil, fmt.Errorf("unsupported verification provider: %s", obj.Spec.Verify.Provider)
}
}
Loading

0 comments on commit 697f885

Please sign in to comment.