1
0
Fork 0
mirror of https://github.com/kyverno/kyverno.git synced 2024-12-14 11:57:48 +00:00

Verify digest (#3679)

* add verifyDigest to check all tags are converted to digests

Signed-off-by: Jim Bugwadia <jim@nirmata.com>

* add required to check for image verification annotation

Signed-off-by: Jim Bugwadia <jim@nirmata.com>

* make fmt

Signed-off-by: Jim Bugwadia <jim@nirmata.com>

* generate CRD

Signed-off-by: Jim Bugwadia <jim@nirmata.com>

* adding imageverify true/false patch

Signed-off-by: anushkamittal20 <anumittal4641@gmail.com>

* patch addition logic

Signed-off-by: anushkamittal20 <anumittal4641@gmail.com>

* image verify CLI tests

Signed-off-by: Jim Bugwadia <jim@nirmata.com>

* fix tests

Signed-off-by: Jim Bugwadia <jim@nirmata.com>

* fixes and unit tests

Signed-off-by: Jim Bugwadia <jim@nirmata.com>

* fix digest mutate

Signed-off-by: Jim Bugwadia <jim@nirmata.com>

* fmt

Signed-off-by: Jim Bugwadia <jim@nirmata.com>

* make codegen

Signed-off-by: Jim Bugwadia <jim@nirmata.com>

* fix policy cache

Signed-off-by: Jim Bugwadia <jim@nirmata.com>

Co-authored-by: anushkamittal20 <anumittal4641@gmail.com>
This commit is contained in:
Jim Bugwadia 2022-04-27 08:09:52 -07:00 committed by GitHub
parent b689f1f15c
commit ab5171cee5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 783 additions and 198 deletions

View file

@ -66,11 +66,21 @@ type ImageVerification struct {
// The repository can also be overridden per Attestor or Attestation.
Repository string `json:"repository,omitempty" yaml:"repository,omitempty"`
// MutateDigest is an optional field which handles the tag-to-digest mutation for the provided image paths.
// MutateDigest enables replacement of image tags with digests.
// Defaults to true.
// +kubebuilder:default=true
// +kubebuilder:validation:Optional
MutateDigest *bool `json:"mutateDigest,omitempty" yaml:"mutateDigest,omitempty"`
// +kubebuilder:validation:Required
MutateDigest bool `json:"mutateDigest,omitempty" yaml:"mutateDigest,omitempty"`
// VerifyDigest validates that images have a digest.
// +kubebuilder:default=true
// +kubebuilder:validation:Required
VerifyDigest bool `json:"verifyDigest,omitempty" yaml:"verifyDigest,omitempty"`
// Required validates that images are verified i.e. have matched passed a signature or attestation check.
// +kubebuilder:default=true
// +kubebuilder:validation:Required
Required bool `json:"required,omitempty" yaml:"required,omitempty"`
}
type AttestorSet struct {

View file

@ -216,7 +216,7 @@ func Test_doesMatchExcludeConflict(t *testing.T) {
var rule Rule
err := json.Unmarshal(testcase.rule, &rule)
assert.NilError(t, err)
errs := rule.ValidateMathExcludeConflict(path)
errs := rule.ValidateMatchExcludeConflict(path)
var expectedErrs field.ErrorList
if testcase.errors != nil {
expectedErrs = testcase.errors(&rule)

View file

@ -77,6 +77,17 @@ func (r *Rule) HasVerifyImages() bool {
return r.VerifyImages != nil && !reflect.DeepEqual(r.VerifyImages, ImageVerification{})
}
// HasImagesValidationChecks checks whether the verifyImages rule has validation checks
func (r *Rule) HasImagesValidationChecks() bool {
for _, v := range r.VerifyImages {
if v.VerifyDigest || v.Required {
return true
}
}
return false
}
// HasValidate checks for validate rule
func (r *Rule) HasValidate() bool {
return !reflect.DeepEqual(r.Validation, Validation{})
@ -87,7 +98,7 @@ func (r *Rule) HasGenerate() bool {
return !reflect.DeepEqual(r.Generation, Generation{})
}
// IsMutatingExisting checks if the mutate rule applies to existing resources
// IsMutateExisting checks if the mutate rule applies to existing resources
func (r *Rule) IsMutateExisting() bool {
return r.Mutation.Targets != nil
}
@ -135,8 +146,8 @@ func (r *Rule) ValidateRuleType(path *field.Path) (errs field.ErrorList) {
return errs
}
// ValidateMathExcludeConflict checks if the resultant of match and exclude block is not an empty set
func (r *Rule) ValidateMathExcludeConflict(path *field.Path) (errs field.ErrorList) {
// ValidateMatchExcludeConflict checks if the resultant of match and exclude block is not an empty set
func (r *Rule) ValidateMatchExcludeConflict(path *field.Path) (errs field.ErrorList) {
if len(r.ExcludeResources.All) > 0 || len(r.MatchResources.All) > 0 {
return errs
}
@ -303,7 +314,7 @@ func (r *Rule) ValidateMathExcludeConflict(path *field.Path) (errs field.ErrorLi
// Validate implements programmatic validation
func (r *Rule) Validate(path *field.Path, namespaced bool, clusterResources sets.String) (errs field.ErrorList) {
errs = append(errs, r.ValidateRuleType(path)...)
errs = append(errs, r.ValidateMathExcludeConflict(path)...)
errs = append(errs, r.ValidateMatchExcludeConflict(path)...)
errs = append(errs, r.MatchResources.Validate(path.Child("match"), namespaced, clusterResources)...)
errs = append(errs, r.ExcludeResources.Validate(path.Child("exclude"), namespaced, clusterResources)...)
return errs

View file

@ -112,7 +112,18 @@ func (s *Spec) HasGenerate() bool {
return false
}
// HasVerifyImages checks for image verification rule types
// HasImagesValidationChecks checks for image verification rules invoked during resource validation
func (s *Spec) HasImagesValidationChecks() bool {
for _, rule := range s.Rules {
if rule.HasImagesValidationChecks() {
return true
}
}
return false
}
// HasVerifyImages checks for image verification rules invoked during resource mutation
func (s *Spec) HasVerifyImages() bool {
for _, rule := range s.Rules {
if rule.HasVerifyImages() {

View file

@ -667,11 +667,6 @@ func (in *ImageVerification) DeepCopyInto(out *ImageVerification) {
(*out)[key] = val
}
}
if in.MutateDigest != nil {
in, out := &in.MutateDigest, &out.MutateDigest
*out = new(bool)
**out = **in
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ImageVerification.

View file

@ -1478,17 +1478,25 @@ spec:
type: string
mutateDigest:
default: true
description: MutateDigest is an optional field which handles the tag-to-digest mutation for the provided image paths. Defaults to true.
description: MutateDigest enables replacement of image tags with digests. Defaults to true.
type: boolean
repository:
description: Repository is an optional alternate OCI repository to use for image signatures and attestations that match this rule. If specified Repository will override the default OCI image repository configured for the installation. The repository can also be overridden per Attestor or Attestation.
type: string
required:
default: true
description: Required validates that images are verified i.e. have matched passed a signature or attestation check.
type: boolean
roots:
description: Roots is the PEM encoded Root certificate chain used for keyless signing Deprecated. Use KeylessAttestor instead.
type: string
subject:
description: Subject is the identity used for keyless signing, for example an email address Deprecated. Use KeylessAttestor instead.
type: string
verifyDigest:
default: true
description: VerifyDigest validates that images have a digest.
type: boolean
type: object
type: array
type: object
@ -3006,17 +3014,25 @@ spec:
type: string
mutateDigest:
default: true
description: MutateDigest is an optional field which handles the tag-to-digest mutation for the provided image paths. Defaults to true.
description: MutateDigest enables replacement of image tags with digests. Defaults to true.
type: boolean
repository:
description: Repository is an optional alternate OCI repository to use for image signatures and attestations that match this rule. If specified Repository will override the default OCI image repository configured for the installation. The repository can also be overridden per Attestor or Attestation.
type: string
required:
default: true
description: Required validates that images are verified i.e. have matched passed a signature or attestation check.
type: boolean
roots:
description: Roots is the PEM encoded Root certificate chain used for keyless signing Deprecated. Use KeylessAttestor instead.
type: string
subject:
description: Subject is the identity used for keyless signing, for example an email address Deprecated. Use KeylessAttestor instead.
type: string
verifyDigest:
default: true
description: VerifyDigest validates that images have a digest.
type: boolean
type: object
type: array
type: object
@ -5245,17 +5261,25 @@ spec:
type: string
mutateDigest:
default: true
description: MutateDigest is an optional field which handles the tag-to-digest mutation for the provided image paths. Defaults to true.
description: MutateDigest enables replacement of image tags with digests. Defaults to true.
type: boolean
repository:
description: Repository is an optional alternate OCI repository to use for image signatures and attestations that match this rule. If specified Repository will override the default OCI image repository configured for the installation. The repository can also be overridden per Attestor or Attestation.
type: string
required:
default: true
description: Required validates that images are verified i.e. have matched passed a signature or attestation check.
type: boolean
roots:
description: Roots is the PEM encoded Root certificate chain used for keyless signing Deprecated. Use KeylessAttestor instead.
type: string
subject:
description: Subject is the identity used for keyless signing, for example an email address Deprecated. Use KeylessAttestor instead.
type: string
verifyDigest:
default: true
description: VerifyDigest validates that images have a digest.
type: boolean
type: object
type: array
type: object
@ -6773,17 +6797,25 @@ spec:
type: string
mutateDigest:
default: true
description: MutateDigest is an optional field which handles the tag-to-digest mutation for the provided image paths. Defaults to true.
description: MutateDigest enables replacement of image tags with digests. Defaults to true.
type: boolean
repository:
description: Repository is an optional alternate OCI repository to use for image signatures and attestations that match this rule. If specified Repository will override the default OCI image repository configured for the installation. The repository can also be overridden per Attestor or Attestation.
type: string
required:
default: true
description: Required validates that images are verified i.e. have matched passed a signature or attestation check.
type: boolean
roots:
description: Roots is the PEM encoded Root certificate chain used for keyless signing Deprecated. Use KeylessAttestor instead.
type: string
subject:
description: Subject is the identity used for keyless signing, for example an email address Deprecated. Use KeylessAttestor instead.
type: string
verifyDigest:
default: true
description: VerifyDigest validates that images have a digest.
type: boolean
type: object
type: array
type: object

View file

@ -601,7 +601,7 @@ func buildPolicyResults(engineResponses []*response.EngineResponse, testResults
for _, info := range infos {
for _, infoResult := range info.Results {
for _, rule := range infoResult.Rules {
if rule.Type != string(response.Validation) {
if rule.Type != string(response.Validation) && rule.Type != string(response.ImageVerify) {
continue
}

View file

@ -471,7 +471,15 @@ OuterLoop:
log.Log.Error(err, "failed to add image variables to context")
}
mutateResponse := engine.Mutate(&engine.PolicyContext{Policy: policy, NewResource: *updatedResource, JSONContext: ctx, NamespaceLabels: namespaceLabels})
policyContext := &engine.PolicyContext{
Policy: policy,
NewResource: *updatedResource,
JSONContext: ctx,
NamespaceLabels: namespaceLabels,
AdmissionInfo: userInfo,
}
mutateResponse := engine.Mutate(policyContext)
if mutateResponse != nil {
engineResponses = append(engineResponses, mutateResponse)
}
@ -493,21 +501,29 @@ OuterLoop:
}
}
verifyImageResponse := engine.VerifyAndPatchImages(policyContext)
if verifyImageResponse != nil && !verifyImageResponse.IsEmpty() {
engineResponses = append(engineResponses, verifyImageResponse)
updateResultCounts(policy, verifyImageResponse, resPath, rc)
}
var policyHasValidate bool
for _, rule := range autogen.ComputeRules(policy) {
if rule.HasValidate() {
if rule.HasValidate() || rule.HasImagesValidationChecks() {
policyHasValidate = true
}
}
policyContext.NewResource = mutateResponse.PatchedResource
var info policyreport.Info
var validateResponse *response.EngineResponse
if policyHasValidate {
policyCtx := &engine.PolicyContext{Policy: policy, NewResource: mutateResponse.PatchedResource, JSONContext: ctx, NamespaceLabels: namespaceLabels, AdmissionInfo: userInfo}
validateResponse = engine.Validate(policyCtx)
validateResponse = engine.Validate(policyContext)
info = ProcessValidateEngineResponse(policy, validateResponse, resPath, rc, policyReport)
}
if validateResponse != nil {
if validateResponse != nil && !validateResponse.IsEmpty() {
engineResponses = append(engineResponses, validateResponse)
}
@ -530,10 +546,10 @@ OuterLoop:
NamespaceLabels: namespaceLabels,
}
generateResponse := engine.ApplyBackgroundChecks(policyContext)
if generateResponse != nil {
if generateResponse != nil && !generateResponse.IsEmpty() {
engineResponses = append(engineResponses, generateResponse)
}
processGenerateEngineResponse(policy, generateResponse, resPath, rc)
updateResultCounts(policy, generateResponse, resPath, rc)
}
return engineResponses, info, nil
@ -693,7 +709,7 @@ func ProcessValidateEngineResponse(policy v1.PolicyInterface, validateResponse *
printCount := 0
for _, policyRule := range autogen.ComputeRules(policy) {
ruleFoundInEngineResponse := false
if !policyRule.HasValidate() {
if !policyRule.HasValidate() && !policyRule.HasImagesValidationChecks() {
continue
}
@ -770,26 +786,27 @@ func buildPVInfo(er *response.EngineResponse, violatedRules []v1.ViolatedRule) p
return info
}
func processGenerateEngineResponse(policy v1.PolicyInterface, generateResponse *response.EngineResponse, resPath string, rc *ResultCounts) {
func updateResultCounts(policy v1.PolicyInterface, engineResponse *response.EngineResponse, resPath string, rc *ResultCounts) {
printCount := 0
for _, policyRule := range autogen.ComputeRules(policy) {
ruleFoundInEngineResponse := false
for i, genResponseRule := range generateResponse.PolicyResponse.Rules {
if policyRule.Name == genResponseRule.Name {
for i, ruleResponse := range engineResponse.PolicyResponse.Rules {
if policyRule.Name == ruleResponse.Name {
ruleFoundInEngineResponse = true
if genResponseRule.Status == response.RuleStatusPass {
if ruleResponse.Status == response.RuleStatusPass {
rc.Pass++
} else {
if printCount < 1 {
fmt.Println("\ngenerate resource is not valid", "policy", policy.GetName(), "resource", resPath)
fmt.Println("\ninvalid resource", "policy", policy.GetName(), "resource", resPath)
printCount++
}
fmt.Printf("%d. %s - %s\n", i+1, genResponseRule.Name, genResponseRule.Message)
fmt.Printf("%d. %s - %s\n", i+1, ruleResponse.Name, ruleResponse.Message)
rc.Fail++
}
continue
}
}
if !ruleFoundInEngineResponse {
rc.Skip++
}

View file

@ -2369,9 +2369,8 @@ spec:
type: string
mutateDigest:
default: true
description: MutateDigest is an optional field which handles
the tag-to-digest mutation for the provided image paths.
Defaults to true.
description: MutateDigest enables replacement of image
tags with digests. Defaults to true.
type: boolean
repository:
description: Repository is an optional alternate OCI repository
@ -2381,6 +2380,12 @@ spec:
The repository can also be overridden per Attestor or
Attestation.
type: string
required:
default: true
description: Required validates that images are verified
i.e. have matched passed a signature or attestation
check.
type: boolean
roots:
description: Roots is the PEM encoded Root certificate
chain used for keyless signing Deprecated. Use KeylessAttestor
@ -2391,6 +2396,11 @@ spec:
signing, for example an email address Deprecated. Use
KeylessAttestor instead.
type: string
verifyDigest:
default: true
description: VerifyDigest validates that images have a
digest.
type: boolean
type: object
type: array
type: object
@ -4834,9 +4844,8 @@ spec:
type: string
mutateDigest:
default: true
description: MutateDigest is an optional field which handles
the tag-to-digest mutation for the provided image paths.
Defaults to true.
description: MutateDigest enables replacement of image
tags with digests. Defaults to true.
type: boolean
repository:
description: Repository is an optional alternate OCI repository
@ -4846,6 +4855,12 @@ spec:
The repository can also be overridden per Attestor or
Attestation.
type: string
required:
default: true
description: Required validates that images are verified
i.e. have matched passed a signature or attestation
check.
type: boolean
roots:
description: Roots is the PEM encoded Root certificate
chain used for keyless signing Deprecated. Use KeylessAttestor
@ -4856,6 +4871,11 @@ spec:
signing, for example an email address Deprecated. Use
KeylessAttestor instead.
type: string
verifyDigest:
default: true
description: VerifyDigest validates that images have a
digest.
type: boolean
type: object
type: array
type: object

View file

@ -2370,9 +2370,8 @@ spec:
type: string
mutateDigest:
default: true
description: MutateDigest is an optional field which handles
the tag-to-digest mutation for the provided image paths.
Defaults to true.
description: MutateDigest enables replacement of image
tags with digests. Defaults to true.
type: boolean
repository:
description: Repository is an optional alternate OCI repository
@ -2382,6 +2381,12 @@ spec:
The repository can also be overridden per Attestor or
Attestation.
type: string
required:
default: true
description: Required validates that images are verified
i.e. have matched passed a signature or attestation
check.
type: boolean
roots:
description: Roots is the PEM encoded Root certificate
chain used for keyless signing Deprecated. Use KeylessAttestor
@ -2392,6 +2397,11 @@ spec:
signing, for example an email address Deprecated. Use
KeylessAttestor instead.
type: string
verifyDigest:
default: true
description: VerifyDigest validates that images have a
digest.
type: boolean
type: object
type: array
type: object
@ -4836,9 +4846,8 @@ spec:
type: string
mutateDigest:
default: true
description: MutateDigest is an optional field which handles
the tag-to-digest mutation for the provided image paths.
Defaults to true.
description: MutateDigest enables replacement of image
tags with digests. Defaults to true.
type: boolean
repository:
description: Repository is an optional alternate OCI repository
@ -4848,6 +4857,12 @@ spec:
The repository can also be overridden per Attestor or
Attestation.
type: string
required:
default: true
description: Required validates that images are verified
i.e. have matched passed a signature or attestation
check.
type: boolean
roots:
description: Roots is the PEM encoded Root certificate
chain used for keyless signing Deprecated. Use KeylessAttestor
@ -4858,6 +4873,11 @@ spec:
signing, for example an email address Deprecated. Use
KeylessAttestor instead.
type: string
verifyDigest:
default: true
description: VerifyDigest validates that images have a
digest.
type: boolean
type: object
type: array
type: object

View file

@ -2386,9 +2386,8 @@ spec:
type: string
mutateDigest:
default: true
description: MutateDigest is an optional field which handles
the tag-to-digest mutation for the provided image paths.
Defaults to true.
description: MutateDigest enables replacement of image
tags with digests. Defaults to true.
type: boolean
repository:
description: Repository is an optional alternate OCI repository
@ -2398,6 +2397,12 @@ spec:
The repository can also be overridden per Attestor or
Attestation.
type: string
required:
default: true
description: Required validates that images are verified
i.e. have matched passed a signature or attestation
check.
type: boolean
roots:
description: Roots is the PEM encoded Root certificate
chain used for keyless signing Deprecated. Use KeylessAttestor
@ -2408,6 +2413,11 @@ spec:
signing, for example an email address Deprecated. Use
KeylessAttestor instead.
type: string
verifyDigest:
default: true
description: VerifyDigest validates that images have a
digest.
type: boolean
type: object
type: array
type: object
@ -4851,9 +4861,8 @@ spec:
type: string
mutateDigest:
default: true
description: MutateDigest is an optional field which handles
the tag-to-digest mutation for the provided image paths.
Defaults to true.
description: MutateDigest enables replacement of image
tags with digests. Defaults to true.
type: boolean
repository:
description: Repository is an optional alternate OCI repository
@ -4863,6 +4872,12 @@ spec:
The repository can also be overridden per Attestor or
Attestation.
type: string
required:
default: true
description: Required validates that images are verified
i.e. have matched passed a signature or attestation
check.
type: boolean
roots:
description: Roots is the PEM encoded Root certificate
chain used for keyless signing Deprecated. Use KeylessAttestor
@ -4873,6 +4888,11 @@ spec:
signing, for example an email address Deprecated. Use
KeylessAttestor instead.
type: string
verifyDigest:
default: true
description: VerifyDigest validates that images have a
digest.
type: boolean
type: object
type: array
type: object
@ -8206,9 +8226,8 @@ spec:
type: string
mutateDigest:
default: true
description: MutateDigest is an optional field which handles
the tag-to-digest mutation for the provided image paths.
Defaults to true.
description: MutateDigest enables replacement of image
tags with digests. Defaults to true.
type: boolean
repository:
description: Repository is an optional alternate OCI repository
@ -8218,6 +8237,12 @@ spec:
The repository can also be overridden per Attestor or
Attestation.
type: string
required:
default: true
description: Required validates that images are verified
i.e. have matched passed a signature or attestation
check.
type: boolean
roots:
description: Roots is the PEM encoded Root certificate
chain used for keyless signing Deprecated. Use KeylessAttestor
@ -8228,6 +8253,11 @@ spec:
signing, for example an email address Deprecated. Use
KeylessAttestor instead.
type: string
verifyDigest:
default: true
description: VerifyDigest validates that images have a
digest.
type: boolean
type: object
type: array
type: object
@ -10672,9 +10702,8 @@ spec:
type: string
mutateDigest:
default: true
description: MutateDigest is an optional field which handles
the tag-to-digest mutation for the provided image paths.
Defaults to true.
description: MutateDigest enables replacement of image
tags with digests. Defaults to true.
type: boolean
repository:
description: Repository is an optional alternate OCI repository
@ -10684,6 +10713,12 @@ spec:
The repository can also be overridden per Attestor or
Attestation.
type: string
required:
default: true
description: Required validates that images are verified
i.e. have matched passed a signature or attestation
check.
type: boolean
roots:
description: Roots is the PEM encoded Root certificate
chain used for keyless signing Deprecated. Use KeylessAttestor
@ -10694,6 +10729,11 @@ spec:
signing, for example an email address Deprecated. Use
KeylessAttestor instead.
type: string
verifyDigest:
default: true
description: VerifyDigest validates that images have a
digest.
type: boolean
type: object
type: array
type: object

View file

@ -2375,9 +2375,8 @@ spec:
type: string
mutateDigest:
default: true
description: MutateDigest is an optional field which handles
the tag-to-digest mutation for the provided image paths.
Defaults to true.
description: MutateDigest enables replacement of image
tags with digests. Defaults to true.
type: boolean
repository:
description: Repository is an optional alternate OCI repository
@ -2387,6 +2386,12 @@ spec:
The repository can also be overridden per Attestor or
Attestation.
type: string
required:
default: true
description: Required validates that images are verified
i.e. have matched passed a signature or attestation
check.
type: boolean
roots:
description: Roots is the PEM encoded Root certificate
chain used for keyless signing Deprecated. Use KeylessAttestor
@ -2397,6 +2402,11 @@ spec:
signing, for example an email address Deprecated. Use
KeylessAttestor instead.
type: string
verifyDigest:
default: true
description: VerifyDigest validates that images have a
digest.
type: boolean
type: object
type: array
type: object
@ -4840,9 +4850,8 @@ spec:
type: string
mutateDigest:
default: true
description: MutateDigest is an optional field which handles
the tag-to-digest mutation for the provided image paths.
Defaults to true.
description: MutateDigest enables replacement of image
tags with digests. Defaults to true.
type: boolean
repository:
description: Repository is an optional alternate OCI repository
@ -4852,6 +4861,12 @@ spec:
The repository can also be overridden per Attestor or
Attestation.
type: string
required:
default: true
description: Required validates that images are verified
i.e. have matched passed a signature or attestation
check.
type: boolean
roots:
description: Roots is the PEM encoded Root certificate
chain used for keyless signing Deprecated. Use KeylessAttestor
@ -4862,6 +4877,11 @@ spec:
signing, for example an email address Deprecated. Use
KeylessAttestor instead.
type: string
verifyDigest:
default: true
description: VerifyDigest validates that images have a
digest.
type: boolean
type: object
type: array
type: object
@ -8171,9 +8191,8 @@ spec:
type: string
mutateDigest:
default: true
description: MutateDigest is an optional field which handles
the tag-to-digest mutation for the provided image paths.
Defaults to true.
description: MutateDigest enables replacement of image
tags with digests. Defaults to true.
type: boolean
repository:
description: Repository is an optional alternate OCI repository
@ -8183,6 +8202,12 @@ spec:
The repository can also be overridden per Attestor or
Attestation.
type: string
required:
default: true
description: Required validates that images are verified
i.e. have matched passed a signature or attestation
check.
type: boolean
roots:
description: Roots is the PEM encoded Root certificate
chain used for keyless signing Deprecated. Use KeylessAttestor
@ -8193,6 +8218,11 @@ spec:
signing, for example an email address Deprecated. Use
KeylessAttestor instead.
type: string
verifyDigest:
default: true
description: VerifyDigest validates that images have a
digest.
type: boolean
type: object
type: array
type: object
@ -10637,9 +10667,8 @@ spec:
type: string
mutateDigest:
default: true
description: MutateDigest is an optional field which handles
the tag-to-digest mutation for the provided image paths.
Defaults to true.
description: MutateDigest enables replacement of image
tags with digests. Defaults to true.
type: boolean
repository:
description: Repository is an optional alternate OCI repository
@ -10649,6 +10678,12 @@ spec:
The repository can also be overridden per Attestor or
Attestation.
type: string
required:
default: true
description: Required validates that images are verified
i.e. have matched passed a signature or attestation
check.
type: boolean
roots:
description: Roots is the PEM encoded Root certificate
chain used for keyless signing Deprecated. Use KeylessAttestor
@ -10659,6 +10694,11 @@ spec:
signing, for example an email address Deprecated. Use
KeylessAttestor instead.
type: string
verifyDigest:
default: true
description: VerifyDigest validates that images have a
digest.
type: boolean
type: object
type: array
type: object

View file

@ -1632,10 +1632,32 @@ bool
</em>
</td>
<td>
<p>MutateDigest is an optional field which handles the tag-to-digest mutation for the provided image paths.
<p>MutateDigest enables replacement of image tags with digests.
Defaults to true.</p>
</td>
</tr>
<tr>
<td>
<code>verifyDigest</code></br>
<em>
bool
</em>
</td>
<td>
<p>VerifyDigest validates that images have a digest.</p>
</td>
</tr>
<tr>
<td>
<code>required</code></br>
<em>
bool
</em>
</td>
<td>
<p>Required validates that images are verified i.e. have matched passed a signature or attestation check.</p>
</td>
</tr>
</tbody>
</table>
<hr />

View file

@ -3,9 +3,13 @@ package engine
import (
"encoding/json"
"fmt"
"reflect"
"strconv"
"strings"
"time"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"github.com/go-logr/logr"
"github.com/google/go-containerregistry/pkg/name"
"github.com/google/go-containerregistry/pkg/v1/remote"
@ -23,8 +27,8 @@ import (
"sigs.k8s.io/controller-runtime/pkg/log"
)
func VerifyAndPatchImages(policyContext *PolicyContext) (resp *response.EngineResponse) {
resp = &response.EngineResponse{}
func VerifyAndPatchImages(policyContext *PolicyContext) *response.EngineResponse {
resp := &response.EngineResponse{}
images := policyContext.JSONContext.ImageInfo()
policy := policyContext.Policy
@ -98,7 +102,7 @@ func VerifyAndPatchImages(policyContext *PolicyContext) (resp *response.EngineRe
}
}
return
return resp
}
func appendError(resp *response.EngineResponse, rule *v1.Rule, msg string, status response.RuleStatus) {
@ -142,57 +146,130 @@ func (iv *imageVerifier) verify(imageVerify *v1.ImageVerification, images map[st
for _, infoMap := range images {
for _, imageInfo := range infoMap {
path := imageInfo.Pointer
image := imageInfo.String()
jmespath := engineUtils.JsonPointerToJMESPath(path)
changed, err := iv.policyContext.JSONContext.HasChanged(jmespath)
if err == nil && !changed {
iv.logger.V(4).Info("no change in image, skipping check", "image", image)
continue
}
if !imageMatches(image, imageVerify.ImageReferences) {
iv.logger.V(4).Info("image does not match pattern", "image", image, "patterns", imageVerify.ImageReferences)
continue
}
if imageVerify.MutateDigest == nil {
mutate := true
imageVerify.MutateDigest = &mutate
jmespath := engineUtils.JsonPointerToJMESPath(imageInfo.Pointer)
changed, err := iv.policyContext.JSONContext.HasChanged(jmespath)
if err == nil && !changed {
iv.logger.V(4).Info("no change in image, skipping check", "image", image)
continue
}
var ruleResp *response.RuleResponse
if len(imageVerify.Attestations) == 0 {
var digest string
ruleResp, digest = iv.verifySignatures(imageVerify, imageInfo)
if imageInfo.Digest == "" && *imageVerify.MutateDigest && ruleResp.Status == response.RuleStatusPass {
err := iv.patchDigest(path, imageInfo, digest, ruleResp)
if err != nil {
ruleResp = ruleResponse(*iv.rule, response.ImageVerify, err.Error(), response.RuleStatusFail, nil)
}
}
} else {
ruleResp = iv.attestImage(imageVerify, imageInfo)
if imageInfo.Digest == "" && *imageVerify.MutateDigest && ruleResp.Status == response.RuleStatusPass {
digest, err := fetchImageDigest(imageInfo.String())
if err != nil {
msg := fmt.Sprintf("fetching image digest from registry error: %s", err)
ruleResp = ruleResponse(*iv.rule, response.ImageVerify, msg, response.RuleStatusFail, nil)
} else {
err = iv.patchDigest(path, imageInfo, digest, ruleResp)
if err != nil {
ruleResp = ruleResponse(*iv.rule, response.ImageVerify, err.Error(), response.RuleStatusFail, nil)
}
}
var digest string
if len(imageVerify.Attestors) > 0 {
if len(imageVerify.Attestations) > 0 {
ruleResp = iv.verifyAttestations(imageVerify, imageInfo)
} else {
ruleResp, digest = iv.verifySignatures(imageVerify, imageInfo)
}
}
iv.resp.PolicyResponse.Rules = append(iv.resp.PolicyResponse.Rules, *ruleResp)
incrementAppliedCount(iv.resp)
if imageVerify.MutateDigest && imageInfo.Digest != "" {
patch, err := iv.handleDigest(digest, imageInfo)
if err != nil {
ruleResp = ruleError(iv.rule, response.ImageVerify, "failed to update digest", err)
}
if ruleResp != nil {
ruleResp.Patches = append(ruleResp.Patches, patch)
}
}
if ruleResp != nil {
if ruleResp.Status == response.RuleStatusPass {
ruleResp = iv.markImageVerified(imageVerify, ruleResp, digest, imageInfo)
}
iv.resp.PolicyResponse.Rules = append(iv.resp.PolicyResponse.Rules, *ruleResp)
incrementAppliedCount(iv.resp)
}
}
}
}
func (iv *imageVerifier) handleDigest(digest string, imageInfo kubeutils.ImageInfo) ([]byte, error) {
if digest == "" {
digest, err := fetchImageDigest(imageInfo.String())
if err != nil {
return nil, err
}
imageInfo.Digest = digest
}
patch, err := makeAddDigestPatch(imageInfo, digest)
if err != nil {
return nil, err
}
return patch, nil
}
func (iv *imageVerifier) markImageVerified(imageVerify *v1.ImageVerification, ruleResp *response.RuleResponse, digest string, imageInfo kubeutils.ImageInfo) *response.RuleResponse {
if hasImageVerifiedAnnotationChanged(iv.policyContext, imageInfo.Name, digest) {
msg := "changes to `images.kyverno.io` annotation are not allowed"
return ruleResponse(*iv.rule, response.ImageVerify, msg, response.RuleStatusFail, nil)
}
if imageVerify.Required {
isImageVerified := ruleResp.Status == response.RuleStatusPass
patch, err := makeImageVerifiedPatch(imageInfo, digest, isImageVerified)
if err == nil {
ruleResp.Patches = [][]byte{patch}
} else {
iv.logger.Error(err, "failed to create patch", "image", imageInfo.String())
}
}
return ruleResp
}
func hasImageVerifiedAnnotationChanged(ctx *PolicyContext, name, digest string) bool {
if reflect.DeepEqual(ctx.OldResource, &unstructured.Unstructured{}) ||
reflect.DeepEqual(ctx.NewResource, &unstructured.Unstructured{}) {
return false
}
key := makeAnnotationKey(name, digest)
newValue := ctx.NewResource.GetAnnotations()[key]
oldValue := ctx.OldResource.GetAnnotations()[key]
return newValue != oldValue
}
func makeImageVerifiedPatch(imageInfo kubeutils.ImageInfo, digest string, verified bool) ([]byte, error) {
var patch = make(map[string]interface{})
annotationKey := makeAnnotationKeyForJSONPatch(imageInfo.Name, digest)
patch["op"] = "add"
patch["path"] = annotationKey
patch["value"] = strconv.FormatBool(verified)
return json.Marshal(patch)
}
func makeAnnotationKeyForJMESPath(imageName, imageDigest string) string {
key := makeAnnotationKey(imageName, imageDigest)
return "request.object.metadata.annotations." + `"` + key + `"`
}
func makeAnnotationKeyForJSONPatch(imageName, imageDigest string) string {
key := makeAnnotationKey(imageName, imageDigest)
return "/metadata/annotations/" + strings.ReplaceAll(key, "/", "~1")
}
func makeAnnotationKey(imageName, imageDigest string) string {
if imageDigest == "" {
return fmt.Sprintf("images.kyverno.io/%s", imageName)
}
return fmt.Sprintf("images.kyverno.io/%s/%s/%s", imageName, imageDigest[0:6], imageDigest[7:])
}
func fetchImageDigest(ref string) (string, error) {
parsedRef, err := name.ParseReference(ref)
if err != nil {
@ -217,7 +294,7 @@ func imageMatches(image string, imagePatterns []string) bool {
func (iv *imageVerifier) verifySignatures(imageVerify *v1.ImageVerification, imageInfo kubeutils.ImageInfo) (*response.RuleResponse, string) {
image := imageInfo.String()
iv.logger.Info("verifying image", "image", image, "attestors", len(imageVerify.Attestors), "attestations", len(imageVerify.Attestations))
iv.logger.V(2).Info("verifying image signatures", "image", image, "attestors", len(imageVerify.Attestors), "attestations", len(imageVerify.Attestations))
var digest string
for i, attestorSet := range imageVerify.Attestors {
@ -382,26 +459,15 @@ func (iv *imageVerifier) buildOptionsAndPath(attestor *v1.Attestor, imageVerify
return opts, path
}
func (iv *imageVerifier) patchDigest(path string, imageInfo kubeutils.ImageInfo, digest string, ruleResp *response.RuleResponse) error {
patch, err := makeAddDigestPatch(path, imageInfo, digest)
if err != nil {
return errors.Wrapf(err, "failed to patch image with digest. image: %s, jsonPath: %s", imageInfo.String(), path)
} else {
iv.logger.V(4).Info("patching verified image with digest", "patch", string(patch))
ruleResp.Patches = [][]byte{patch}
}
return nil
}
func makeAddDigestPatch(path string, imageInfo kubeutils.ImageInfo, digest string) ([]byte, error) {
func makeAddDigestPatch(imageInfo kubeutils.ImageInfo, digest string) ([]byte, error) {
var patch = make(map[string]interface{})
patch["op"] = "replace"
patch["path"] = path
patch["path"] = imageInfo.Pointer
patch["value"] = imageInfo.String() + "@" + digest
return json.Marshal(patch)
}
func (iv *imageVerifier) attestImage(imageVerify *v1.ImageVerification, imageInfo kubeutils.ImageInfo) *response.RuleResponse {
func (iv *imageVerifier) verifyAttestations(imageVerify *v1.ImageVerification, imageInfo kubeutils.ImageInfo) *response.RuleResponse {
image := imageInfo.String()
start := time.Now()

View file

@ -0,0 +1,91 @@
package engine
import (
"fmt"
"github.com/go-logr/logr"
gojmespath "github.com/jmespath/go-jmespath"
kyverno "github.com/kyverno/kyverno/api/kyverno/v1"
"github.com/kyverno/kyverno/pkg/engine/response"
kubeutils "github.com/kyverno/kyverno/pkg/utils/kube"
"github.com/pkg/errors"
)
func processImageValidationRule(log logr.Logger, ctx *PolicyContext, rule *kyverno.Rule) *response.RuleResponse {
if err := LoadContext(log, rule.Context, ctx, rule.Name); err != nil {
if _, ok := err.(gojmespath.NotFoundError); ok {
log.V(3).Info("failed to load context", "reason", err.Error())
} else {
log.Error(err, "failed to load context")
}
return ruleError(rule, response.Validation, "failed to load context", err)
}
preconditionsPassed, err := checkPreconditions(log, ctx, rule.RawAnyAllConditions)
if err != nil {
return ruleError(rule, response.Validation, "failed to evaluate preconditions", err)
}
if !preconditionsPassed {
if ctx.Policy.GetSpec().ValidationFailureAction == kyverno.Audit {
return nil
}
return ruleResponse(*rule, response.Validation, "preconditions not met", response.RuleStatusSkip, nil)
}
for _, v := range rule.VerifyImages {
imageVerify := v.Convert()
for _, infoMap := range ctx.JSONContext.ImageInfo() {
for _, imageInfo := range infoMap {
image := imageInfo.String()
if !imageMatches(image, imageVerify.ImageReferences) {
log.V(4).Info("image does not match pattern", "image", image, "patterns", imageVerify.ImageReferences)
return nil
}
if err := validateImage(ctx, imageVerify, imageInfo); err != nil {
return ruleResponse(*rule, response.ImageVerify, err.Error(), response.RuleStatusFail, nil)
}
}
}
}
return ruleResponse(*rule, response.Validation, "image verified", response.RuleStatusPass, nil)
}
func validateImage(ctx *PolicyContext, imageVerify *kyverno.ImageVerification, imageInfo kubeutils.ImageInfo) error {
image := imageInfo.String()
if imageVerify.VerifyDigest && imageInfo.Digest == "" {
return fmt.Errorf("missing digest for %s", image)
}
if imageVerify.Required {
verified, err := isImageVerified(ctx, imageInfo)
if err != nil {
return err
}
if !verified {
return fmt.Errorf("unverified image %s", image)
}
}
return nil
}
func isImageVerified(ctx *PolicyContext, imageInfo kubeutils.ImageInfo) (bool, error) {
key := makeAnnotationKeyForJMESPath(imageInfo.Name, imageInfo.Digest)
data, err := ctx.JSONContext.Query(key)
if err != nil {
return false, errors.Wrapf(err, "failed to query annotation for %s", key)
}
result, ok := data.(string)
if !ok {
return false, errors.Wrapf(err, "failed to convert data %s", key)
}
return result == "true", nil
}

View file

@ -2,9 +2,14 @@ package engine
import (
"encoding/json"
"fmt"
"strings"
"testing"
kubeutils "github.com/kyverno/kyverno/pkg/utils/kube"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"sigs.k8s.io/controller-runtime/pkg/log"
kyverno "github.com/kyverno/kyverno/api/kyverno/v1"
"github.com/kyverno/kyverno/pkg/cosign"
"github.com/kyverno/kyverno/pkg/engine/context"
@ -114,7 +119,10 @@ var testPolicyBad = `{
var testResource = `{
"apiVersion": "v1",
"kind": "Pod",
"metadata": {"name": "test"},
"metadata": {
"name": "test",
"annotations": {}
},
"spec": {
"containers": [
{
@ -132,7 +140,7 @@ var payloads = [][]byte{
}
func Test_CosignMockAttest(t *testing.T) {
policyContext := buildContext(t, testPolicyGood, testResource)
policyContext := buildContext(t, testPolicyGood, testResource, "")
err := cosign.SetMock("ghcr.io/jimbugwadia/pause2:latest", payloads)
assert.NilError(t, err)
@ -143,7 +151,7 @@ func Test_CosignMockAttest(t *testing.T) {
}
func Test_CosignMockAttest_fail(t *testing.T) {
policyContext := buildContext(t, testPolicyBad, testResource)
policyContext := buildContext(t, testPolicyBad, testResource, "")
err := cosign.SetMock("ghcr.io/jimbugwadia/pause2:latest", payloads)
assert.NilError(t, err)
@ -152,33 +160,40 @@ func Test_CosignMockAttest_fail(t *testing.T) {
assert.Equal(t, er.PolicyResponse.Rules[0].Status, response.RuleStatusFail)
}
func buildContext(t *testing.T, policy, resource string) *PolicyContext {
policyRaw := []byte(policy)
resourceRaw := []byte(resource)
func buildContext(t *testing.T, policy, resource string, oldResource string) *PolicyContext {
var cpol kyverno.ClusterPolicy
err := json.Unmarshal(policyRaw, &cpol)
if err != nil {
t.Error(err)
}
resourceUnstructured, err := utils.ConvertToUnstructured(resourceRaw)
err := json.Unmarshal([]byte(policy), &cpol)
assert.NilError(t, err)
resourceUnstructured, err := utils.ConvertToUnstructured([]byte(resource))
assert.NilError(t, err)
ctx := context.NewContext()
err = context.AddResource(ctx, resourceRaw)
if err != nil {
t.Error(err)
}
err = context.AddResource(ctx, []byte(resource))
assert.NilError(t, err)
policyContext := &PolicyContext{
Policy: &cpol,
JSONContext: ctx,
NewResource: *resourceUnstructured}
NewResource: *resourceUnstructured,
}
if oldResource != "" {
oldResourceUnstructured, err := utils.ConvertToUnstructured([]byte(oldResource))
assert.NilError(t, err)
err = context.AddOldResource(ctx, []byte(oldResource))
assert.NilError(t, err)
policyContext.OldResource = *oldResourceUnstructured
}
if err := ctx.AddImageInfos(resourceUnstructured); err != nil {
t.Errorf("unable to add image info to variables context: %v", err)
t.Fail()
}
return policyContext
}
@ -304,29 +319,29 @@ var testVerifyImageKey = `-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj
var testOtherKey = `-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEyBg8yod24/wIcc5QqlVLtCfL+6Te+nwdPdTvMb1AiZn24zBToHJVZvQdYLgRWAbh0Jd+6JhEwsDmnXRrlV7rfw==\n-----END PUBLIC KEY-----\n`
func Test_SignatureGoodSigned(t *testing.T) {
policyContext := buildContext(t, testSampleSingleKeyPolicy, testSampleResource)
policyContext := buildContext(t, testSampleSingleKeyPolicy, testSampleResource, "")
cosign.ClearMock()
err := VerifyAndPatchImages(policyContext)
assert.Equal(t, len(err.PolicyResponse.Rules), 1)
assert.Equal(t, err.PolicyResponse.Rules[0].Status, response.RuleStatusPass)
assert.Equal(t, err.PolicyResponse.Rules[0].Status, response.RuleStatusPass, err.PolicyResponse.Rules[0].Message)
}
func Test_SignatureUnsigned(t *testing.T) {
cosign.ClearMock()
unsigned := strings.Replace(testSampleResource, ":signed", ":unsigned", -1)
policyContext := buildContext(t, testSampleSingleKeyPolicy, unsigned)
policyContext := buildContext(t, testSampleSingleKeyPolicy, unsigned, "")
err := VerifyAndPatchImages(policyContext)
assert.Equal(t, len(err.PolicyResponse.Rules), 1)
assert.Equal(t, err.PolicyResponse.Rules[0].Status, response.RuleStatusFail)
assert.Equal(t, err.PolicyResponse.Rules[0].Status, response.RuleStatusFail, err.PolicyResponse.Rules[0].Message)
}
func Test_SignatureWrongKey(t *testing.T) {
cosign.ClearMock()
otherKey := strings.Replace(testSampleResource, ":signed", ":signed-by-someone-else", -1)
policyContext := buildContext(t, testSampleSingleKeyPolicy, otherKey)
policyContext := buildContext(t, testSampleSingleKeyPolicy, otherKey, "")
err := VerifyAndPatchImages(policyContext)
assert.Equal(t, len(err.PolicyResponse.Rules), 1)
assert.Equal(t, err.PolicyResponse.Rules[0].Status, response.RuleStatusFail)
assert.Equal(t, err.PolicyResponse.Rules[0].Status, response.RuleStatusFail, err.PolicyResponse.Rules[0].Message)
}
func Test_SignaturesMultiKey(t *testing.T) {
@ -334,20 +349,20 @@ func Test_SignaturesMultiKey(t *testing.T) {
policy := strings.Replace(testSampleMultipleKeyPolicy, "KEY1", testVerifyImageKey, -1)
policy = strings.Replace(policy, "KEY2", testVerifyImageKey, -1)
policy = strings.Replace(policy, "COUNT", "0", -1)
policyContext := buildContext(t, policy, testSampleResource)
policyContext := buildContext(t, policy, testSampleResource, "")
err := VerifyAndPatchImages(policyContext)
assert.Equal(t, len(err.PolicyResponse.Rules), 1)
assert.Equal(t, err.PolicyResponse.Rules[0].Status, response.RuleStatusPass)
assert.Equal(t, err.PolicyResponse.Rules[0].Status, response.RuleStatusPass, err.PolicyResponse.Rules[0].Message)
}
func Test_SignaturesMultiKeyFail(t *testing.T) {
cosign.ClearMock()
policy := strings.Replace(testSampleMultipleKeyPolicy, "KEY1", testVerifyImageKey, -1)
policy = strings.Replace(policy, "COUNT", "0", -1)
policyContext := buildContext(t, policy, testSampleResource)
policyContext := buildContext(t, policy, testSampleResource, "")
err := VerifyAndPatchImages(policyContext)
assert.Equal(t, len(err.PolicyResponse.Rules), 1)
assert.Equal(t, err.PolicyResponse.Rules[0].Status, response.RuleStatusFail)
assert.Equal(t, err.PolicyResponse.Rules[0].Status, response.RuleStatusFail, err.PolicyResponse.Rules[0].Message)
}
func Test_SignaturesMultiKeyOneGoodKey(t *testing.T) {
@ -355,10 +370,10 @@ func Test_SignaturesMultiKeyOneGoodKey(t *testing.T) {
policy := strings.Replace(testSampleMultipleKeyPolicy, "KEY1", testVerifyImageKey, -1)
policy = strings.Replace(policy, "KEY2", testOtherKey, -1)
policy = strings.Replace(policy, "COUNT", "1", -1)
policyContext := buildContext(t, policy, testSampleResource)
policyContext := buildContext(t, policy, testSampleResource, "")
err := VerifyAndPatchImages(policyContext)
assert.Equal(t, len(err.PolicyResponse.Rules), 1)
assert.Equal(t, err.PolicyResponse.Rules[0].Status, response.RuleStatusPass)
assert.Equal(t, err.PolicyResponse.Rules[0].Status, response.RuleStatusPass, err.PolicyResponse.Rules[0].Message)
}
func Test_SignaturesMultiKeyZeroGoodKey(t *testing.T) {
@ -366,10 +381,10 @@ func Test_SignaturesMultiKeyZeroGoodKey(t *testing.T) {
policy := strings.Replace(testSampleMultipleKeyPolicy, "KEY1", testOtherKey, -1)
policy = strings.Replace(policy, "KEY2", testOtherKey, -1)
policy = strings.Replace(policy, "COUNT", "1", -1)
policyContext := buildContext(t, policy, testSampleResource)
policyContext := buildContext(t, policy, testSampleResource, "")
err := VerifyAndPatchImages(policyContext)
assert.Equal(t, len(err.PolicyResponse.Rules), 1)
assert.Equal(t, err.PolicyResponse.Rules[0].Status, response.RuleStatusFail)
assert.Equal(t, err.PolicyResponse.Rules[0].Status, response.RuleStatusFail, err.PolicyResponse.Rules[0].Message)
}
var testNestedAttestorPolicy = `
@ -439,7 +454,7 @@ func Test_NestedAttestors(t *testing.T) {
policy := strings.Replace(testNestedAttestorPolicy, "KEY1", testVerifyImageKey, -1)
policy = strings.Replace(policy, "KEY2", testVerifyImageKey, -1)
policy = strings.Replace(policy, "COUNT", "0", -1)
policyContext := buildContext(t, policy, testSampleResource)
policyContext := buildContext(t, policy, testSampleResource, "")
err := VerifyAndPatchImages(policyContext)
assert.Equal(t, len(err.PolicyResponse.Rules), 1)
assert.Equal(t, err.PolicyResponse.Rules[0].Status, response.RuleStatusPass)
@ -447,7 +462,7 @@ func Test_NestedAttestors(t *testing.T) {
policy = strings.Replace(testNestedAttestorPolicy, "KEY1", testVerifyImageKey, -1)
policy = strings.Replace(policy, "KEY2", testOtherKey, -1)
policy = strings.Replace(policy, "COUNT", "0", -1)
policyContext = buildContext(t, policy, testSampleResource)
policyContext = buildContext(t, policy, testSampleResource, "")
err = VerifyAndPatchImages(policyContext)
assert.Equal(t, len(err.PolicyResponse.Rules), 1)
assert.Equal(t, err.PolicyResponse.Rules[0].Status, response.RuleStatusFail)
@ -455,7 +470,7 @@ func Test_NestedAttestors(t *testing.T) {
policy = strings.Replace(testNestedAttestorPolicy, "KEY1", testVerifyImageKey, -1)
policy = strings.Replace(policy, "KEY2", testOtherKey, -1)
policy = strings.Replace(policy, "COUNT", "1", -1)
policyContext = buildContext(t, policy, testSampleResource)
policyContext = buildContext(t, policy, testSampleResource, "")
err = VerifyAndPatchImages(policyContext)
assert.Equal(t, len(err.PolicyResponse.Rules), 1)
assert.Equal(t, err.PolicyResponse.Rules[0].Status, response.RuleStatusPass)
@ -483,3 +498,66 @@ func createStaticKeyAttestorSet(s string) *kyverno.AttestorSet {
},
}
}
func Test_ChangedAnnotation(t *testing.T) {
name := "nginx"
digest := "sha256:859ab6768a6f26a79bc42b231664111317d095a4f04e4b6fe79ce37b3d199097"
annotationKey := makeAnnotationKey(name, digest)
annotationNew := fmt.Sprintf("\"annotations\": {\"%s\": \"%s\"}", annotationKey, "true")
newResource := strings.ReplaceAll(testResource, "\"annotations\": {}", annotationNew)
policyContext := buildContext(t, testPolicyGood, testResource, testResource)
hasChanged := hasImageVerifiedAnnotationChanged(policyContext, name, digest)
assert.Equal(t, hasChanged, false)
policyContext = buildContext(t, testPolicyGood, newResource, testResource)
hasChanged = hasImageVerifiedAnnotationChanged(policyContext, name, digest)
assert.Equal(t, hasChanged, true)
annotationOld := fmt.Sprintf("\"annotations\": {\"%s\": \"%s\"}", annotationKey, "false")
oldResource := strings.ReplaceAll(testResource, "\"annotations\": {}", annotationOld)
policyContext = buildContext(t, testPolicyGood, newResource, oldResource)
hasChanged = hasImageVerifiedAnnotationChanged(policyContext, name, digest)
assert.Equal(t, hasChanged, true)
}
func Test_MarkImageVerified(t *testing.T) {
imageVerifyRule := &kyverno.ImageVerification{Required: true}
iv := &imageVerifier{
logger: log.Log,
policyContext: buildContext(t, testPolicyGood, testResource, ""),
rule: &kyverno.Rule{VerifyImages: []*kyverno.ImageVerification{imageVerifyRule}},
resp: &response.EngineResponse{},
}
ruleResp := &response.RuleResponse{Status: response.RuleStatusPass}
digest := "sha256:859ab6768a6f26a79bc42b231664111317d095a4f04e4b6fe79ce37b3d199097"
imageInfo := kubeutils.ImageInfo{}
imageInfo.Name = "nginx"
iv.markImageVerified(imageVerifyRule, ruleResp, digest, imageInfo)
assert.Equal(t, len(ruleResp.Patches), 1)
u := applyPatches(t, ruleResp)
key := makeAnnotationKey(imageInfo.Name, digest)
value := u.GetAnnotations()[key]
assert.Equal(t, value, "true")
ruleResp.Patches = nil
imageVerifyRule = &kyverno.ImageVerification{Required: false}
iv.rule = &kyverno.Rule{VerifyImages: []*kyverno.ImageVerification{imageVerifyRule}}
iv.markImageVerified(imageVerifyRule, ruleResp, digest, imageInfo)
assert.Equal(t, len(ruleResp.Patches), 0)
}
func applyPatches(t *testing.T, ruleResp *response.RuleResponse) unstructured.Unstructured {
patchedResource, err := utils.ApplyPatches([]byte(testResource), ruleResp.Patches)
assert.NilError(t, err)
assert.Assert(t, patchedResource != nil)
u := unstructured.Unstructured{}
err = u.UnmarshalJSON(patchedResource)
assert.NilError(t, err)
return u
}

View file

@ -86,7 +86,7 @@ const (
//Generation type for generation rule
Generation RuleType = "Generation"
// ImageVerify type for image verification
ImageVerify RuleType = "All"
ImageVerify RuleType = "ImageVerify"
)
//RuleResponse details for each rule application
@ -149,6 +149,11 @@ func (er EngineResponse) IsFailed() bool {
return false
}
//IsEmpty checks if any rule results are present
func (er EngineResponse) IsEmpty() bool {
return len(er.PolicyResponse.Rules) == 0
}
//GetPatches returns all the patches joined
func (er EngineResponse) GetPatches() [][]byte {
var patches [][]byte

View file

@ -93,7 +93,9 @@ func validateResource(log logr.Logger, ctx *PolicyContext) *response.EngineRespo
rules := autogen.ComputeRules(ctx.Policy)
for i := range rules {
rule := &rules[i]
if !rule.HasValidate() {
hasValidate := rule.HasValidate()
hasValidateImage := rule.HasImagesValidationChecks()
if !hasValidate && !hasValidateImage {
continue
}
@ -106,7 +108,13 @@ func validateResource(log logr.Logger, ctx *PolicyContext) *response.EngineRespo
ctx.JSONContext.Reset()
startTime := time.Now()
ruleResp := processValidationRule(log, ctx, rule)
var ruleResp *response.RuleResponse
if hasValidate {
ruleResp = processValidationRule(log, ctx, rule)
} else if hasValidateImage {
ruleResp = processImageValidationRule(log, ctx, rule)
}
if ruleResp != nil {
addRuleResponse(log, resp, ruleResp, startTime)
}
@ -207,6 +215,7 @@ func (v *validator) validate() *response.RuleResponse {
if err != nil {
return ruleError(v.rule, response.Validation, "failed to evaluate preconditions", err)
}
if !preconditionsPassed && (v.ctx.Policy.GetSpec().ValidationFailureAction != kyverno.Audit || store.GetMock()) {
return ruleResponse(*v.rule, response.Validation, "preconditions not met", response.RuleStatusSkip, nil)
}

View file

@ -42,11 +42,12 @@ type policyCache struct {
// newPolicyCache ...
func newPolicyCache(log logr.Logger, pLister kyvernolister.ClusterPolicyLister, npLister kyvernolister.PolicyLister) Interface {
namesCache := map[PolicyType]map[string]bool{
Mutate: make(map[string]bool),
ValidateEnforce: make(map[string]bool),
ValidateAudit: make(map[string]bool),
Generate: make(map[string]bool),
VerifyImages: make(map[string]bool),
Mutate: make(map[string]bool),
ValidateEnforce: make(map[string]bool),
ValidateAudit: make(map[string]bool),
Generate: make(map[string]bool),
VerifyImagesMutate: make(map[string]bool),
VerifyImagesValidate: make(map[string]bool),
}
return &policyCache{

View file

@ -39,12 +39,6 @@ func (m *pMap) addPolicyToCache(policy kyverno.PolicyInterface) {
}
}
mutateMap := m.nameCacheMap[Mutate]
validateEnforceMap := m.nameCacheMap[ValidateEnforce]
validateAuditMap := m.nameCacheMap[ValidateAudit]
generateMap := m.nameCacheMap[Generate]
imageVerifyMap := m.nameCacheMap[VerifyImages]
var pName = policy.GetName()
pSpace := policy.GetNamespace()
if pSpace != "" {
@ -54,24 +48,17 @@ func (m *pMap) addPolicyToCache(policy kyverno.PolicyInterface) {
for _, rule := range autogen.ComputeRules(policy) {
if len(rule.MatchResources.Any) > 0 {
for _, rmr := range rule.MatchResources.Any {
addCacheHelper(rmr, m, rule, mutateMap, pName, enforcePolicy, validateEnforceMap, validateAuditMap, generateMap, imageVerifyMap)
addCacheHelper(rmr, m, rule, pName, enforcePolicy)
}
} else if len(rule.MatchResources.All) > 0 {
for _, rmr := range rule.MatchResources.All {
addCacheHelper(rmr, m, rule, mutateMap, pName, enforcePolicy, validateEnforceMap, validateAuditMap, generateMap, imageVerifyMap)
addCacheHelper(rmr, m, rule, pName, enforcePolicy)
}
} else {
r := kyverno.ResourceFilter{UserInfo: rule.MatchResources.UserInfo, ResourceDescription: rule.MatchResources.ResourceDescription}
addCacheHelper(r, m, rule, mutateMap, pName, enforcePolicy, validateEnforceMap, validateAuditMap, generateMap, imageVerifyMap)
addCacheHelper(r, m, rule, pName, enforcePolicy)
}
}
m.nameCacheMap[Mutate] = mutateMap
m.nameCacheMap[ValidateEnforce] = validateEnforceMap
m.nameCacheMap[ValidateAudit] = validateAuditMap
m.nameCacheMap[Generate] = generateMap
m.nameCacheMap[VerifyImages] = imageVerifyMap
}
func (m *pMap) get(key PolicyType, gvk, namespace string) (names []string) {
@ -126,7 +113,7 @@ func (m *pMap) update(old kyverno.PolicyInterface, new kyverno.PolicyInterface)
m.addPolicyToCache(new)
}
func addCacheHelper(rmr kyverno.ResourceFilter, m *pMap, rule kyverno.Rule, mutateMap map[string]bool, pName string, enforcePolicy bool, validateEnforceMap map[string]bool, validateAuditMap map[string]bool, generateMap map[string]bool, imageVerifyMap map[string]bool) {
func addCacheHelper(rmr kyverno.ResourceFilter, m *pMap, rule kyverno.Rule, pName string, enforcePolicy bool) {
for _, gvk := range rmr.Kinds {
_, k := kubeutils.GetKindFromGVK(gvk)
kind := strings.Title(k)
@ -136,48 +123,58 @@ func addCacheHelper(rmr kyverno.ResourceFilter, m *pMap, rule kyverno.Rule, muta
}
if rule.HasMutate() {
if !mutateMap[kind+"/"+pName] {
mutateMap[kind+"/"+pName] = true
if !m.nameCacheMap[Mutate][kind+"/"+pName] {
m.nameCacheMap[Mutate][kind+"/"+pName] = true
mutatePolicy := m.kindDataMap[kind][Mutate]
m.kindDataMap[kind][Mutate] = append(mutatePolicy, pName)
}
continue
}
if rule.HasValidate() {
if enforcePolicy {
if !validateEnforceMap[kind+"/"+pName] {
validateEnforceMap[kind+"/"+pName] = true
if !m.nameCacheMap[ValidateEnforce][kind+"/"+pName] {
m.nameCacheMap[ValidateEnforce][kind+"/"+pName] = true
validatePolicy := m.kindDataMap[kind][ValidateEnforce]
m.kindDataMap[kind][ValidateEnforce] = append(validatePolicy, pName)
}
continue
}
// ValidateAudit
if !validateAuditMap[kind+"/"+pName] {
validateAuditMap[kind+"/"+pName] = true
if !m.nameCacheMap[ValidateAudit][kind+"/"+pName] {
m.nameCacheMap[ValidateAudit][kind+"/"+pName] = true
validatePolicy := m.kindDataMap[kind][ValidateAudit]
m.kindDataMap[kind][ValidateAudit] = append(validatePolicy, pName)
}
continue
}
if rule.HasGenerate() {
if !generateMap[kind+"/"+pName] {
generateMap[kind+"/"+pName] = true
if !m.nameCacheMap[Generate][kind+"/"+pName] {
m.nameCacheMap[Generate][kind+"/"+pName] = true
generatePolicy := m.kindDataMap[kind][Generate]
m.kindDataMap[kind][Generate] = append(generatePolicy, pName)
}
continue
}
if rule.HasVerifyImages() {
if !imageVerifyMap[kind+"/"+pName] {
imageVerifyMap[kind+"/"+pName] = true
imageVerifyMapPolicy := m.kindDataMap[kind][VerifyImages]
m.kindDataMap[kind][VerifyImages] = append(imageVerifyMapPolicy, pName)
if !m.nameCacheMap[VerifyImagesMutate][kind+"/"+pName] {
m.nameCacheMap[VerifyImagesMutate][kind+"/"+pName] = true
imageVerifyMapPolicy := m.kindDataMap[kind][VerifyImagesMutate]
m.kindDataMap[kind][VerifyImagesMutate] = append(imageVerifyMapPolicy, pName)
}
if rule.HasImagesValidationChecks() {
m.nameCacheMap[VerifyImagesValidate][kind+"/"+pName] = true
imageVerifyMapPolicy := m.kindDataMap[kind][VerifyImagesValidate]
m.kindDataMap[kind][VerifyImagesValidate] = append(imageVerifyMapPolicy, pName)
}
continue
}
}

View file

@ -9,5 +9,6 @@ const (
ValidateEnforce
ValidateAudit
Generate
VerifyImages
VerifyImagesMutate
VerifyImagesValidate
)

View file

@ -249,7 +249,7 @@ func (m *webhookConfigManager) enqueueAllPolicies() {
logger := m.log.WithName("enqueueAllPolicies")
policies, err := m.listAllPolicies()
if err != nil {
logger.Error(err, "unabled to list policies")
logger.Error(err, "unable to list policies")
}
for _, policy := range policies {
m.enqueue(policy)
@ -434,7 +434,7 @@ func (m *webhookConfigManager) buildWebhooks(namespace string) (res []*webhook,
for _, p := range policies {
spec := p.GetSpec()
if spec.HasValidate() || spec.HasGenerate() || spec.HasMutate() {
if spec.HasValidate() || spec.HasGenerate() || spec.HasMutate() || spec.HasImagesValidationChecks() {
if spec.GetFailurePolicy() == kyverno.Ignore {
m.mergeWebhook(validateIgnore, p, true)
} else {

View file

@ -139,7 +139,7 @@ func (ws *WebhookServer) resourceMutation(request *admissionv1.AdmissionRequest)
requestTime := time.Now().Unix()
mutatePolicies := ws.pCache.GetPolicies(policycache.Mutate, request.Kind.Kind, request.Namespace)
verifyImagesPolicies := ws.pCache.GetPolicies(policycache.VerifyImages, request.Kind.Kind, request.Namespace)
verifyImagesPolicies := ws.pCache.GetPolicies(policycache.VerifyImagesMutate, request.Kind.Kind, request.Namespace)
if len(mutatePolicies) == 0 && len(verifyImagesPolicies) == 0 {
logger.V(4).Info("no policies matched admission request")
@ -188,6 +188,10 @@ func (ws *WebhookServer) resourceValidation(request *admissionv1.AdmissionReques
policies := ws.pCache.GetPolicies(policycache.ValidateEnforce, request.Kind.Kind, request.Namespace)
mutatePolicies := ws.pCache.GetPolicies(policycache.Mutate, request.Kind.Kind, request.Namespace)
generatePolicies := ws.pCache.GetPolicies(policycache.Generate, request.Kind.Kind, request.Namespace)
imageVerifyValidatePolicies := ws.pCache.GetPolicies(policycache.VerifyImagesValidate, request.Kind.Kind, request.Namespace)
policies = append(policies, imageVerifyValidatePolicies...)
if len(generatePolicies) == 0 && request.Operation == admissionv1.Update {
// handle generate source resource updates
go ws.handleUpdatesForGenerateRules(request, []kyverno.PolicyInterface{})

View file

@ -0,0 +1,16 @@
name: test-image-digest
policies:
- policies.yaml
resources:
- resources.yaml
results:
- policy: require-image-digest
rule: check-digest
resource: no-digest
kind: Pod
status: fail
- policy: require-image-digest
rule: check-digest
resource: with-digest
kind: Pod
status: pass

View file

@ -0,0 +1,20 @@
---
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-image-digest
annotations:
pod-policies.kyverno.io/autogen-controllers: none
spec:
rules:
- name: check-digest
match:
resources:
kinds:
- Pod
verifyImages:
- imageReferences:
- "*"
verifyDigest: true

View file

@ -0,0 +1,24 @@
---
apiVersion: v1
kind: Pod
metadata:
name: no-digest
namespace: test
labels:
app: app
spec:
containers:
- name: nginx
image: nginx:latest
---
apiVersion: v1
kind: Pod
metadata:
name: with-digest
namespace: test
labels:
app: app
spec:
containers:
- name: nginx
image: nginx:latest@sha256:859ab6768a6f26a79bc42b231664111317d095a4f04e4b6fe79ce37b3d199097

View file

@ -0,0 +1,12 @@
name: test-image-digest
policies:
- policies.yaml
resources:
- resources.yaml
results:
# Requires Kyverno CLI updates
# - policy: verify-signature
# rule: check-static-key
# resource: signed
# kind: Pod
# status: pass

View file

@ -0,0 +1,25 @@
---
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: verify-signature
annotations:
pod-policies.kyverno.io/autogen-controllers: none
spec:
rules:
- name: check-static-key
match:
resources:
kinds:
- Pod
verifyImages:
- imageReferences:
- "ghcr.io/kyverno/test-verify-image:*"
attestors:
- entries:
- staticKey:
key: |-
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEyBg8yod24/wIcc5QqlVLtCfL+6Te
+nwdPdTvMb1AiZn24zBToHJVZvQdYLgRWAbh0Jd+6JhEwsDmnXRrlV7rfw==
-----END PUBLIC KEY-----

View file

@ -0,0 +1,18 @@
---
apiVersion: v1
kind: Pod
metadata:
name: signed
sspec:
containers:
- name: signed
image: ghcr.io/kyverno/test-verify-image:signed
---
apiVersion: v1
kind: Pod
metadata:
name: unsigned
sspec:
containers:
- name: signed
image: ghcr.io/kyverno/test-verify-image:unsigned