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 Aug 28, 2022
1 parent 430f507 commit 7438508
Show file tree
Hide file tree
Showing 9 changed files with 1,295 additions and 75 deletions.
1 change: 0 additions & 1 deletion api/v1beta1/zz_generated.deepcopy.go

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

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"

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

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

// Verify 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 @@ -148,11 +152,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
12 changes: 10 additions & 2 deletions api/v1beta2/zz_generated.deepcopy.go

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

24 changes: 24 additions & 0 deletions config/crd/bases/source.toolkit.fluxcd.io_ocirepositories.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,30 @@ spec:
on a remote container registry.
pattern: ^oci://.*$
type: string
verify:
description: Verify 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
116 changes: 111 additions & 5 deletions controllers/ocirepository_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ import (
"strings"
"time"

"github.com/fluxcd/source-controller/internal/signature"

"github.com/Masterminds/semver/v3"
"github.com/google/go-containerregistry/pkg/authn"
"github.com/google/go-containerregistry/pkg/authn/k8schain"
Expand Down Expand Up @@ -362,6 +364,38 @@ func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, obj *sour
return sreconcile.ResultEmpty, e
}

// Verify the image
if obj.Spec.Verify != nil {
provider := obj.Spec.Verify.Provider
if provider == "cosign" {
if verified, err := r.verify(ctx, obj, url); err != nil {
e := serror.NewGeneric(
fmt.Errorf("failed to verify '%s' using provider '%s': %w", url, provider, err),
sourcev1.SourceVerifiedFailedReason,
)
conditions.MarkFalse(obj, sourcev1.SourceVerifiedCondition, e.Reason, e.Err.Error())
return sreconcile.ResultEmpty, e
} else {
if !verified {
e := serror.NewGeneric(
fmt.Errorf("no matching signatures '%s' using provider '%s': %w", url, provider, err),
sourcev1.SourceVerifiedFailedReason,
)
conditions.MarkFalse(obj, sourcev1.SourceVerifiedCondition, e.Reason, e.Err.Error())
return sreconcile.ResultEmpty, nil
}
}
} else {
err := fmt.Errorf("could not found the given %s provider, only valid provider for verification is: cosign", provider)
e := serror.NewGeneric(
fmt.Errorf("failed to verify '%s' using provider '%s': %w", url, provider, err),
sourcev1.SourceVerifiedFailedReason,
)
conditions.MarkFalse(obj, sourcev1.SourceVerifiedCondition, e.Reason, e.Err.Error())
return sreconcile.ResultEmpty, e
}
}

// Pull artifact from the remote container registry
img, err := crane.Pull(url, options...)
if err != nil {
Expand Down Expand Up @@ -658,7 +692,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 @@ -705,7 +738,8 @@ func (r *OCIRepositoryReconciler) craneOptions(ctx context.Context) []crane.Opti
// The hostname of any URL in the Status of the object are updated, to ensure
// they match the Storage server hostname of current runtime.
func (r *OCIRepositoryReconciler) reconcileStorage(ctx context.Context,
obj *sourcev1.OCIRepository, _ *sourcev1.Artifact, _ string) (sreconcile.Result, error) {
obj *sourcev1.OCIRepository, _ *sourcev1.Artifact, _ string,
) (sreconcile.Result, error) {
// Garbage collect previous advertised artifact(s) from storage
_ = r.garbageCollect(ctx, obj)

Expand Down Expand Up @@ -741,7 +775,8 @@ func (r *OCIRepositoryReconciler) reconcileStorage(ctx context.Context,
// On a successful archive, the Artifact in the Status of the object is set,
// and the symlink in the Storage is updated to its path.
func (r *OCIRepositoryReconciler) reconcileArtifact(ctx context.Context,
obj *sourcev1.OCIRepository, metadata *sourcev1.Artifact, dir string) (sreconcile.Result, error) {
obj *sourcev1.OCIRepository, metadata *sourcev1.Artifact, dir string,
) (sreconcile.Result, error) {
// Calculate revision
revision := metadata.Revision

Expand Down Expand Up @@ -885,7 +920,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 All @@ -898,7 +934,8 @@ func (r *OCIRepositoryReconciler) eventLogf(ctx context.Context,

// notify emits notification related to the reconciliation.
func (r *OCIRepositoryReconciler) notify(ctx context.Context,
oldObj, newObj *sourcev1.OCIRepository, res sreconcile.Result, resErr error) {
oldObj, newObj *sourcev1.OCIRepository, res sreconcile.Result, resErr error,
) {
// Notify successful reconciliation for new artifact and recovery from any
// failure.
if resErr == nil && res == sreconcile.ResultSuccess && newObj.Status.Artifact != nil {
Expand Down Expand Up @@ -942,3 +979,72 @@ func (r *OCIRepositoryReconciler) notify(ctx context.Context,
}
}
}

// notify emits notification related to the reconciliation.
func (r *OCIRepositoryReconciler) verify(ctx context.Context, obj *sourcev1.OCIRepository, url string) (bool, error) {
// get the public keys from the given secret
secretRef := obj.Spec.Verify.SecretRef

// Generate the registry credential keychain either from static credentials or using cloud OIDC
keychain, err := r.keychain(ctx, obj)
if err != nil {
return false, err
}

authnKeychain := signature.WithAuthnKeychain(keychain)

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

if secretRef != nil {
ctrl.LoggerFrom(ctx).Info(fmt.Sprintf("secretRef found: %s", secretRef.Name))
certSecretName := types.NamespacedName{
Namespace: obj.Namespace,
Name: secretRef.Name,
}

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

// 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 := signature.New(signature.WithPublicKey(data), authnKeychain)
if err != nil {
return false, err
}

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

if len(signatures) > 0 {
return true, nil
}
}
}
} else {
ctrl.LoggerFrom(ctx).Info(fmt.Sprintf("secretRef not found: %s, trying keyless verification.", secretRef.Name))
verifier, err := signature.New(authnKeychain)
if err != nil {
return false, err
}

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

if len(signatures) > 0 {
return true, nil
}
}
return false, nil
}
Loading

0 comments on commit 7438508

Please sign in to comment.