Skip to content

Commit 697f260

Browse files
Dentraxdeveloper-guy
authored andcommitted
Introduce Initial OCIRepository Source Verification
Fixes #863 Signed-off-by: Furkan <[email protected]> Co-authored-by: Batuhan <[email protected]> Signed-off-by: Batuhan Apaydın <[email protected]>
1 parent 54d706a commit 697f260

File tree

15 files changed

+1548
-67
lines changed

15 files changed

+1548
-67
lines changed

.github/workflows/e2e.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ on:
99
push:
1010
branches:
1111
- main
12+
- feature/863
1213

1314
permissions:
1415
contents: read # for actions/checkout to fetch code

api/v1beta2/condition_types.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,10 @@ const (
7171
// required fields, or the provided credentials do not match.
7272
AuthenticationFailedReason string = "AuthenticationFailed"
7373

74+
// VerificationError signals that the Source's verification
75+
// check failed.
76+
VerificationError string = "VerificationError"
77+
7478
// DirCreationFailedReason signals a failure caused by a directory creation
7579
// operation.
7680
DirCreationFailedReason string = "DirectoryCreationFailed"

api/v1beta2/ocirepository_types.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,12 @@ type OCIRepositorySpec struct {
7878
// +optional
7979
SecretRef *meta.LocalObjectReference `json:"secretRef,omitempty"`
8080

81+
// Verify contains the secret name containing the trusted public keys
82+
// used to verify the signature and specifies which provider to use to check
83+
// whether OCI image is authentic.
84+
// +optional
85+
Verify *OCIRepositoryVerification `json:"verify,omitempty"`
86+
8187
// ServiceAccountName is the name of the Kubernetes ServiceAccount used to authenticate
8288
// the image pull if the service account has attached pull secrets. For more information:
8389
// https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/#add-imagepullsecrets-to-a-service-account
@@ -156,11 +162,13 @@ type OCILayerSelector struct {
156162
type OCIRepositoryVerification struct {
157163
// Provider specifies the technology used to sign the OCI Artifact.
158164
// +kubebuilder:validation:Enum=cosign
165+
// +kubebuilder:default:=cosign
159166
Provider string `json:"provider"`
160167

161168
// SecretRef specifies the Kubernetes Secret containing the
162169
// trusted public keys.
163-
SecretRef meta.LocalObjectReference `json:"secretRef"`
170+
// +optional
171+
SecretRef *meta.LocalObjectReference `json:"secretRef"`
164172
}
165173

166174
// OCIRepositoryStatus defines the observed state of OCIRepository

api/v1beta2/zz_generated.deepcopy.go

Lines changed: 10 additions & 1 deletion
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_ocirepositories.yaml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,31 @@ spec:
148148
on a remote container registry.
149149
pattern: ^oci://.*$
150150
type: string
151+
verify:
152+
description: Verify contains the secret name containing the trusted
153+
public keys used to verify the signature and specifies which provider
154+
to use to check whether OCI image is authentic.
155+
properties:
156+
provider:
157+
default: cosign
158+
description: Provider specifies the technology used to sign the
159+
OCI Artifact.
160+
enum:
161+
- cosign
162+
type: string
163+
secretRef:
164+
description: SecretRef specifies the Kubernetes Secret containing
165+
the trusted public keys.
166+
properties:
167+
name:
168+
description: Name of the referent.
169+
type: string
170+
required:
171+
- name
172+
type: object
173+
required:
174+
- provider
175+
type: object
151176
required:
152177
- interval
153178
- url

config/manager/deployment.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ spec:
5151
valueFrom:
5252
fieldRef:
5353
fieldPath: metadata.namespace
54+
- name: TUF_ROOT
55+
value: "/tmp/.sigstore"
5456
args:
5557
- --watch-all-namespaces
5658
- --log-level=info
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
apiVersion: source.toolkit.fluxcd.io/v1beta2
3+
kind: OCIRepository
4+
metadata:
5+
name: podinfo-deploy-signed-with-key
6+
spec:
7+
interval: 5m
8+
url: oci://ghcr.io/stefanprodan/podinfo-deploy
9+
ref:
10+
semver: "6.2.x"
11+
verify:
12+
provider: cosign
13+
secretRef:
14+
name: cosign-key
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
apiVersion: source.toolkit.fluxcd.io/v1beta2
3+
kind: OCIRepository
4+
metadata:
5+
name: podinfo-deploy-signed-with-keyless
6+
spec:
7+
interval: 5m
8+
url: oci://ghcr.io/stefanprodan/manifests/podinfo
9+
ref:
10+
semver: "6.2.x"
11+
verify:
12+
provider: cosign

controllers/ocirepository_controller.go

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

31+
soci "github.com/fluxcd/source-controller/internal/oci"
32+
3133
"github.com/Masterminds/semver/v3"
3234
"github.com/google/go-containerregistry/pkg/authn"
3335
"github.com/google/go-containerregistry/pkg/authn/k8schain"
@@ -408,6 +410,20 @@ func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, obj *sour
408410

409411
// Extract the content of the first artifact layer
410412
if !obj.GetArtifact().HasRevision(revision) {
413+
if obj.Spec.Verify != nil {
414+
provider := obj.Spec.Verify.Provider
415+
err := r.verifyOCISourceSignature(ctx, obj, url, keychain)
416+
if err != nil {
417+
e := serror.NewGeneric(
418+
fmt.Errorf("failed to verify OCI image signature '%s' using provider '%s': %w", url, provider, err),
419+
sourcev1.VerificationError,
420+
)
421+
conditions.MarkFalse(obj, sourcev1.SourceVerifiedCondition, e.Reason, e.Err.Error())
422+
return sreconcile.ResultEmpty, e
423+
}
424+
425+
conditions.MarkTrue(obj, sourcev1.SourceVerifiedCondition, meta.SucceededReason, "OCI image %s with digest %s verified.", url, imgDigest)
426+
}
411427
layers, err := img.Layers()
412428
if err != nil {
413429
e := serror.NewGeneric(
@@ -484,6 +500,90 @@ func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, obj *sour
484500
return sreconcile.ResultSuccess, nil
485501
}
486502

503+
// verifyOCISourceSignature verifies the authenticity of the given image reference url. First, it tries to keyful approach
504+
// by looking at whether the given secret exists. Then, if it does not exist, it pushes a keyless approach for verification.
505+
func (r *OCIRepositoryReconciler) verifyOCISourceSignature(ctx context.Context, obj *sourcev1.OCIRepository, url string, keychain authn.Keychain) error {
506+
// Verify the image
507+
if obj.Spec.Verify != nil {
508+
provider := obj.Spec.Verify.Provider
509+
switch provider {
510+
case "cosign":
511+
// get the public keys from the given secret
512+
secretRef := obj.Spec.Verify.SecretRef
513+
514+
defaultCosignOciOpts := []soci.Options{
515+
soci.WithAuthnKeychain(keychain),
516+
soci.WithContext(ctx),
517+
}
518+
519+
ref, err := name.ParseReference(url)
520+
if err != nil {
521+
return err
522+
}
523+
524+
if secretRef != nil {
525+
certSecretName := types.NamespacedName{
526+
Namespace: obj.Namespace,
527+
Name: secretRef.Name,
528+
}
529+
530+
var pubSecret corev1.Secret
531+
if err := r.Get(ctx, certSecretName, &pubSecret); err != nil {
532+
return err
533+
}
534+
535+
signatureVerified := false
536+
// traverse all public keys and try to verify the signature
537+
// this is brute-force approach, but it is ok for now
538+
for k, data := range pubSecret.Data {
539+
// search for public keys in the secret
540+
if strings.HasSuffix(k, ".pub") {
541+
verifier, err := soci.New(append(defaultCosignOciOpts, soci.WithPublicKey(data))...)
542+
if err != nil {
543+
return err
544+
}
545+
546+
signatures, _, err := verifier.VerifyImageSignatures(ctx, ref)
547+
if err != nil {
548+
continue
549+
}
550+
551+
if signatures != nil {
552+
signatureVerified = true
553+
break
554+
}
555+
}
556+
}
557+
558+
if !signatureVerified {
559+
ctrl.LoggerFrom(ctx).Error(err, "none of the keys in the secret %s succeeded to verify for the image %s", secretRef.Name)
560+
return fmt.Errorf("no matching signatures were found for the image %s", url)
561+
}
562+
563+
return nil
564+
565+
} else {
566+
ctrl.LoggerFrom(ctx).Info("no secret reference is provided, trying to verify the image using keyless approach")
567+
verifier, err := soci.New(defaultCosignOciOpts...)
568+
if err != nil {
569+
return err
570+
}
571+
572+
signatures, _, err := verifier.VerifyImageSignatures(ctx, ref)
573+
if err != nil {
574+
return err
575+
}
576+
577+
if len(signatures) > 0 {
578+
return nil
579+
}
580+
}
581+
return nil
582+
}
583+
}
584+
return nil
585+
}
586+
487587
// parseRepositoryURL validates and extracts the repository URL.
488588
func (r *OCIRepositoryReconciler) parseRepositoryURL(obj *sourcev1.OCIRepository) (string, error) {
489589
if !strings.HasPrefix(obj.Spec.URL, sourcev1.OCIRepositoryPrefix) {
@@ -651,7 +751,6 @@ func (r *OCIRepositoryReconciler) transport(ctx context.Context, obj *sourcev1.O
651751
tlsConfig.RootCAs = syscerts
652752
}
653753
return transport, nil
654-
655754
}
656755

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

0 commit comments

Comments
 (0)