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:
parent
b689f1f15c
commit
ab5171cee5
29 changed files with 783 additions and 198 deletions
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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++
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 />
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
91
pkg/engine/imageVerifyValidate.go
Normal file
91
pkg/engine/imageVerifyValidate.go
Normal 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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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{
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,5 +9,6 @@ const (
|
|||
ValidateEnforce
|
||||
ValidateAudit
|
||||
Generate
|
||||
VerifyImages
|
||||
VerifyImagesMutate
|
||||
VerifyImagesValidate
|
||||
)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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{})
|
||||
|
|
16
test/cli/test/images/digest/kyverno-test.yaml
Normal file
16
test/cli/test/images/digest/kyverno-test.yaml
Normal 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
|
20
test/cli/test/images/digest/policies.yaml
Normal file
20
test/cli/test/images/digest/policies.yaml
Normal 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
|
||||
|
||||
|
24
test/cli/test/images/digest/resources.yaml
Normal file
24
test/cli/test/images/digest/resources.yaml
Normal 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
|
12
test/cli/test/images/signatures/kyverno-test.yaml
Normal file
12
test/cli/test/images/signatures/kyverno-test.yaml
Normal 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
|
25
test/cli/test/images/signatures/policies.yaml
Normal file
25
test/cli/test/images/signatures/policies.yaml
Normal 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-----
|
18
test/cli/test/images/signatures/resources.yaml
Normal file
18
test/cli/test/images/signatures/resources.yaml
Normal 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
|
Loading…
Reference in a new issue