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 8, 2022
1 parent dc80d4f commit d881461
Show file tree
Hide file tree
Showing 13 changed files with 1,432 additions and 66 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
29 changes: 29 additions & 0 deletions config/testdata/ocirepository/signed.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
apiVersion: source.toolkit.fluxcd.io/v1beta2
kind: OCIRepository
metadata:
name: podinfo-deploy-signed
spec:
interval: 5m
url: oci://ghcr.io/stefanprodan/podinfo-deploy
ref:
semver: "6.0.x"
verify:
provider: cosign
secretRef:
name: cosign-key
---
apiVersion: kustomize.toolkit.fluxcd.io/v1beta1
kind: Kustomization
metadata:
name: podinfo-deploy-signed
spec:
sourceRef:
kind: OCIRepository
name: podinfo-deploy-signed
interval: 60m
retryInterval: 5m
path: ./
prune: true
wait: true
timeout: 2m
27 changes: 27 additions & 0 deletions config/testdata/ocirepository/unsigned.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
apiVersion: source.toolkit.fluxcd.io/v1beta2
kind: OCIRepository
metadata:
name: podinfo-deploy-unsigned
spec:
interval: 5m
url: oci://ghcr.io/stefanprodan/podinfo-deploy
ref:
semver: "6.0.x"
secretRef:
name: ghcr-auth
---
apiVersion: kustomize.toolkit.fluxcd.io/v1beta2
kind: Kustomization
metadata:
name: podinfo-deploy-unsigned
spec:
sourceRef:
kind: OCIRepository
name: podinfo-deploy-unsigned
interval: 60m
retryInterval: 5m
path: ./
prune: true
wait: true
timeout: 2m
105 changes: 103 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)
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, "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,91 @@ 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) 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

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

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
}

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

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

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

if !verified {
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 {
verifier, err := soci.New(authnKeychain)
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 +752,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 +983,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
37 changes: 37 additions & 0 deletions docs/api/source.md
Original file line number Diff line number Diff line change
Expand Up @@ -1028,6 +1028,22 @@ The secret must be of type kubernetes.io/dockerconfigjson.</p>
</tr>
<tr>
<td>
<code>verify</code><br>
<em>
<a href="#source.toolkit.fluxcd.io/v1beta2.OCIRepositoryVerification">
OCIRepositoryVerification
</a>
</em>
</td>
<td>
<em>(Optional)</em>
<p>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.</p>
</td>
</tr>
<tr>
<td>
<code>serviceAccountName</code><br>
<em>
string
Expand Down Expand Up @@ -2772,6 +2788,22 @@ The secret must be of type kubernetes.io/dockerconfigjson.</p>
</tr>
<tr>
<td>
<code>verify</code><br>
<em>
<a href="#source.toolkit.fluxcd.io/v1beta2.OCIRepositoryVerification">
OCIRepositoryVerification
</a>
</em>
</td>
<td>
<em>(Optional)</em>
<p>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.</p>
</td>
</tr>
<tr>
<td>
<code>serviceAccountName</code><br>
<em>
string
Expand Down Expand Up @@ -2967,6 +2999,10 @@ github.com/fluxcd/pkg/apis/meta.ReconcileRequestStatus
</div>
<h3 id="source.toolkit.fluxcd.io/v1beta2.OCIRepositoryVerification">OCIRepositoryVerification
</h3>
<p>
(<em>Appears on:</em>
<a href="#source.toolkit.fluxcd.io/v1beta2.OCIRepositorySpec">OCIRepositorySpec</a>)
</p>
<p>OCIRepositoryVerification verifies the authenticity of an OCI Artifact</p>
<div class="md-typeset__scrollwrap">
<div class="md-typeset__table">
Expand Down Expand Up @@ -2999,6 +3035,7 @@ github.com/fluxcd/pkg/apis/meta.LocalObjectReference
</em>
</td>
<td>
<em>(Optional)</em>
<p>SecretRef specifies the Kubernetes Secret containing the
trusted public keys.</p>
</td>
Expand Down
Loading

0 comments on commit d881461

Please sign in to comment.