Skip to content

Commit 85658aa

Browse files
committed
feat: use kyverno for image signature verification
TODO: - unit tests - tests with cosign v3 - check that the status from 'kyverno.io/verify-images' is a pass Signed-off-by: Maryam Tahhan <mtahhan@redhat.com>
1 parent c81df35 commit 85658aa

File tree

12 files changed

+139
-329
lines changed

12 files changed

+139
-329
lines changed

Makefile

Lines changed: 3 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -305,15 +305,15 @@ else
305305
endif
306306

307307
.PHONY: deploy
308-
deploy: manifests kustomize prepare-deploy webhook-secret-file deploy-cert-manager redeploy ## Deploy controller and agent to the K8s cluster specified in ~/.kube/config
308+
deploy: manifests kustomize prepare-deploy deploy-cert-manager redeploy ## Deploy controller and agent to the K8s cluster specified in ~/.kube/config
309309

310310
.PHONY: redeploy
311311
redeploy: ## Redeploy controller and agent to the K8s cluster after deploy and undeploy have been called. Skips some onetime steps in deploy.
312312
$(KUSTOMIZE) build $(DEPLOY_PATH) | $(KUBECTL) apply -f -
313313
@echo "Deployment to $(DEPLOY_PATH) completed."
314314

315315
.PHONY: undeploy
316-
undeploy: kustomize delete-webhook-secret-file ## Undeploy operator and agent from the K8s cluster specified in ~/.kube/config.
316+
undeploy: kustomize ## Undeploy operator and agent from the K8s cluster specified in ~/.kube/config.
317317
@echo "Calling undeploy script"
318318
$(UNDEPLOY_SCRIPT) $(FORCE)
319319
@if [ $$? -ne 0 ]; then \
@@ -367,22 +367,6 @@ get-example-images:
367367
deploy-webhook-certs:
368368
$(KUBECTL) apply -k config/webhook
369369

370-
.PHONY: webhook-secret-file
371-
webhook-secret-file:
372-
@mkdir -p config/secret
373-
@[ -s config/secret/mutation.env ] || \
374-
(echo 'Generating config/secret/mutation.env'; \
375-
printf 'MUTATION_SIGNING_KEY=%s\n' "$$(head -c 32 /dev/urandom | base64 | tr -d '\n')" > config/secret/mutation.env)
376-
377-
.PHONY: delete-webhook-secret-file
378-
delete-webhook-secret-file:
379-
@rm -f 0config/secret/mutation.env
380-
381-
.PHONY: rotate-webhook-secret
382-
rotate-webhook-secret:
383-
@printf 'MUTATION_SIGNING_KEY=%s\n' "$$(head -c 32 /dev/urandom | base64 | tr -d '\n')" > config/secret/mutation.env
384-
$(KUSTOMIZE) build config/secret | $(KUBECTL) apply -f -
385-
386370
.PHONY: get-cert-manager-images
387371
get-cert-manager-images:
388372
@echo "Getting Images ..."
@@ -415,7 +399,7 @@ deploy-cert-manager: get-cert-manager-images
415399
$(KUBECTL) wait --for=condition=Ready --timeout=120s -n cert-manager pod -l app=webhook
416400

417401
.PHONY: undeploy-cert-manager
418-
undeploy-cert-manager: delete-webhook-secret-file
402+
undeploy-cert-manager:
419403
@echo "Undeploy cert-manager"
420404
$(KUBECTL) delete -f https://github.com/cert-manager/cert-manager/releases/latest/download/cert-manager.yaml --ignore-not-found=$(ignore-not-found)
421405

api/v1alpha1/clustergkmcache_webhook.go

Lines changed: 10 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package v1alpha1
33
import (
44
"context"
55
"fmt"
6-
"time"
76

87
apierrors "k8s.io/apimachinery/pkg/api/errors"
98
"k8s.io/apimachinery/pkg/runtime"
@@ -38,10 +37,6 @@ func (w *ClusterGKMCache) SetupWebhookWithManager(mgr ctrl.Manager) error {
3837
// +kubebuilder:webhook:path=/validate-gkm-io-v1alpha1-clustergkmcache,mutating=false,failurePolicy=fail,sideEffects=None,groups=gkm.io,resources=clustergkmcaches,verbs=create;update,versions=v1alpha1,name=vclustergkmcache.kb.io,admissionReviewVersions=v1
3938

4039
// Default implements the mutating webhook logic for defaulting.
41-
// The mutating webhook writes both the resolved digest and a
42-
// gkm.io/mutationSig that’s bound to the current AdmissionRequest UID + image
43-
// + digest. The validating webhooks only accept the digest if that signature
44-
// is valid, which guarantees the digest came from the mutator (not the user).
4540
func (w *ClusterGKMCache) Default(ctx context.Context, obj runtime.Object) error {
4641
clustergkmcacheLog.V(1).Info("Mutating Webhook called", "object", obj)
4742

@@ -60,18 +55,11 @@ func (w *ClusterGKMCache) Default(ctx context.Context, obj runtime.Object) error
6055
return nil
6156
}
6257

63-
// Resolve & verify image -> digest
64-
cctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
65-
defer cancel()
66-
67-
clustergkmcacheLog.V(1).Info("Verifying image signature", "image", cache.Spec.Image)
68-
digest, err := verifyImageSignature(cctx, cache.Spec.Image)
69-
if err != nil {
70-
clustergkmcacheLog.Error(err, "failed to verify image or resolve digest")
71-
return apierrors.NewBadRequest(fmt.Sprintf(
72-
"image signature verification failed for '%s': %s",
73-
cache.Spec.Image, err.Error(),
74-
))
58+
// First check if the image already contains a digest (e.g., from Kyverno mutation)
59+
var digest string
60+
if extractedDigest := extractDigestFromImage(cache.Spec.Image); extractedDigest != "" {
61+
clustergkmcacheLog.Info("Image already contains digest (likely from Kyverno)", "image", cache.Spec.Image, "digest", extractedDigest)
62+
digest = extractedDigest
7563
}
7664
resolvedDigest, digestFound := cache.Annotations[utils.GMKCacheAnnotationResolvedDigest]
7765
if digestFound {
@@ -82,24 +70,6 @@ func (w *ClusterGKMCache) Default(ctx context.Context, obj runtime.Object) error
8270
}
8371
cache.Annotations[utils.GMKCacheAnnotationResolvedDigest] = digest
8472

85-
// Bind a mutation signature to THIS AdmissionRequest UID
86-
req, err := admission.RequestFromContext(ctx)
87-
if err != nil {
88-
return apierrors.NewBadRequest("unable to read admission request from context")
89-
}
90-
secret, err := mutationKeyFromEnv()
91-
if err != nil {
92-
return apierrors.NewBadRequest(err.Error())
93-
}
94-
sig, err := signMutation(secret, "", cache.Spec.Image, digest)
95-
if err != nil {
96-
return apierrors.NewBadRequest(fmt.Sprintf("failed to sign mutation: %v", err))
97-
}
98-
cache.Annotations[utils.GMKCacheAnnotationMutationSig] = sig
99-
100-
// Audit for convenience (not part of trust)
101-
cache.Annotations[utils.GMKCacheAnnotationLastMutatedBy] = req.UserInfo.Username
102-
10373
clustergkmcacheLog.Info("added/updated resolvedDigest", "image", cache.Spec.Image, "digest", digest)
10474
return nil
10575
}
@@ -115,36 +85,12 @@ func (w *ClusterGKMCache) ValidateCreate(ctx context.Context, obj runtime.Object
11585
return nil, fmt.Errorf("spec.image must be set")
11686
}
11787

118-
// The validator sees the mutated object.
119-
// If resolvedDigest is present, it must carry a valid mutationSig for THIS request.
120-
digest := cache.Annotations[utils.GMKCacheAnnotationResolvedDigest]
121-
sig := cache.Annotations[utils.GMKCacheAnnotationMutationSig]
122-
123-
if digest != "" {
124-
secret, err := mutationKeyFromEnv()
125-
if err != nil {
126-
return nil, fmt.Errorf("%s", err.Error())
127-
}
128-
if !verifyMutation(secret, "", cache.Spec.Image, digest, sig) {
129-
return nil, fmt.Errorf("%s present but missing/invalid %s; digest must be set only by the mutating webhook",
130-
utils.GMKCacheAnnotationResolvedDigest, utils.GMKCacheAnnotationMutationSig)
131-
}
132-
}
133-
134-
// Defense in depth
135-
// Recompute digest from the image (same logic used by mutator).
136-
// The mutator adds the gkm.io/resolvedDigest annotation
137-
// If we just check it exists then the validator will fail.
138-
// We just recompute the digest and compare it. If it's OK
139-
// we accept the CR object.
140-
digest, err := verifyImageSignature(ctx, cache.Spec.Image)
141-
if err != nil {
142-
return nil, fmt.Errorf("image signature verification failed: %w", err)
88+
if _, exists := cache.Annotations[utils.GMKCacheAnnotationResolvedDigest]; !exists {
89+
return nil, fmt.Errorf("%s must be set by mutating webhook", utils.GMKCacheAnnotationResolvedDigest)
14390
}
14491

145-
ann := cache.Annotations["gkm.io/resolvedDigest"]
146-
if ann == "" || ann != digest {
147-
return nil, fmt.Errorf("gkm.io/resolvedDigest mismatch - this is not the digest of the verified image")
92+
if _, exists := cache.Annotations[utils.KyvernoVerifyImagesAnnotation]; !exists {
93+
return nil, fmt.Errorf("%s must be set by kyverno", utils.KyvernoVerifyImagesAnnotation)
14894
}
14995

15096
return nil, nil
@@ -164,7 +110,6 @@ func (w *ClusterGKMCache) ValidateUpdate(_ context.Context, oldObj, newObj runti
164110

165111
oldDigest := oldCache.Annotations[utils.GMKCacheAnnotationResolvedDigest]
166112
newDigest := newCache.Annotations[utils.GMKCacheAnnotationResolvedDigest]
167-
newSig := newCache.Annotations[utils.GMKCacheAnnotationMutationSig]
168113

169114
// If image didn't change, digest must not change.
170115
if oldImg == newImg {
@@ -178,18 +123,10 @@ func (w *ClusterGKMCache) ValidateUpdate(_ context.Context, oldObj, newObj runti
178123
if newImg == "" {
179124
return nil, fmt.Errorf("spec.image must be set")
180125
}
181-
if newDigest == "" || newSig == "" {
126+
if newDigest == "" {
182127
return nil, fmt.Errorf("%s must be set by mutating webhook when spec.image changes", utils.GMKCacheAnnotationResolvedDigest)
183128
}
184129

185-
secret, err := mutationKeyFromEnv()
186-
if err != nil {
187-
return nil, fmt.Errorf("%s", err.Error())
188-
}
189-
if !verifyMutation(secret, "", newImg, newDigest, newSig) {
190-
return nil, fmt.Errorf("invalid %s for updated image; digest must be set only by the mutating webhook", utils.GMKCacheAnnotationMutationSig)
191-
}
192-
193130
return nil, nil
194131
}
195132

0 commit comments

Comments
 (0)