Skip to content

Commit 88346fd

Browse files
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 9df0102 commit 88346fd

File tree

9 files changed

+1219
-71
lines changed

9 files changed

+1219
-71
lines changed

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+
// SourceVerifiedFailedReason signals that the Source's verification
75+
// check failed.
76+
SourceVerifiedFailedReason string = "SourceVerificationFailed"
77+
7478
// DirCreationFailedReason signals a failure caused by a directory creation
7579
// operation.
7680
DirCreationFailedReason string = "DirectoryCreationFailed"

api/v1beta2/ocirepository_types.go

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

81+
// Verify specifies which provider to use to check whether OCI image is authentic.
82+
// Implements RFC-0003.
83+
// +kubebuilder:validation:Enum=cosign
84+
// +kubebuilder:default:=cosign
85+
// +optional
86+
Verify *OCIRepositoryVerification `json:"verify,omitempty"`
87+
8188
// ServiceAccountName is the name of the Kubernetes ServiceAccount used to authenticate
8289
// the image pull if the service account has attached pull secrets. For more information:
8390
// https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/#add-imagepullsecrets-to-a-service-account

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_ocirepositories.yaml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,33 @@ spec:
142142
on a remote container registry.
143143
pattern: ^oci://.*$
144144
type: string
145+
verify:
146+
default: cosign
147+
description: Verify specifies which provider to use to check whether
148+
OCI image is authentic. Implements RFC-0003.
149+
enum:
150+
- cosign
151+
properties:
152+
provider:
153+
description: Provider specifies the technology used to sign the
154+
OCI Artifact.
155+
enum:
156+
- cosign
157+
type: string
158+
secretRef:
159+
description: SecretRef specifies the Kubernetes Secret containing
160+
the trusted public keys.
161+
properties:
162+
name:
163+
description: Name of the referent.
164+
type: string
165+
required:
166+
- name
167+
type: object
168+
required:
169+
- provider
170+
- secretRef
171+
type: object
145172
required:
146173
- interval
147174
- url

controllers/ocirepository_controller.go

Lines changed: 73 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"context"
2121
"crypto/tls"
2222
"crypto/x509"
23+
"encoding/base64"
2324
"errors"
2425
"fmt"
2526
"net/http"
@@ -28,6 +29,8 @@ import (
2829
"strings"
2930
"time"
3031

32+
"github.com/fluxcd/source-controller/internal/verify"
33+
3134
"github.com/Masterminds/semver/v3"
3235
"github.com/google/go-containerregistry/pkg/authn"
3336
"github.com/google/go-containerregistry/pkg/authn/k8schain"
@@ -362,6 +365,18 @@ func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, obj *sour
362365
return sreconcile.ResultEmpty, e
363366
}
364367

368+
// Verify the image
369+
if obj.Spec.Verify != nil {
370+
if _, err := r.verify(ctx, obj, url); err != nil {
371+
e := serror.NewGeneric(
372+
fmt.Errorf("failed to verify '%s' using provider '%s': %w", url, obj.Spec.Verify.Provider, err),
373+
sourcev1.SourceVerifiedFailedReason,
374+
)
375+
conditions.MarkTrue(obj, sourcev1.SourceVerifiedCondition, e.Reason, e.Err.Error())
376+
return sreconcile.ResultEmpty, e
377+
}
378+
}
379+
365380
// Pull artifact from the remote container registry
366381
img, err := crane.Pull(url, options...)
367382
if err != nil {
@@ -658,7 +673,6 @@ func (r *OCIRepositoryReconciler) transport(ctx context.Context, obj *sourcev1.O
658673
tlsConfig.RootCAs = syscerts
659674
}
660675
return transport, nil
661-
662676
}
663677

664678
// oidcAuth generates the OIDC credential authenticator based on the specified cloud provider.
@@ -705,7 +719,8 @@ func (r *OCIRepositoryReconciler) craneOptions(ctx context.Context) []crane.Opti
705719
// The hostname of any URL in the Status of the object are updated, to ensure
706720
// they match the Storage server hostname of current runtime.
707721
func (r *OCIRepositoryReconciler) reconcileStorage(ctx context.Context,
708-
obj *sourcev1.OCIRepository, _ *sourcev1.Artifact, _ string) (sreconcile.Result, error) {
722+
obj *sourcev1.OCIRepository, _ *sourcev1.Artifact, _ string,
723+
) (sreconcile.Result, error) {
709724
// Garbage collect previous advertised artifact(s) from storage
710725
_ = r.garbageCollect(ctx, obj)
711726

@@ -741,7 +756,8 @@ func (r *OCIRepositoryReconciler) reconcileStorage(ctx context.Context,
741756
// On a successful archive, the Artifact in the Status of the object is set,
742757
// and the symlink in the Storage is updated to its path.
743758
func (r *OCIRepositoryReconciler) reconcileArtifact(ctx context.Context,
744-
obj *sourcev1.OCIRepository, metadata *sourcev1.Artifact, dir string) (sreconcile.Result, error) {
759+
obj *sourcev1.OCIRepository, metadata *sourcev1.Artifact, dir string,
760+
) (sreconcile.Result, error) {
745761
// Calculate revision
746762
revision := metadata.Revision
747763

@@ -885,7 +901,8 @@ func (r *OCIRepositoryReconciler) garbageCollect(ctx context.Context, obj *sourc
885901
// that this is a simple log. While the debug log contains complete details
886902
// about the event.
887903
func (r *OCIRepositoryReconciler) eventLogf(ctx context.Context,
888-
obj runtime.Object, eventType string, reason string, messageFmt string, args ...interface{}) {
904+
obj runtime.Object, eventType, reason, messageFmt string, args ...interface{},
905+
) {
889906
msg := fmt.Sprintf(messageFmt, args...)
890907
// Log and emit event.
891908
if eventType == corev1.EventTypeWarning {
@@ -898,7 +915,8 @@ func (r *OCIRepositoryReconciler) eventLogf(ctx context.Context,
898915

899916
// notify emits notification related to the reconciliation.
900917
func (r *OCIRepositoryReconciler) notify(ctx context.Context,
901-
oldObj, newObj *sourcev1.OCIRepository, res sreconcile.Result, resErr error) {
918+
oldObj, newObj *sourcev1.OCIRepository, res sreconcile.Result, resErr error,
919+
) {
902920
// Notify successful reconciliation for new artifact and recovery from any
903921
// failure.
904922
if resErr == nil && res == sreconcile.ResultSuccess && newObj.Status.Artifact != nil {
@@ -942,3 +960,53 @@ func (r *OCIRepositoryReconciler) notify(ctx context.Context,
942960
}
943961
}
944962
}
963+
964+
// notify emits notification related to the reconciliation.
965+
func (r *OCIRepositoryReconciler) verify(ctx context.Context, obj *sourcev1.OCIRepository, url string) (bool, error) {
966+
// get the public keys from the given secret
967+
certSecretName := types.NamespacedName{
968+
Namespace: obj.Namespace,
969+
Name: obj.Spec.Verify.SecretRef.Name,
970+
}
971+
972+
var pubSecret corev1.Secret
973+
if err := r.Get(ctx, certSecretName, &pubSecret); err != nil {
974+
return false, err
975+
}
976+
977+
ref, err := name.ParseReference(url)
978+
if err != nil {
979+
return false, err
980+
}
981+
982+
// traverse all public keys and try to verify the signature
983+
// this is brute-force approach, but it is ok for now
984+
verified := false
985+
for _, data := range pubSecret.Data {
986+
pubRaw, err := base64.StdEncoding.DecodeString(string(data))
987+
if err != nil {
988+
return false, err
989+
}
990+
991+
opts := []verify.Options{
992+
verify.WithPublicKey(pubRaw),
993+
}
994+
995+
verifier, err := verify.New(obj.Spec.Verify.Provider, opts...)
996+
if err != nil {
997+
return false, err
998+
}
999+
1000+
signatures, _, err := verifier.VerifyImageSignatures(ctx, ref)
1001+
if err != nil {
1002+
return false, err
1003+
}
1004+
1005+
if len(signatures) > 0 {
1006+
verified = true
1007+
break
1008+
}
1009+
}
1010+
1011+
return verified, nil
1012+
}

0 commit comments

Comments
 (0)