Skip to content

Commit

Permalink
Introduce Initial OCIRepository Source Verification
Browse files Browse the repository at this point in the history
Fixes fluxcd#863

Signed-off-by: Furkan <furkan.turkal@trendyol.com>
Co-authored-by: Batuhan <batuhan.apaydin@trendyol.com>
Signed-off-by: Batuhan Apaydın <batuhan.apaydin@trendyol.com>
  • Loading branch information
Dentrax and developer-guy committed Sep 13, 2022
1 parent 9e853a9 commit 71c0bb0
Show file tree
Hide file tree
Showing 15 changed files with 1,548 additions and 67 deletions.
1 change: 1 addition & 0 deletions .github/workflows/e2e.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ on:
push:
branches:
- main
- feature/863

permissions:
contents: read # for actions/checkout to fetch code
Expand Down
4 changes: 4 additions & 0 deletions api/v1beta2/condition_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ const (
// required fields, or the provided credentials do not match.
AuthenticationFailedReason string = "AuthenticationFailed"

// VerificationError signals that the Source's verification
// check failed.
VerificationError string = "VerificationError"

// DirCreationFailedReason signals a failure caused by a directory creation
// operation.
DirCreationFailedReason string = "DirectoryCreationFailed"
Expand Down
10 changes: 9 additions & 1 deletion api/v1beta2/ocirepository_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,12 @@ type OCIRepositorySpec struct {
// +optional
SecretRef *meta.LocalObjectReference `json:"secretRef,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.
// +optional
Verify *OCIRepositoryVerification `json:"verify,omitempty"`

// ServiceAccountName is the name of the Kubernetes ServiceAccount used to authenticate
// the image pull if the service account has attached pull secrets. For more information:
// https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/#add-imagepullsecrets-to-a-service-account
Expand Down Expand Up @@ -152,11 +158,13 @@ type OCILayerSelector struct {
type OCIRepositoryVerification struct {
// Provider specifies the technology used to sign the OCI Artifact.
// +kubebuilder:validation:Enum=cosign
// +kubebuilder:default:=cosign
Provider string `json:"provider"`

// SecretRef specifies the Kubernetes Secret containing the
// trusted public keys.
SecretRef meta.LocalObjectReference `json:"secretRef"`
// +optional
SecretRef *meta.LocalObjectReference `json:"secretRef"`
}

// OCIRepositoryStatus defines the observed state of OCIRepository
Expand Down
11 changes: 10 additions & 1 deletion 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_ocirepositories.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,31 @@ spec:
on a remote container registry.
pattern: ^oci://.*$
type: string
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
required:
- interval
- url
Expand Down
2 changes: 2 additions & 0 deletions config/manager/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ spec:
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: TUF_ROOT
value: "/tmp/.sigstore"
args:
- --watch-all-namespaces
- --log-level=info
Expand Down
14 changes: 14 additions & 0 deletions config/testdata/ocirepository/signed-with-key.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
apiVersion: source.toolkit.fluxcd.io/v1beta2
kind: OCIRepository
metadata:
name: podinfo-deploy-signed-with-key
spec:
interval: 5m
url: oci://ghcr.io/stefanprodan/podinfo-deploy
ref:
semver: "6.2.x"
verify:
provider: cosign
secretRef:
name: cosign-key
12 changes: 12 additions & 0 deletions config/testdata/ocirepository/signed-with-keyless.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
apiVersion: source.toolkit.fluxcd.io/v1beta2
kind: OCIRepository
metadata:
name: podinfo-deploy-signed-with-keyless
spec:
interval: 5m
url: oci://ghcr.io/stefanprodan/manifests/podinfo
ref:
semver: "6.2.x"
verify:
provider: cosign
104 changes: 102 additions & 2 deletions controllers/ocirepository_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ import (
"strings"
"time"

soci "github.com/fluxcd/source-controller/internal/oci"

"github.com/Masterminds/semver/v3"
"github.com/google/go-containerregistry/pkg/authn"
"github.com/google/go-containerregistry/pkg/authn/k8schain"
Expand Down Expand Up @@ -408,6 +410,20 @@ func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, obj *sour

// Extract the content of the first artifact layer
if !obj.GetArtifact().HasRevision(revision) {
if obj.Spec.Verify != nil {
provider := obj.Spec.Verify.Provider
err := r.verifyOCISourceSignature(ctx, obj, url, keychain)
if err != nil {
e := serror.NewGeneric(
fmt.Errorf("failed to verify OCI image signature '%s' using provider '%s': %w", url, provider, err),
sourcev1.VerificationError,
)
conditions.MarkFalse(obj, sourcev1.SourceVerifiedCondition, e.Reason, e.Err.Error())
return sreconcile.ResultEmpty, e
}

conditions.MarkTrue(obj, sourcev1.SourceVerifiedCondition, meta.SucceededReason, "OCI image %s with digest %s verified.", url, imgDigest)
}
layers, err := img.Layers()
if err != nil {
e := serror.NewGeneric(
Expand Down Expand Up @@ -484,6 +500,90 @@ func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, obj *sour
return sreconcile.ResultSuccess, nil
}

// verifyOCISourceSignature verifies the authenticity of the given image reference url. First, it tries to keyful approach
// by looking at whether the given secret exists. Then, if it does not exist, it pushes a keyless approach for verification.
func (r *OCIRepositoryReconciler) verifyOCISourceSignature(ctx context.Context, obj *sourcev1.OCIRepository, url string, keychain authn.Keychain) error {
// Verify the image
if obj.Spec.Verify != nil {
provider := obj.Spec.Verify.Provider
switch provider {
case "cosign":
// get the public keys from the given secret
secretRef := obj.Spec.Verify.SecretRef

defaultCosignOciOpts := []soci.Options{
soci.WithAuthnKeychain(keychain),
soci.WithContext(ctx),
}

ref, err := name.ParseReference(url)
if err != nil {
return err
}

if 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 err
}

signatureVerified := false
// traverse all public keys and try to verify the signature
// this is brute-force approach, but it is ok for now
for k, data := range pubSecret.Data {
// search for public keys in the secret
if strings.HasSuffix(k, ".pub") {
verifier, err := soci.New(append(defaultCosignOciOpts, soci.WithPublicKey(data))...)
if err != nil {
return err
}

signatures, _, err := verifier.VerifyImageSignatures(ctx, ref)
if err != nil {
continue
}

if signatures != nil {
signatureVerified = true
break
}
}
}

if !signatureVerified {
ctrl.LoggerFrom(ctx).Error(err, "none of the keys in the secret %s succeeded to verify for the image %s", secretRef.Name)
return fmt.Errorf("no matching signatures were found for the image %s", url)
}

return nil

} else {
ctrl.LoggerFrom(ctx).Info("no secret reference is provided, trying to verify the image using keyless approach")
verifier, err := soci.New(defaultCosignOciOpts...)
if err != nil {
return err
}

signatures, _, err := verifier.VerifyImageSignatures(ctx, ref)
if err != nil {
return err
}

if len(signatures) > 0 {
return nil
}
}
return nil
}
}
return nil
}

// parseRepositoryURL validates and extracts the repository URL.
func (r *OCIRepositoryReconciler) parseRepositoryURL(obj *sourcev1.OCIRepository) (string, error) {
if !strings.HasPrefix(obj.Spec.URL, sourcev1.OCIRepositoryPrefix) {
Expand Down Expand Up @@ -651,7 +751,6 @@ func (r *OCIRepositoryReconciler) transport(ctx context.Context, obj *sourcev1.O
tlsConfig.RootCAs = syscerts
}
return transport, nil

}

// oidcAuth generates the OIDC credential authenticator based on the specified cloud provider.
Expand Down Expand Up @@ -883,7 +982,8 @@ func (r *OCIRepositoryReconciler) garbageCollect(ctx context.Context, obj *sourc
// that this is a simple log. While the debug log contains complete details
// about the event.
func (r *OCIRepositoryReconciler) eventLogf(ctx context.Context,
obj runtime.Object, eventType string, reason string, messageFmt string, args ...interface{}) {
obj runtime.Object, eventType, reason, messageFmt string, args ...interface{},
) {
msg := fmt.Sprintf(messageFmt, args...)
// Log and emit event.
if eventType == corev1.EventTypeWarning {
Expand Down
Loading

0 comments on commit 71c0bb0

Please sign in to comment.