From d42328fa9124813d8a67b8a01ef199742c2be732 Mon Sep 17 00:00:00 2001 From: Denny Date: Fri, 29 Apr 2022 08:18:37 -0400 Subject: [PATCH] [Cosigned] Add signature pull secrets (#1805) * Add signaturePullSecrets support Signed-off-by: Denny Hoang * Abstract signaturePullSecrets remoteOpts Signed-off-by: Denny Hoang * Add validation and signaturePullSecrets test cases Signed-off-by: Denny Hoang * Test Authorities RemoteOpts count Signed-off-by: Denny Hoang * Comment on not storing in Authority RemoteOpts Signed-off-by: Denny Hoang * Fix lint issue Signed-off-by: Denny Hoang * Add podSpec signaturePullSecrets test Signed-off-by: Denny Hoang * Add valid signaturePullSecrets test Signed-off-by: Denny Hoang * early return err; add signaturePullSecrets comment Signed-off-by: Denny Hoang * codegen update Signed-off-by: Denny Hoang --- config/300-clusterimagepolicy.yaml | 9 ++ pkg/apis/config/image_policies_test.go | 47 +++++++ pkg/apis/config/store_test.go | 3 + .../testdata/config-image-policies.yaml | 20 +++ .../v1alpha1/clusterimagepolicy_types.go | 5 + .../v1alpha1/clusterimagepolicy_validation.go | 12 +- .../clusterimagepolicy_validation_test.go | 48 ++++++- .../v1alpha1/zz_generated.deepcopy.go | 9 +- .../clusterimagepolicy_types.go | 41 +++++- pkg/cosign/kubernetes/webhook/validator.go | 24 ++-- .../kubernetes/webhook/validator_test.go | 122 ++++++++++++++++-- .../clusterimagepolicy_test.go | 30 +++++ 12 files changed, 345 insertions(+), 25 deletions(-) diff --git a/config/300-clusterimagepolicy.yaml b/config/300-clusterimagepolicy.yaml index cd5545806f1..bf4524c41ad 100644 --- a/config/300-clusterimagepolicy.yaml +++ b/config/300-clusterimagepolicy.yaml @@ -139,6 +139,15 @@ spec: properties: oci: type: string + signaturePullSecrets: + description: SignaturePullSecrets is an optional list of references to secrets in the same namespace as the deploying resource for pulling any of the signatures used by this Source. + type: array + items: + type: object + properties: + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string images: type: array items: diff --git a/pkg/apis/config/image_policies_test.go b/pkg/apis/config/image_policies_test.go index 0336bb80afe..3b3eead05a9 100644 --- a/pkg/apis/config/image_policies_test.go +++ b/pkg/apis/config/image_policies_test.go @@ -154,6 +154,37 @@ func TestGetAuthorities(t *testing.T) { if got := c[matchedPolicy].Authorities[0].Attestations[0].Data; got != want { t.Errorf("Did not get what I wanted %q, got %+v", want, got) } + + // Test source oci + matchedPolicy = "cluster-image-policy-source-oci" + c, err = defaults.GetMatchingPolicies("sourceocionly") + checkGetMatches(t, c, err) + if len(c) != 1 { + t.Errorf("Wanted 1 match, got %d", len(c)) + } + + checkSourceOCI(t, c[matchedPolicy].Authorities) + want = "example.registry.com/alternative/signature" + if got := c[matchedPolicy].Authorities[0].Sources[0].OCI; got != want { + t.Errorf("Did not get what I wanted %q, got %+v", want, got) + } + + // Test source signaturePullSecrets + matchedPolicy = "cluster-image-policy-source-oci-signature-pull-secrets" + c, err = defaults.GetMatchingPolicies("sourceocisignaturepullsecrets") + checkGetMatches(t, c, err) + if len(c) != 1 { + t.Errorf("Wanted 1 match, got %d", len(c)) + } + + checkSourceOCI(t, c[matchedPolicy].Authorities) + if got := len(c[matchedPolicy].Authorities[0].Sources[0].SignaturePullSecrets); got != 1 { + t.Errorf("Did not get what I wanted %d, got %d", 1, got) + } + want = "examplePullSecret" + if got := c[matchedPolicy].Authorities[0].Sources[0].SignaturePullSecrets[0].Name; got != want { + t.Errorf("Did not get what I wanted %q, got %+v", want, got) + } } func checkGetMatches(t *testing.T, c map[string]webhookcip.ClusterImagePolicy, err error) { @@ -191,3 +222,19 @@ func checkPublicKey(t *testing.T, gotKey crypto.PublicKey) { t.Errorf("Did not get what I wanted %s, got %s", inlineKeyData, string(pemBytes)) } } + +func checkSourceOCI(t *testing.T, authority []webhookcip.Authority) { + t.Helper() + + if got := len(authority); got != 1 { + t.Errorf("Did not get what I wanted %d, got %d", 1, got) + } + if got := len(authority[0].Sources); got != 1 { + t.Errorf("Did not get what I wanted %d, got %d", 1, got) + } + + want := len(authority[0].Sources) + if got := len(authority[0].RemoteOpts); got != want { + t.Errorf("Did not get what I wanted %d, got %d", want, got) + } +} diff --git a/pkg/apis/config/store_test.go b/pkg/apis/config/store_test.go index 767548920e8..cfb163f7298 100644 --- a/pkg/apis/config/store_test.go +++ b/pkg/apis/config/store_test.go @@ -20,6 +20,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" + "github.com/sigstore/cosign/pkg/oci/remote" "k8s.io/apimachinery/pkg/api/resource" logtesting "knative.dev/pkg/logging/testing" @@ -28,6 +29,8 @@ import ( var ignoreStuff = cmp.Options{ cmpopts.IgnoreUnexported(resource.Quantity{}), + // Ignore functional remote options + cmpopts.IgnoreTypes((remote.Option)(nil)), } func TestStoreLoadWithContext(t *testing.T) { diff --git a/pkg/apis/config/testdata/config-image-policies.yaml b/pkg/apis/config/testdata/config-image-policies.yaml index 23a0d0f6c79..30238e47680 100644 --- a/pkg/apis/config/testdata/config-image-policies.yaml +++ b/pkg/apis/config/testdata/config-image-policies.yaml @@ -105,3 +105,23 @@ data: policy: type: cue data: "cip level cue here" + cluster-image-policy-source-oci: | + images: + - regex: .*sourceocionly.* + authorities: + - name: attestation-0 + key: + data: inlinedata here + source: + - oci: "example.registry.com/alternative/signature" + cluster-image-policy-source-oci-signature-pull-secrets: | + images: + - regex: .*sourceocisignaturepullsecrets.* + authorities: + - name: attestation-0 + key: + data: inlinedata here + source: + - oci: "example.registry.com/alternative/signature" + signaturePullSecrets: + - name: examplePullSecret diff --git a/pkg/apis/cosigned/v1alpha1/clusterimagepolicy_types.go b/pkg/apis/cosigned/v1alpha1/clusterimagepolicy_types.go index bad7c36483b..c4a6329e279 100644 --- a/pkg/apis/cosigned/v1alpha1/clusterimagepolicy_types.go +++ b/pkg/apis/cosigned/v1alpha1/clusterimagepolicy_types.go @@ -115,6 +115,11 @@ type KeyRef struct { type Source struct { // +optional OCI string `json:"oci,omitempty"` + // SignaturePullSecrets is an optional list of references to secrets in the + // same namespace as the deploying resource for pulling any of the signatures + // used by this Source. + // +optional + SignaturePullSecrets []v1.LocalObjectReference `json:"signaturePullSecrets,omitempty"` } // TLog specifies the URL to a transparency log that holds diff --git a/pkg/apis/cosigned/v1alpha1/clusterimagepolicy_validation.go b/pkg/apis/cosigned/v1alpha1/clusterimagepolicy_validation.go index a307f33a834..0f5aff7bea0 100644 --- a/pkg/apis/cosigned/v1alpha1/clusterimagepolicy_validation.go +++ b/pkg/apis/cosigned/v1alpha1/clusterimagepolicy_validation.go @@ -84,8 +84,8 @@ func (authority *Authority) Validate(ctx context.Context) *apis.FieldError { errs = errs.Also(authority.Keyless.Validate(ctx).ViaField("keyless")) } - for _, source := range authority.Sources { - errs = errs.Also(source.Validate(ctx).ViaField("source")) + for i, source := range authority.Sources { + errs = errs.Also(source.Validate(ctx).ViaFieldIndex("source", i)) } for _, att := range authority.Attestations { @@ -144,6 +144,14 @@ func (source *Source) Validate(ctx context.Context) *apis.FieldError { if source.OCI == "" { errs = errs.Also(apis.ErrMissingField("oci")) } + + if len(source.SignaturePullSecrets) > 0 { + for i, secret := range source.SignaturePullSecrets { + if secret.Name == "" { + errs = errs.Also(apis.ErrMissingField("name")).ViaFieldIndex("signaturePullSecrets", i) + } + } + } return errs } diff --git a/pkg/apis/cosigned/v1alpha1/clusterimagepolicy_validation_test.go b/pkg/apis/cosigned/v1alpha1/clusterimagepolicy_validation_test.go index 9848f0a15e5..9610ca2d1ff 100644 --- a/pkg/apis/cosigned/v1alpha1/clusterimagepolicy_validation_test.go +++ b/pkg/apis/cosigned/v1alpha1/clusterimagepolicy_validation_test.go @@ -19,6 +19,7 @@ import ( "testing" "github.com/stretchr/testify/require" + v1 "k8s.io/api/core/v1" "knative.dev/pkg/apis" ) @@ -415,7 +416,7 @@ func TestAuthoritiesValidation(t *testing.T) { { name: "Should fail when source oci is empty", expectErr: true, - errorString: "missing field(s): spec.authorities[0].source.oci", + errorString: "missing field(s): spec.authorities[0].source[0].oci", policy: ClusterImagePolicy{ Spec: ClusterImagePolicySpec{ Images: []ImagePattern{{Regex: ".*"}}, @@ -486,6 +487,51 @@ func TestAuthoritiesValidation(t *testing.T) { }, }, }, + { + name: "Should fail with signaturePullSecret name empty", + expectErr: true, + errorString: "missing field(s): spec.authorities[0].source[0].signaturePullSecrets[0].name", + policy: ClusterImagePolicy{ + Spec: ClusterImagePolicySpec{ + Images: []ImagePattern{{Regex: ".*"}}, + Authorities: []Authority{ + { + Key: &KeyRef{KMS: "kms://key/path"}, + Sources: []Source{ + { + OCI: "registry1", + SignaturePullSecrets: []v1.LocalObjectReference{ + {Name: ""}, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "Should pass with signaturePullSecret name filled", + expectErr: false, + policy: ClusterImagePolicy{ + Spec: ClusterImagePolicySpec{ + Images: []ImagePattern{{Regex: ".*"}}, + Authorities: []Authority{ + { + Key: &KeyRef{KMS: "kms://key/path"}, + Sources: []Source{ + { + OCI: "registry1", + SignaturePullSecrets: []v1.LocalObjectReference{ + {Name: "testPullSecrets"}, + }, + }, + }, + }, + }, + }, + }, + }, } for _, test := range tests { diff --git a/pkg/apis/cosigned/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/cosigned/v1alpha1/zz_generated.deepcopy.go index c6926846691..4bfa48469a3 100644 --- a/pkg/apis/cosigned/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/cosigned/v1alpha1/zz_generated.deepcopy.go @@ -62,7 +62,9 @@ func (in *Authority) DeepCopyInto(out *Authority) { if in.Sources != nil { in, out := &in.Sources, &out.Sources *out = make([]Source, len(*in)) - copy(*out, *in) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } } if in.CTLog != nil { in, out := &in.CTLog, &out.CTLog @@ -311,6 +313,11 @@ func (in *Policy) DeepCopy() *Policy { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Source) DeepCopyInto(out *Source) { *out = *in + if in.SignaturePullSecrets != nil { + in, out := &in.SignaturePullSecrets, &out.SignaturePullSecrets + *out = make([]v1.LocalObjectReference, len(*in)) + copy(*out, *in) + } return } diff --git a/pkg/cosign/kubernetes/webhook/clusterimagepolicy/clusterimagepolicy_types.go b/pkg/cosign/kubernetes/webhook/clusterimagepolicy/clusterimagepolicy_types.go index cbda0c7c0fb..d283519705d 100644 --- a/pkg/cosign/kubernetes/webhook/clusterimagepolicy/clusterimagepolicy_types.go +++ b/pkg/cosign/kubernetes/webhook/clusterimagepolicy/clusterimagepolicy_types.go @@ -15,17 +15,21 @@ package clusterimagepolicy import ( + "context" "crypto" "crypto/x509" "encoding/json" "encoding/pem" + "github.com/google/go-containerregistry/pkg/authn/k8schain" "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/v1/remote" "github.com/pkg/errors" "github.com/sigstore/cosign/pkg/apis/cosigned/v1alpha1" - "github.com/sigstore/cosign/pkg/oci/remote" - + ociremote "github.com/sigstore/cosign/pkg/oci/remote" "knative.dev/pkg/apis" + kubeclient "knative.dev/pkg/client/injection/kube/client" + "knative.dev/pkg/logging" ) // ClusterImagePolicy defines the images that go through verification @@ -58,7 +62,7 @@ type Authority struct { // RemoteOpts are not marshalled because they are an unsupported type // RemoteOpts will be populated by the Authority UnmarshalJSON override // +optional - RemoteOpts []remote.Option `json:"-"` + RemoteOpts []ociremote.Option `json:"-"` // +optional Attestations []AttestationPolicy `json:"attestations,omitempty"` } @@ -139,7 +143,7 @@ func (a *Authority) UnmarshalJSON(data []byte) error { if targetRepoOverride, err := name.NewRepository(source.OCI); err != nil { return errors.Wrap(err, "failed to determine source") } else if (targetRepoOverride != name.Repository{}) { - rawAuthority.RemoteOpts = append(rawAuthority.RemoteOpts, remote.WithTargetRepository(targetRepoOverride)) + rawAuthority.RemoteOpts = append(rawAuthority.RemoteOpts, ociremote.WithTargetRepository(targetRepoOverride)) } } } @@ -149,6 +153,35 @@ func (a *Authority) UnmarshalJSON(data []byte) error { return nil } +// SourceSignaturePullSecretsOpts creates the signaturePullSecrets remoteOpts +// This is not stored in the Authority under RemoteOpts as the namespace can be different +func (a *Authority) SourceSignaturePullSecretsOpts(ctx context.Context, namespace string) ([]ociremote.Option, error) { + var ret []ociremote.Option + for _, source := range a.Sources { + if len(source.SignaturePullSecrets) > 0 { + signaturePullSecrets := make([]string, 0, len(source.SignaturePullSecrets)) + for _, s := range source.SignaturePullSecrets { + signaturePullSecrets = append(signaturePullSecrets, s.Name) + } + + opt := k8schain.Options{ + Namespace: namespace, + ImagePullSecrets: signaturePullSecrets, + } + + kc, err := k8schain.New(ctx, kubeclient.Get(ctx), opt) + if err != nil { + logging.FromContext(ctx).Errorf("failed creating keychain: %+v", err) + return nil, err + } + + ret = append(ret, ociremote.WithRemoteOptions(remote.WithAuthFromKeychain(kc))) + } + } + + return ret, nil +} + func ConvertClusterImagePolicyV1alpha1ToWebhook(in *v1alpha1.ClusterImagePolicy) *ClusterImagePolicy { copyIn := in.DeepCopy() diff --git a/pkg/cosign/kubernetes/webhook/validator.go b/pkg/cosign/kubernetes/webhook/validator.go index 54440b133fd..d1a2454fd1a 100644 --- a/pkg/cosign/kubernetes/webhook/validator.go +++ b/pkg/cosign/kubernetes/webhook/validator.go @@ -76,7 +76,7 @@ func (v *Validator) ValidatePodSpecable(ctx context.Context, wp *duckv1.WithPod) ServiceAccountName: wp.Spec.Template.Spec.ServiceAccountName, ImagePullSecrets: imagePullSecrets, } - return v.validatePodSpec(ctx, &wp.Spec.Template.Spec, opt).ViaField("spec.template.spec") + return v.validatePodSpec(ctx, wp.Namespace, &wp.Spec.Template.Spec, opt).ViaField("spec.template.spec") } // ValidatePod implements duckv1.PodValidator @@ -94,7 +94,7 @@ func (v *Validator) ValidatePod(ctx context.Context, p *duckv1.Pod) *apis.FieldE ServiceAccountName: p.Spec.ServiceAccountName, ImagePullSecrets: imagePullSecrets, } - return v.validatePodSpec(ctx, &p.Spec, opt).ViaField("spec") + return v.validatePodSpec(ctx, p.Namespace, &p.Spec, opt).ViaField("spec") } // ValidateCronJob implements duckv1.CronJobValidator @@ -112,10 +112,10 @@ func (v *Validator) ValidateCronJob(ctx context.Context, c *duckv1.CronJob) *api ServiceAccountName: c.Spec.JobTemplate.Spec.Template.Spec.ServiceAccountName, ImagePullSecrets: imagePullSecrets, } - return v.validatePodSpec(ctx, &c.Spec.JobTemplate.Spec.Template.Spec, opt).ViaField("spec.jobTemplate.spec.template.spec") + return v.validatePodSpec(ctx, c.Namespace, &c.Spec.JobTemplate.Spec.Template.Spec, opt).ViaField("spec.jobTemplate.spec.template.spec") } -func (v *Validator) validatePodSpec(ctx context.Context, ps *corev1.PodSpec, opt k8schain.Options) (errs *apis.FieldError) { +func (v *Validator) validatePodSpec(ctx context.Context, namespace string, ps *corev1.PodSpec, opt k8schain.Options) (errs *apis.FieldError) { kc, err := k8schain.New(ctx, v.client, opt) if err != nil { logging.FromContext(ctx).Warnf("Unable to build k8schain: %v", err) @@ -171,7 +171,7 @@ func (v *Validator) validatePodSpec(ctx context.Context, ps *corev1.PodSpec, opt // If there is at least one policy that matches, that means it // has to be satisfied. if len(policies) > 0 { - signatures, fieldErrors := validatePolicies(ctx, ref, policies, ociremote.WithRemoteOptions(remote.WithAuthFromKeychain(kc))) + signatures, fieldErrors := validatePolicies(ctx, namespace, ref, policies, ociremote.WithRemoteOptions(remote.WithAuthFromKeychain(kc))) if len(signatures) != len(policies) { logging.FromContext(ctx).Warnf("Failed to validate at least one policy for %s", ref.Name()) @@ -235,7 +235,7 @@ func (v *Validator) validatePodSpec(ctx context.Context, ps *corev1.PodSpec, opt // Note that if an image does not match any policies, it's perfectly // reasonable that the return value is 0, nil since there were no errors, but // the image was not validated against any matching policy and hence authority. -func validatePolicies(ctx context.Context, ref name.Reference, policies map[string]webhookcip.ClusterImagePolicy, remoteOpts ...ociremote.Option) (map[string]*PolicyResult, map[string][]error) { +func validatePolicies(ctx context.Context, namespace string, ref name.Reference, policies map[string]webhookcip.ClusterImagePolicy, remoteOpts ...ociremote.Option) (map[string]*PolicyResult, map[string][]error) { type retChannelType struct { name string policyResult *PolicyResult @@ -258,7 +258,7 @@ func validatePolicies(ctx context.Context, ref name.Reference, policies map[stri go func() { result := retChannelType{name: cipName} - result.policyResult, result.errors = ValidatePolicy(ctx, ref, cip, remoteOpts...) + result.policyResult, result.errors = ValidatePolicy(ctx, namespace, ref, cip, remoteOpts...) if len(result.errors) == 0 { // Ok, at least one Authority on the policy passed. If there's a CIP level // policy, apply it against the results of the successful Authorities @@ -314,7 +314,7 @@ func validatePolicies(ctx context.Context, ref name.Reference, policies map[stri // signatures OR attestations if atttestations were specified. // Returns PolicyResult, or errors encountered if none of the authorities // passed. -func ValidatePolicy(ctx context.Context, ref name.Reference, cip webhookcip.ClusterImagePolicy, remoteOpts ...ociremote.Option) (*PolicyResult, []error) { +func ValidatePolicy(ctx context.Context, namespace string, ref name.Reference, cip webhookcip.ClusterImagePolicy, remoteOpts ...ociremote.Option) (*PolicyResult, []error) { // Each gofunc creates and puts one of these into a results channel. // Once each gofunc finishes, we go through the channel and pull out // the results. @@ -335,6 +335,14 @@ func ValidatePolicy(ctx context.Context, ref name.Reference, cip webhookcip.Clus authorityRemoteOpts := remoteOpts authorityRemoteOpts = append(authorityRemoteOpts, authority.RemoteOpts...) + signaturePullSecretsOpts, err := authority.SourceSignaturePullSecretsOpts(ctx, namespace) + if err != nil { + result.err = err + results <- result + return + } + authorityRemoteOpts = append(authorityRemoteOpts, signaturePullSecretsOpts...) + if len(authority.Attestations) > 0 { // We're doing the verify-attestations path, so validate (.att) validatedAttestations, err := ValidatePolicyAttestationsForAuthority(ctx, ref, authority, authorityRemoteOpts...) diff --git a/pkg/cosign/kubernetes/webhook/validator_test.go b/pkg/cosign/kubernetes/webhook/validator_test.go index 4dedd275ac5..183ef8e7bd1 100644 --- a/pkg/cosign/kubernetes/webhook/validator_test.go +++ b/pkg/cosign/kubernetes/webhook/validator_test.go @@ -122,11 +122,23 @@ UoJou2P8sbDxpLiE/v3yLw1/jyOrCPWYHWFXnyyeGlkgSVefG54tNoK7Uw== }) kc := fakekube.Get(ctx) - kc.CoreV1().ServiceAccounts("default").Create(ctx, &corev1.ServiceAccount{ - ObjectMeta: metav1.ObjectMeta{ - Name: "default", - }, - }, metav1.CreateOptions{}) + // Setup service acc and fakeSignaturePullSecrets for "default" and "cosign-system" namespace + for _, ns := range []string{"default", system.Namespace()} { + kc.CoreV1().ServiceAccounts(ns).Create(ctx, &corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: "default", + }, + }, metav1.CreateOptions{}) + + kc.CoreV1().Secrets(ns).Create(ctx, &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "fakeSignaturePullSecrets", + }, + Data: map[string][]byte{ + "dockerconfigjson": []byte(`{"auths":{"https://index.docker.io/v1/":{"username":"username","password":"password","auth":"dXNlcm5hbWU6cGFzc3dvcmQ="}}`), + }, + }, metav1.CreateOptions{}) + } v := NewValidator(ctx, secretName) @@ -468,6 +480,98 @@ UoJou2P8sbDxpLiE/v3yLw1/jyOrCPWYHWFXnyyeGlkgSVefG54tNoK7Uw== return errs }(), cvs: fail, + }, { + name: "simple, error, authority source signaturePullSecrets, non exisiting secret", + ps: &corev1.PodSpec{ + InitContainers: []corev1.Container{{ + Name: "setup-stuff", + Image: digest.String(), + }}, + Containers: []corev1.Container{{ + Name: "user-container", + Image: digest.String(), + }}, + }, + customContext: config.ToContext(ctx, + &config.Config{ + ImagePolicyConfig: &config.ImagePolicyConfig{ + Policies: map[string]webhookcip.ClusterImagePolicy{ + "cluster-image-policy": { + Images: []v1alpha1.ImagePattern{{ + Regex: ".*", + }}, + Authorities: []webhookcip.Authority{ + { + Key: &webhookcip.KeyRef{ + Data: authorityKeyCosignPubString, + PublicKeys: []crypto.PublicKey{authorityKeyCosignPub}, + }, + Sources: []v1alpha1.Source{{ + OCI: "example.com/alternative/signature", + SignaturePullSecrets: []corev1.LocalObjectReference{{ + Name: "non-existing-secret", + }}, + }}, + }, + }, + }, + }, + }, + }, + ), + want: func() *apis.FieldError { + var errs *apis.FieldError + fe := apis.ErrGeneric("failed policy: cluster-image-policy", "image").ViaFieldIndex("initContainers", 0) + fe.Details = fmt.Sprintf("%s secrets \"non-existing-secret\" not found", digest.String()) + errs = errs.Also(fe) + + fe2 := apis.ErrGeneric("failed policy: cluster-image-policy", "image").ViaFieldIndex("containers", 0) + fe2.Details = fmt.Sprintf("%s secrets \"non-existing-secret\" not found", digest.String()) + errs = errs.Also(fe2) + + return errs + }(), + cvs: fail, + }, { + name: "simple, no error, authority source signaturePullSecrets, valid secret", + ps: &corev1.PodSpec{ + InitContainers: []corev1.Container{{ + Name: "setup-stuff", + Image: digest.String(), + }}, + Containers: []corev1.Container{{ + Name: "user-container", + Image: digest.String(), + }}, + }, + customContext: config.ToContext(ctx, + &config.Config{ + ImagePolicyConfig: &config.ImagePolicyConfig{ + Policies: map[string]webhookcip.ClusterImagePolicy{ + "cluster-image-policy": { + Images: []v1alpha1.ImagePattern{{ + Regex: ".*", + }}, + Authorities: []webhookcip.Authority{ + { + Key: &webhookcip.KeyRef{ + Data: authorityKeyCosignPubString, + PublicKeys: []crypto.PublicKey{authorityKeyCosignPub}, + }, + Sources: []v1alpha1.Source{{ + OCI: "example.com/alternative/signature", + SignaturePullSecrets: []corev1.LocalObjectReference{{ + Name: "fakeSignaturePullSecrets", + }}, + }}, + }, + }, + }, + }, + }, + }, + ), + cvs: authorityPublicKeyCVS, }} for _, test := range tests { @@ -480,7 +584,7 @@ UoJou2P8sbDxpLiE/v3yLw1/jyOrCPWYHWFXnyyeGlkgSVefG54tNoK7Uw== } // Check the core mechanics - got := v.validatePodSpec(testContext, test.ps, k8schain.Options{}) + got := v.validatePodSpec(testContext, system.Namespace(), test.ps, k8schain.Options{}) if (got != nil) != (test.want != nil) { t.Errorf("validatePodSpec() = %v, wanted %v", got, test.want) } else if got != nil && got.Error() != test.want.Error() { @@ -1373,7 +1477,7 @@ UoJou2P8sbDxpLiE/v3yLw1/jyOrCPWYHWFXnyyeGlkgSVefG54tNoK7Uw== if test.customContext != nil { testContext = test.customContext } - got, gotErrs := ValidatePolicy(testContext, digest, test.policy) + got, gotErrs := ValidatePolicy(testContext, system.Namespace(), digest, test.policy) validateErrors(t, test.wantErrs, gotErrs) if !reflect.DeepEqual(test.want, got) { t.Errorf("unexpected PolicyResult, want: %+v got: %+v", test.want, got) @@ -1418,7 +1522,7 @@ func TestValidatePolicyCancelled(t *testing.T) { } wantErrs := []string{"context was canceled before validation completed"} cancelFunc() - _, gotErrs := ValidatePolicy(testContext, digest, cip) + _, gotErrs := ValidatePolicy(testContext, system.Namespace(), digest, cip) validateErrors(t, wantErrs, gotErrs) } @@ -1445,6 +1549,6 @@ func TestValidatePoliciesCancelled(t *testing.T) { } wantErrs := []string{"context was canceled before validation completed"} cancelFunc() - _, gotErrs := validatePolicies(testContext, digest, map[string]webhookcip.ClusterImagePolicy{"testcip": cip}) + _, gotErrs := validatePolicies(testContext, system.Namespace(), digest, map[string]webhookcip.ClusterImagePolicy{"testcip": cip}) validateErrors(t, wantErrs, gotErrs["internalerror"]) } diff --git a/pkg/reconciler/clusterimagepolicy/clusterimagepolicy_test.go b/pkg/reconciler/clusterimagepolicy/clusterimagepolicy_test.go index ee6e667be52..95e7ab97a0b 100644 --- a/pkg/reconciler/clusterimagepolicy/clusterimagepolicy_test.go +++ b/pkg/reconciler/clusterimagepolicy/clusterimagepolicy_test.go @@ -83,6 +83,8 @@ RCTfQ5s1kD+hGMSE1rH7s46hmXEeyhnlRnaGF8eMU/SBJE/2NKPnxE7WzQ== // This is the patch for inlined secret for keyless cakey ref data inlinedSecretKeylessPatch = `[{"op":"replace","path":"/data/test-cip-2","value":"{\"images\":[{\"glob\":\"ghcr.io/example/*\"}],\"authorities\":[{\"name\":\"authority-0\",\"keyless\":{\"ca-cert\":{\"data\":\"-----BEGIN PUBLIC KEY-----\\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAExB6+H6054/W1SJgs5JR6AJr6J35J\\nRCTfQ5s1kD+hGMSE1rH7s46hmXEeyhnlRnaGF8eMU/SBJE/2NKPnxE7WzQ==\\n-----END PUBLIC KEY-----\"}}}]}"}]` + + replaceCIPKeySourcePatch = `[{"op":"replace","path":"/data/test-cip","value":"{\"images\":[{\"glob\":\"ghcr.io/example/*\"}],\"authorities\":[{\"name\":\"authority-0\",\"key\":{\"data\":\"-----BEGIN PUBLIC KEY-----\\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAExB6+H6054/W1SJgs5JR6AJr6J35J\\nRCTfQ5s1kD+hGMSE1rH7s46hmXEeyhnlRnaGF8eMU/SBJE/2NKPnxE7WzQ==\\n-----END PUBLIC KEY-----\"},\"source\":[{\"oci\":\"example.com/alternative/signature\",\"signaturePullSecrets\":[{\"name\":\"signaturePullSecretName\"}]}]}]}"}]` ) func TestReconcile(t *testing.T) { @@ -527,6 +529,34 @@ func TestReconcile(t *testing.T) { WantPatches: []clientgotesting.PatchActionImpl{ patchKMS(mainContext, t, fakeKMSKey), }, + }, { + Name: "Key with data, source, and signature pull secrets", + Key: testKey, + + SkipNamespaceValidation: true, // Cluster scoped + Objects: []runtime.Object{ + NewClusterImagePolicy(cipName, + WithFinalizer, + WithImagePattern(v1alpha1.ImagePattern{ + Glob: glob, + }), + WithAuthority(v1alpha1.Authority{ + Key: &v1alpha1.KeyRef{ + Data: validPublicKeyData, + }, + Sources: []v1alpha1.Source{{ + OCI: "example.com/alternative/signature", + SignaturePullSecrets: []corev1.LocalObjectReference{ + {Name: "signaturePullSecretName"}, + }, + }}, + }), + ), + makeConfigMap(), + }, + WantPatches: []clientgotesting.PatchActionImpl{ + makePatch(replaceCIPKeySourcePatch), + }, }, {}} logger := logtesting.TestLogger(t)