Skip to content

Commit 3b966fe

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 430f507 commit 3b966fe

File tree

9 files changed

+1144
-73
lines changed

9 files changed

+1144
-73
lines changed

api/v1beta1/zz_generated.deepcopy.go

Lines changed: 0 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

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,12 @@ 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+
// https://github.com/fluxcd/flux2/tree/main/rfcs/0003-kubernetes-oci
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
@@ -148,6 +154,7 @@ type OCILayerSelector struct {
148154
type OCIRepositoryVerification struct {
149155
// Provider specifies the technology used to sign the OCI Artifact.
150156
// +kubebuilder:validation:Enum=cosign
157+
// +kubebuilder:default:=cosign
151158
Provider string `json:"provider"`
152159

153160
// SecretRef specifies the Kubernetes Secret containing the

api/v1beta2/zz_generated.deepcopy.go

Lines changed: 5 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: 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: 80 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/signature"
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,29 @@ 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+
provider := obj.Spec.Verify.Provider
371+
if provider == "cosign" {
372+
if _, err := r.verify(ctx, obj, url); err != nil {
373+
e := serror.NewGeneric(
374+
fmt.Errorf("failed to verify '%s' using provider '%s': %w", url, provider, err),
375+
sourcev1.SourceVerifiedFailedReason,
376+
)
377+
conditions.MarkFalse(obj, sourcev1.SourceVerifiedCondition, e.Reason, e.Err.Error())
378+
return sreconcile.ResultEmpty, e
379+
}
380+
} else {
381+
err := fmt.Errorf("could not found the given %s provider, only valid provider for verification is: cosign", provider)
382+
e := serror.NewGeneric(
383+
fmt.Errorf("failed to verify '%s' using provider '%s': %w", url, provider, err),
384+
sourcev1.SourceVerifiedFailedReason,
385+
)
386+
conditions.MarkFalse(obj, sourcev1.SourceVerifiedCondition, e.Reason, e.Err.Error())
387+
return sreconcile.ResultEmpty, e
388+
}
389+
}
390+
365391
// Pull artifact from the remote container registry
366392
img, err := crane.Pull(url, options...)
367393
if err != nil {
@@ -658,7 +684,6 @@ func (r *OCIRepositoryReconciler) transport(ctx context.Context, obj *sourcev1.O
658684
tlsConfig.RootCAs = syscerts
659685
}
660686
return transport, nil
661-
662687
}
663688

664689
// oidcAuth generates the OIDC credential authenticator based on the specified cloud provider.
@@ -705,7 +730,8 @@ func (r *OCIRepositoryReconciler) craneOptions(ctx context.Context) []crane.Opti
705730
// The hostname of any URL in the Status of the object are updated, to ensure
706731
// they match the Storage server hostname of current runtime.
707732
func (r *OCIRepositoryReconciler) reconcileStorage(ctx context.Context,
708-
obj *sourcev1.OCIRepository, _ *sourcev1.Artifact, _ string) (sreconcile.Result, error) {
733+
obj *sourcev1.OCIRepository, _ *sourcev1.Artifact, _ string,
734+
) (sreconcile.Result, error) {
709735
// Garbage collect previous advertised artifact(s) from storage
710736
_ = r.garbageCollect(ctx, obj)
711737

@@ -741,7 +767,8 @@ func (r *OCIRepositoryReconciler) reconcileStorage(ctx context.Context,
741767
// On a successful archive, the Artifact in the Status of the object is set,
742768
// and the symlink in the Storage is updated to its path.
743769
func (r *OCIRepositoryReconciler) reconcileArtifact(ctx context.Context,
744-
obj *sourcev1.OCIRepository, metadata *sourcev1.Artifact, dir string) (sreconcile.Result, error) {
770+
obj *sourcev1.OCIRepository, metadata *sourcev1.Artifact, dir string,
771+
) (sreconcile.Result, error) {
745772
// Calculate revision
746773
revision := metadata.Revision
747774

@@ -885,7 +912,8 @@ func (r *OCIRepositoryReconciler) garbageCollect(ctx context.Context, obj *sourc
885912
// that this is a simple log. While the debug log contains complete details
886913
// about the event.
887914
func (r *OCIRepositoryReconciler) eventLogf(ctx context.Context,
888-
obj runtime.Object, eventType string, reason string, messageFmt string, args ...interface{}) {
915+
obj runtime.Object, eventType, reason, messageFmt string, args ...interface{},
916+
) {
889917
msg := fmt.Sprintf(messageFmt, args...)
890918
// Log and emit event.
891919
if eventType == corev1.EventTypeWarning {
@@ -898,7 +926,8 @@ func (r *OCIRepositoryReconciler) eventLogf(ctx context.Context,
898926

899927
// notify emits notification related to the reconciliation.
900928
func (r *OCIRepositoryReconciler) notify(ctx context.Context,
901-
oldObj, newObj *sourcev1.OCIRepository, res sreconcile.Result, resErr error) {
929+
oldObj, newObj *sourcev1.OCIRepository, res sreconcile.Result, resErr error,
930+
) {
902931
// Notify successful reconciliation for new artifact and recovery from any
903932
// failure.
904933
if resErr == nil && res == sreconcile.ResultSuccess && newObj.Status.Artifact != nil {
@@ -942,3 +971,49 @@ func (r *OCIRepositoryReconciler) notify(ctx context.Context,
942971
}
943972
}
944973
}
974+
975+
// notify emits notification related to the reconciliation.
976+
func (r *OCIRepositoryReconciler) verify(ctx context.Context, obj *sourcev1.OCIRepository, url string) (bool, error) {
977+
// get the public keys from the given secret
978+
certSecretName := types.NamespacedName{
979+
Namespace: obj.Namespace,
980+
Name: obj.Spec.Verify.SecretRef.Name,
981+
}
982+
983+
var pubSecret corev1.Secret
984+
if err := r.Get(ctx, certSecretName, &pubSecret); err != nil {
985+
return false, err
986+
}
987+
988+
ref, err := name.ParseReference(url)
989+
if err != nil {
990+
return false, err
991+
}
992+
993+
// traverse all public keys and try to verify the signature
994+
// this is brute-force approach, but it is ok for now
995+
verified := false
996+
for _, data := range pubSecret.Data {
997+
pubRaw, err := base64.StdEncoding.DecodeString(string(data))
998+
if err != nil {
999+
return false, err
1000+
}
1001+
1002+
verifier, err := signature.New(signature.WithPublicKey(pubRaw))
1003+
if err != nil {
1004+
return false, err
1005+
}
1006+
1007+
signatures, _, err := verifier.VerifyImageSignatures(ctx, ref)
1008+
if err != nil {
1009+
return false, err
1010+
}
1011+
1012+
if len(signatures) > 0 {
1013+
verified = true
1014+
break
1015+
}
1016+
}
1017+
1018+
return verified, nil
1019+
}

0 commit comments

Comments
 (0)