2023-03-30 11:51:16 +02:00
package validation
import (
"context"
"encoding/json"
"fmt"
2024-03-11 02:32:05 -07:00
"regexp"
2023-12-26 19:58:08 +05:30
"strings"
2023-03-30 11:51:16 +02:00
"github.com/go-logr/logr"
kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1"
2023-12-22 12:13:58 +02:00
kyvernov2beta1 "github.com/kyverno/kyverno/api/kyverno/v2beta1"
2023-03-30 11:51:16 +02:00
engineapi "github.com/kyverno/kyverno/pkg/engine/api"
"github.com/kyverno/kyverno/pkg/engine/handlers"
2023-11-13 17:43:25 +02:00
engineutils "github.com/kyverno/kyverno/pkg/engine/utils"
2023-03-30 11:51:16 +02:00
"github.com/kyverno/kyverno/pkg/pss"
2023-12-26 19:58:08 +05:30
pssutils "github.com/kyverno/kyverno/pkg/pss/utils"
2024-03-11 02:32:05 -07:00
"github.com/kyverno/kyverno/pkg/utils/api"
2023-03-30 11:51:16 +02:00
appsv1 "k8s.io/api/apps/v1"
batchv1 "k8s.io/api/batch/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
2023-11-13 17:43:25 +02:00
"k8s.io/client-go/tools/cache"
2023-03-30 11:51:16 +02:00
)
type validatePssHandler struct { }
2023-04-03 21:58:58 +02:00
func NewValidatePssHandler ( ) ( handlers . Handler , error ) {
return validatePssHandler { } , nil
2023-03-30 11:51:16 +02:00
}
func ( h validatePssHandler ) Process (
ctx context . Context ,
logger logr . Logger ,
policyContext engineapi . PolicyContext ,
resource unstructured . Unstructured ,
rule kyvernov1 . Rule ,
2023-04-03 21:58:58 +02:00
_ engineapi . EngineContextLoader ,
2024-03-28 10:31:40 +01:00
exceptions [ ] * kyvernov2beta1 . PolicyException ,
2023-03-30 11:51:16 +02:00
) ( unstructured . Unstructured , [ ] engineapi . RuleResponse ) {
2023-12-26 19:58:08 +05:30
if engineutils . IsDeleteRequest ( policyContext ) {
logger . V ( 3 ) . Info ( "skipping PSS validation on deleted resource" )
return resource , nil
}
2023-11-13 17:43:25 +02:00
// check if there is a policy exception matches the incoming resource
exception := engineutils . MatchesException ( exceptions , policyContext , logger )
2024-01-26 20:43:07 +02:00
if exception != nil && ! exception . HasPodSecurity ( ) {
2023-11-13 17:43:25 +02:00
key , err := cache . MetaNamespaceKeyFunc ( exception )
if err != nil {
logger . Error ( err , "failed to compute policy exception key" , "namespace" , exception . GetNamespace ( ) , "name" , exception . GetName ( ) )
return resource , handlers . WithError ( rule , engineapi . Validation , "failed to compute exception key" , err )
} else {
logger . V ( 3 ) . Info ( "policy rule skipped due to policy exception" , "exception" , key )
return resource , handlers . WithResponses (
engineapi . RuleSkip ( rule . Name , engineapi . Validation , "rule skipped due to policy exception " + key ) . WithException ( exception ) ,
)
}
}
2023-03-30 11:51:16 +02:00
// Marshal pod metadata and spec
2023-04-03 06:57:48 +02:00
podSecurity := rule . Validation . PodSecurity
2023-04-13 20:02:39 +08:00
if resource . Object == nil {
resource = policyContext . OldResource ( )
}
2023-03-30 11:51:16 +02:00
podSpec , metadata , err := getSpec ( resource )
if err != nil {
2023-04-05 12:35:38 +02:00
return resource , handlers . WithError ( rule , engineapi . Validation , "Error while getting new resource" , err )
2023-03-30 11:51:16 +02:00
}
pod := & corev1 . Pod {
Spec : * podSpec ,
ObjectMeta : * metadata ,
}
2024-01-26 20:43:07 +02:00
levelVersion , err := pss . ParseVersion ( podSecurity . Level , podSecurity . Version )
2023-03-30 11:51:16 +02:00
if err != nil {
2023-04-05 12:35:38 +02:00
return resource , handlers . WithError ( rule , engineapi . Validation , "failed to parse pod security api version" , err )
2023-03-30 11:51:16 +02:00
}
2024-01-26 20:43:07 +02:00
allowed , pssChecks := pss . EvaluatePod ( levelVersion , podSecurity . Exclude , pod )
2023-12-26 19:58:08 +05:30
pssChecks = convertChecks ( pssChecks , resource . GetKind ( ) )
2024-03-11 02:32:05 -07:00
pssChecks = addImages ( pssChecks , policyContext . JSONContext ( ) . ImageInfo ( ) )
2023-04-05 12:35:38 +02:00
podSecurityChecks := engineapi . PodSecurityChecks {
2023-03-30 11:51:16 +02:00
Level : podSecurity . Level ,
Version : podSecurity . Version ,
Checks : pssChecks ,
}
if allowed {
msg := fmt . Sprintf ( "Validation rule '%s' passed." , rule . Name )
2023-04-05 12:35:38 +02:00
return resource , handlers . WithResponses (
engineapi . RulePass ( rule . Name , engineapi . Validation , msg ) . WithPodSecurityChecks ( podSecurityChecks ) ,
)
2023-03-30 11:51:16 +02:00
} else {
2024-01-26 20:43:07 +02:00
// apply pod security exceptions if exist
if exception != nil && exception . HasPodSecurity ( ) {
pssChecks , err = pss . ApplyPodSecurityExclusion ( levelVersion , exception . Spec . PodSecurity , pssChecks , pod )
if len ( pssChecks ) == 0 && err == nil {
key , err := cache . MetaNamespaceKeyFunc ( exception )
if err != nil {
logger . Error ( err , "failed to compute policy exception key" , "namespace" , exception . GetNamespace ( ) , "name" , exception . GetName ( ) )
return resource , handlers . WithError ( rule , engineapi . Validation , "failed to compute exception key" , err )
} else {
podSecurityChecks . Checks = pssChecks
logger . V ( 3 ) . Info ( "policy rule skipped due to policy exception" , "exception" , key )
return resource , handlers . WithResponses (
engineapi . RuleSkip ( rule . Name , engineapi . Validation , "rule skipped due to policy exception " + key ) . WithException ( exception ) . WithPodSecurityChecks ( podSecurityChecks ) ,
)
}
}
}
2023-03-30 11:51:16 +02:00
msg := fmt . Sprintf ( ` Validation rule '%s' failed. It violates PodSecurity "%s:%s": %s ` , rule . Name , podSecurity . Level , podSecurity . Version , pss . FormatChecksPrint ( pssChecks ) )
2023-04-05 12:35:38 +02:00
return resource , handlers . WithResponses (
engineapi . RuleFail ( rule . Name , engineapi . Validation , msg ) . WithPodSecurityChecks ( podSecurityChecks ) ,
)
2023-03-30 11:51:16 +02:00
}
}
2023-12-26 19:58:08 +05:30
func convertChecks ( checks [ ] pssutils . PSSCheckResult , kind string ) ( newChecks [ ] pssutils . PSSCheckResult ) {
if kind == "DaemonSet" || kind == "Deployment" || kind == "Job" || kind == "StatefulSet" || kind == "ReplicaSet" || kind == "ReplicationController" {
for i := range checks {
for j := range * checks [ i ] . CheckResult . ErrList {
( * checks [ i ] . CheckResult . ErrList ) [ j ] . Field = strings . ReplaceAll ( ( * checks [ i ] . CheckResult . ErrList ) [ j ] . Field , "spec" , "spec.template.spec" )
}
}
} else if kind == "CronJob" {
for i := range checks {
for j := range * checks [ i ] . CheckResult . ErrList {
( * checks [ i ] . CheckResult . ErrList ) [ j ] . Field = strings . ReplaceAll ( ( * checks [ i ] . CheckResult . ErrList ) [ j ] . Field , "spec" , "spec.jobTemplate.spec.template.spec" )
}
}
}
for i := range checks {
for j := range * checks [ i ] . CheckResult . ErrList {
( * checks [ i ] . CheckResult . ErrList ) [ j ] . Field = strings . ReplaceAll ( ( * checks [ i ] . CheckResult . ErrList ) [ j ] . Field , "metadata" , "spec.template.metadata" )
}
}
return checks
}
2024-03-11 02:32:05 -07:00
// Extract container names from PSS error details. Here are some example inputs:
// - "containers \"nginx\", \"busybox\" must set securityContext.allowPrivilegeEscalation=false"
// - "containers \"nginx\", \"busybox\" must set securityContext.capabilities.drop=[\"ALL\"]"
// - "pod or containers \"nginx\", \"busybox\" must set securityContext.runAsNonRoot=true"
// - "pod or containers \"nginx\", \"busybox\" must set securityContext.seccompProfile.type to \"RuntimeDefault\" or \"Localhost\""
// - "pod or container \"nginx\" must set securityContext.seccompProfile.type to \"RuntimeDefault\" or \"Localhost\""
// - "container \"nginx\" must set securityContext.allowPrivilegeEscalation=false"
var regexContainerNames = regexp . MustCompile ( ` container(?:s)?\s*(.*?)\s*must ` )
func addImages ( checks [ ] pssutils . PSSCheckResult , imageInfos map [ string ] map [ string ] api . ImageInfo ) [ ] pssutils . PSSCheckResult {
for i , check := range checks {
text := check . CheckResult . ForbiddenDetail
matches := regexContainerNames . FindAllStringSubmatch ( text , - 1 )
if len ( matches ) > 0 {
s := strings . ReplaceAll ( matches [ 0 ] [ 1 ] , "\"" , "" )
s = strings . ReplaceAll ( s , " " , "" )
containerNames := strings . Split ( s , "," )
checks [ i ] . Images = getImages ( containerNames , imageInfos )
}
}
return checks
}
// return image references for containers
func getImages ( containerNames [ ] string , imageInfos map [ string ] map [ string ] api . ImageInfo ) [ ] string {
var images [ ] string
for _ , cn := range containerNames {
image := getImageReference ( cn , imageInfos )
images = append ( images , image )
}
return images
}
// return an image references for a container name
// if the image is not found, the name is returned
func getImageReference ( name string , imageInfos map [ string ] map [ string ] api . ImageInfo ) string {
if containers , ok := imageInfos [ "containers" ] ; ok {
if imageInfo , ok := containers [ name ] ; ok {
return imageInfo . String ( )
}
}
if initContainers , ok := imageInfos [ "initContainers" ] ; ok {
if imageInfo , ok := initContainers [ name ] ; ok {
return imageInfo . String ( )
}
}
if ephemeralContainers , ok := imageInfos [ "ephemeralContainers" ] ; ok {
if imageInfo , ok := ephemeralContainers [ name ] ; ok {
return imageInfo . String ( )
}
}
return name
}
2023-03-30 11:51:16 +02:00
func getSpec ( resource unstructured . Unstructured ) ( podSpec * corev1 . PodSpec , metadata * metav1 . ObjectMeta , err error ) {
kind := resource . GetKind ( )
if kind == "DaemonSet" || kind == "Deployment" || kind == "Job" || kind == "StatefulSet" || kind == "ReplicaSet" || kind == "ReplicationController" {
var deployment appsv1 . Deployment
resourceBytes , err := resource . MarshalJSON ( )
if err != nil {
return nil , nil , err
}
err = json . Unmarshal ( resourceBytes , & deployment )
if err != nil {
return nil , nil , err
}
podSpec = & deployment . Spec . Template . Spec
metadata = & deployment . Spec . Template . ObjectMeta
return podSpec , metadata , nil
} else if kind == "CronJob" {
var cronJob batchv1 . CronJob
resourceBytes , err := resource . MarshalJSON ( )
if err != nil {
return nil , nil , err
}
err = json . Unmarshal ( resourceBytes , & cronJob )
if err != nil {
return nil , nil , err
}
podSpec = & cronJob . Spec . JobTemplate . Spec . Template . Spec
metadata = & cronJob . Spec . JobTemplate . ObjectMeta
2024-03-11 02:32:05 -07:00
return podSpec , metadata , nil
2023-03-30 11:51:16 +02:00
} else if kind == "Pod" {
var pod corev1 . Pod
resourceBytes , err := resource . MarshalJSON ( )
if err != nil {
return nil , nil , err
}
err = json . Unmarshal ( resourceBytes , & pod )
if err != nil {
return nil , nil , err
}
podSpec = & pod . Spec
metadata = & pod . ObjectMeta
return podSpec , metadata , nil
}
2024-03-11 02:32:05 -07:00
return nil , nil , fmt . Errorf ( "could not find correct resource type" )
2023-03-30 11:51:16 +02:00
}