1
0
Fork 0
mirror of https://github.com/kyverno/kyverno.git synced 2025-04-08 10:04:25 +00:00

Merge branch 'master' of github.com:nirmata/kyverno

This commit is contained in:
shivkumar dudhani 2020-01-11 11:14:56 -08:00
commit 7d66b952e7
45 changed files with 1706 additions and 953 deletions

View file

@ -3,17 +3,17 @@
##################################
# DEFAULTS
##################################
GIT_VERSION := $(shell git describe --dirty --always --tags)
GIT_BRANCH := $(shell git branch | grep \* | cut -d ' ' -f2)
GIT_HASH := $(GIT_BRANCH)/$(shell git log -1 --pretty=format:"%H")
TIMESTAMP := $(shell date '+%Y-%m-%d_%I:%M:%S%p')
REGISTRY=index.docker.io
REPO=$(REGISTRY)/nirmata/kyverno
IMAGE_TAG=$(GIT_VERSION)
GOOS ?= $(shell go env GOOS)
LD_FLAGS="-s -w -X $(PACKAGE)/pkg/version.BuildVersion=$(GIT_VERSION) -X $(PACKAGE)/pkg/version.BuildHash=$(GIT_HASH) -X $(PACKAGE)/pkg/version.BuildTime=$(TIMESTAMP)"
GIT_VERSION := $(shell git describe --dirty --always --tags)
GIT_BRANCH := $(shell git branch | grep \* | cut -d ' ' -f2)
GIT_HASH := $(GIT_BRANCH)/$(shell git log -1 --pretty=format:"%H")
TIMESTAMP := $(shell date '+%Y-%m-%d_%I:%M:%S%p')
##################################
# KYVERNO
##################################

View file

@ -106,6 +106,8 @@ spec:
generate:
kind: ConfigMap
name: zk-kafka-address
# create the resource in the new namespace
namespace: "{{request.object.name}}"
data:
kind: ConfigMap
data:

View file

@ -29,12 +29,12 @@ import (
var (
kubeconfig string
serverIP string
cpu bool
memory bool
webhookTimeout int
//TODO: this has been added to backward support command line arguments
// will be removed in future and the configuration will be set only via configmaps
filterK8Resources string
// User FQDN as CSR CN
fqdncn bool
)
func main() {
@ -184,7 +184,7 @@ func main() {
)
// CONFIGURE CERTIFICATES
tlsPair, err := client.InitTLSPemPair(clientConfig)
tlsPair, err := client.InitTLSPemPair(clientConfig, fqdncn)
if err != nil {
glog.Fatalf("Failed to initialize TLS key/certificate pair: %v\n", err)
}
@ -265,6 +265,8 @@ func init() {
flag.IntVar(&webhookTimeout, "webhooktimeout", 3, "timeout for webhook configurations")
flag.StringVar(&kubeconfig, "kubeconfig", "", "Path to a kubeconfig. Only required if out-of-cluster.")
flag.StringVar(&serverIP, "serverIP", "", "IP address where Kyverno controller runs. Only required if out-of-cluster.")
// Generate CSR with CN as FQDN due to https://github.com/nirmata/kyverno/issues/542
flag.BoolVar(&fqdncn, "fqdn-as-cn", false, "use FQDN as Common Name in CSR")
config.LogDefaultFlags()
flag.Parse()
}

View file

@ -304,13 +304,6 @@ spec:
type: string
message:
type: string
managedResource:
type: object
properties:
kind:
type: string
creationBlocked:
type: boolean
---
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
@ -383,13 +376,6 @@ spec:
type: string
message:
type: string
managedResource:
type: object
properties:
kind:
type: string
creationBlocked:
type: boolean
---
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
@ -534,16 +520,14 @@ spec:
serviceAccountName: kyverno-service-account
initContainers:
- name: kyverno-pre
image: nirmata/kyvernopre:latest
image: nirmata/kyvernopre:v1.1.0
containers:
- name: kyverno
image: nirmata/kyverno:latest
image: nirmata/kyverno:v1.1.0
args:
- "--filterK8Resources=[Event,*,*][*,kube-system,*][*,kube-public,*][*,kube-node-lease,*][Node,*,*][APIService,*,*][TokenReview,*,*][SubjectAccessReview,*,*][*,kyverno,*]"
# customize webhook timout
# - "--webhooktimeout=4"
# open one of the profiling flag here
# - "--cpu=true"
ports:
- containerPort: 443
env:

View file

@ -304,13 +304,6 @@ spec:
type: string
message:
type: string
managedResource:
type: object
properties:
kind:
type: string
creationBlocked:
type: boolean
---
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
@ -383,13 +376,6 @@ spec:
type: string
message:
type: string
managedResource:
type: object
properties:
kind:
type: string
creationBlocked:
type: boolean
---
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition

View file

@ -10,6 +10,8 @@ There are 2 ways to configure the secure communications link between Kyverno and
Kyverno can request a CA signed certificate-key pair from `kube-controller-manager`. This method requires that the kube-controller-manager is configured to act as a certificate signer. To verify that this option is enabled for your cluster, check the command-line args for the kube-controller-manager. If `--cluster-signing-cert-file` and `--cluster-signing-key-file` are passed to the controller manager with paths to your CA's key-pair, then you can proceed to install Kyverno using this method.
**Deploying on EKS requires enabling a command-line argument `--fqdncn` in the 'kyverno' container in the deployment, due to a current limitation with the certificates returned by EKS for CSR(bug: https://github.com/awslabs/amazon-eks-ami/issues/341)**
To install Kyverno in a cluster that supports certificate signing, run the following command on a host with kubectl `cluster-admin` access:
````sh
@ -130,11 +132,6 @@ To run controller in this mode you should prepare TLS key/certificate pair for d
2. Start the controller using the following command: `sudo kyverno --kubeconfig=~/.kube/config --serverIP=<server_IP>`
# Try Kyverno without a Kubernetes cluster
The [Kyverno CLI](documentation/testing-policies.md#test-using-the-kyverno-cli) allows you to write and test policies without installing Kyverno in a Kubernetes cluster. Some features are not supported without a Kubernetes cluster.
# Filter kuberenetes resources that admission webhook should not process
The admission webhook checks if a policy is applicable on all admission requests. The kubernetes kinds that are not be processed can be filtered by adding the configmap named `init-config` in namespace `kyverno` and specifying the resources to be filtered under `data.resourceFilters`
@ -152,7 +149,8 @@ data:
```
By default we have specified Nodes, Events, APIService & SubjectAccessReview as the kinds to be skipped in the default configmap
[install.yaml](https://github.com/nirmata/kyverno/raw/master/definitions/init_configMap.yaml).
[install.yaml](https://github.com/nirmata/kyverno/raw/master/definitions/install.yaml).
---
<small>*Read Next >> [Writing Policies](/documentation/writing-policies.md)*</small>

View file

@ -76,7 +76,7 @@ spec :
- key: name # compares (key operator value)
operator: Equal
value: name # constant "name" == "name"
- key: "{{serviceAccount}}" # refer to a pre-defined variable serviceAccount
- key: "{{serviceAccountName}}" # refer to a pre-defined variable serviceAccountName
operator: NotEqual
value: "user1" # if service
# Each rule can contain a single validate, mutate, or generate directive

View file

@ -278,19 +278,9 @@ type ResourceSpec struct {
// ViolatedRule stores the information regarding the rule
type ViolatedRule struct {
Name string `json:"name"`
Type string `json:"type"`
Message string `json:"message"`
ManagedResource ManagedResourceSpec `json:"managedResource,omitempty"`
}
// ManagedResourceSpec is used when the violations is created on resource owner
// to determing the kind of child resource that caused the violation
type ManagedResourceSpec struct {
Kind string `json:"kind,omitempty"`
// Is not used in processing, but will is present for backward compatablitiy
Namespace string `json:"namespace,omitempty"`
CreationBlocked bool `json:"creationBlocked,omitempty"`
Name string `json:"name"`
Type string `json:"type"`
Message string `json:"message"`
}
//PolicyViolationStatus provides information regarding policyviolation status

View file

@ -319,22 +319,6 @@ func (in *Generation) DeepCopy() *Generation {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ManagedResourceSpec) DeepCopyInto(out *ManagedResourceSpec) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ManagedResourceSpec.
func (in *ManagedResourceSpec) DeepCopy() *ManagedResourceSpec {
if in == nil {
return nil
}
out := new(ManagedResourceSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *MatchResources) DeepCopyInto(out *MatchResources) {
*out = *in
@ -714,7 +698,6 @@ func (in *Validation) DeepCopy() *Validation {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ViolatedRule) DeepCopyInto(out *ViolatedRule) {
*out = *in
out.ManagedResource = in.ManagedResource
return
}

View file

@ -18,15 +18,15 @@ import (
// InitTLSPemPair Loads or creates PEM private key and TLS certificate for webhook server.
// Created pair is stored in cluster's secret.
// Returns struct with key/certificate pair.
func (c *Client) InitTLSPemPair(configuration *rest.Config) (*tls.TlsPemPair, error) {
func (c *Client) InitTLSPemPair(configuration *rest.Config, fqdncn bool) (*tls.TlsPemPair, error) {
certProps, err := c.GetTLSCertProps(configuration)
if err != nil {
return nil, err
}
tlsPair := c.ReadTlsPair(certProps)
if tls.IsTlsPairShouldBeUpdated(tlsPair) {
if tls.IsTLSPairShouldBeUpdated(tlsPair) {
glog.Info("Generating new key/certificate pair for TLS")
tlsPair, err = c.GenerateTlsPemPair(certProps)
tlsPair, err = c.generateTLSPemPair(certProps, fqdncn)
if err != nil {
return nil, err
}
@ -40,15 +40,15 @@ func (c *Client) InitTLSPemPair(configuration *rest.Config) (*tls.TlsPemPair, er
return tlsPair, nil
}
//GenerateTlsPemPair Issues TLS certificate for webhook server using given PEM private key
//generateTlsPemPair Issues TLS certificate for webhook server using given PEM private key
// Returns signed and approved TLS certificate in PEM format
func (c *Client) GenerateTlsPemPair(props tls.TlsCertificateProps) (*tls.TlsPemPair, error) {
privateKey, err := tls.TlsGeneratePrivateKey()
func (c *Client) generateTLSPemPair(props tls.TlsCertificateProps, fqdncn bool) (*tls.TlsPemPair, error) {
privateKey, err := tls.TLSGeneratePrivateKey()
if err != nil {
return nil, err
}
certRequest, err := tls.TlsCertificateGenerateRequest(privateKey, props)
certRequest, err := tls.CertificateGenerateRequest(privateKey, props, fqdncn)
if err != nil {
return nil, fmt.Errorf("Unable to create certificate request: %v", err)
}
@ -65,7 +65,7 @@ func (c *Client) GenerateTlsPemPair(props tls.TlsCertificateProps) (*tls.TlsPemP
return &tls.TlsPemPair{
Certificate: tlsCert,
PrivateKey: tls.TlsPrivateKeyToPem(privateKey),
PrivateKey: tls.TLSPrivateKeyToPem(privateKey),
}, nil
}

View file

@ -2,9 +2,10 @@ package context
import (
"encoding/json"
"fmt"
"github.com/golang/glog"
"github.com/jmespath/go-jmespath"
jmespath "github.com/jmespath/go-jmespath"
)
//Query the JSON context with JMESPATH search path
@ -14,7 +15,7 @@ func (ctx *Context) Query(query string) (interface{}, error) {
queryPath, err := jmespath.Compile(query)
if err != nil {
glog.V(4).Infof("incorrect query %s: %v", query, err)
return emptyResult, err
return emptyResult, fmt.Errorf("incorrect query %s: %v", query, err)
}
// search
ctx.mu.RLock()
@ -22,14 +23,14 @@ func (ctx *Context) Query(query string) (interface{}, error) {
var data interface{}
if err := json.Unmarshal(ctx.jsonRaw, &data); err != nil {
glog.V(4).Infof("failed to unmarshall context")
return emptyResult, err
glog.V(4).Infof("failed to unmarshall context: %v", err)
return emptyResult, fmt.Errorf("failed to unmarshall context: %v", err)
}
result, err := queryPath.Search(data)
if err != nil {
glog.V(4).Infof("failed to search query %s: %v", query, err)
return emptyResult, err
return emptyResult, fmt.Errorf("failed to search query %s: %v", query, err)
}
return result, nil
}

View file

@ -1,16 +1,22 @@
package engine
import (
"fmt"
"github.com/golang/glog"
kyverno "github.com/nirmata/kyverno/pkg/api/kyverno/v1"
"github.com/nirmata/kyverno/pkg/engine/context"
"github.com/nirmata/kyverno/pkg/engine/rbac"
"github.com/nirmata/kyverno/pkg/engine/response"
"github.com/nirmata/kyverno/pkg/engine/utils"
"github.com/nirmata/kyverno/pkg/engine/variables"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
//GenerateNew returns the list of rules that are applicable on this policy and resource
// GenerateNew
// 1. validate variables to be susbtitute in the general ruleInfo (match,exclude,condition)
// - the caller has to check the ruleResponse to determine whether the path exist
// 2. returns the list of rules that are applicable on this policy and resource, if 1 succeed
func GenerateNew(policyContext PolicyContext) (resp response.EngineResponse) {
policy := policyContext.Policy
resource := policyContext.NewResource
@ -55,6 +61,13 @@ func filterRules(policy kyverno.ClusterPolicy, resource unstructured.Unstructure
}
for _, rule := range policy.Spec.Rules {
if paths := validateGeneralRuleInfoVariables(ctx, rule); len(paths) != 0 {
glog.Infof("referenced path not present in generate rule %s, resource %s/%s/%s, path: %s", rule.Name, resource.GetKind(), resource.GetNamespace(), resource.GetName(), paths)
resp.PolicyResponse.Rules = append(resp.PolicyResponse.Rules,
newPathNotPresentRuleResponse(rule.Name, utils.Mutation.String(), fmt.Sprintf("path not present: %s", paths)))
continue
}
if ruleResp := filterRule(rule, resource, admissionInfo, ctx); ruleResp != nil {
resp.PolicyResponse.Rules = append(resp.PolicyResponse.Rules, *ruleResp)
}

View file

@ -22,7 +22,7 @@ import (
"github.com/nirmata/kyverno/pkg/engine/variables"
)
// processOverlay processes validation patterns on the resource
// processOverlay processes mutation overlay on the resource
func ProcessOverlay(ctx context.EvalInterface, rule kyverno.Rule, resource unstructured.Unstructured) (resp response.RuleResponse, patchedResource unstructured.Unstructured) {
startTime := time.Now()
glog.V(4).Infof("started applying overlay rule %q (%v)", rule.Name, startTime)
@ -32,6 +32,16 @@ func ProcessOverlay(ctx context.EvalInterface, rule kyverno.Rule, resource unstr
resp.RuleStats.ProcessingTime = time.Since(startTime)
glog.V(4).Infof("finished applying overlay rule %q (%v)", resp.Name, resp.RuleStats.ProcessingTime)
}()
// if referenced path not present, we skip processing the rule and report violation
if invalidPaths := variables.ValidateVariables(ctx, rule.Mutation.Overlay); len(invalidPaths) != 0 {
resp.Success = true
resp.PathNotPresent = true
resp.Message = fmt.Sprintf("referenced path not present: %s", invalidPaths)
glog.V(3).Infof("Skip applying rule '%s' on resource '%s/%s/%s': %s", rule.Name, resource.GetKind(), resource.GetNamespace(), resource.GetName(), resp.Message)
return resp, resource
}
// substitute variables
// first pass we substitute all the JMESPATH substitution for the variable
// variable: {{<JMESPATH>}}

View file

@ -1,6 +1,7 @@
package engine
import (
"fmt"
"reflect"
"strings"
"time"
@ -10,6 +11,7 @@ import (
"github.com/nirmata/kyverno/pkg/engine/mutate"
"github.com/nirmata/kyverno/pkg/engine/rbac"
"github.com/nirmata/kyverno/pkg/engine/response"
"github.com/nirmata/kyverno/pkg/engine/utils"
"github.com/nirmata/kyverno/pkg/engine/variables"
)
@ -55,6 +57,13 @@ func Mutate(policyContext PolicyContext) (resp response.EngineResponse) {
continue
}
if paths := validateGeneralRuleInfoVariables(ctx, rule); len(paths) != 0 {
glog.Infof("referenced path not present in rule %s, resource %s/%s/%s, path: %s", rule.Name, resource.GetKind(), resource.GetNamespace(), resource.GetName(), paths)
resp.PolicyResponse.Rules = append(resp.PolicyResponse.Rules,
newPathNotPresentRuleResponse(rule.Name, utils.Mutation.String(), fmt.Sprintf("path not present in rule info: %s", paths)))
continue
}
startTime := time.Now()
if !rbac.MatchAdmissionInfo(rule, policyContext.AdmissionInfo) {
glog.V(3).Infof("rule '%s' cannot be applied on %s/%s/%s, admission permission: %v",
@ -82,11 +91,20 @@ func Mutate(policyContext PolicyContext) (resp response.EngineResponse) {
if rule.Mutation.Overlay != nil {
var ruleResponse response.RuleResponse
ruleResponse, patchedResource = mutate.ProcessOverlay(ctx, rule, patchedResource)
if ruleResponse.Success == true && ruleResponse.Patches == nil {
// overlay pattern does not match the resource conditions
glog.V(4).Infof(ruleResponse.Message)
continue
} else if ruleResponse.Success == true {
if ruleResponse.Success == true {
// - variable substitution path is not present
if ruleResponse.PathNotPresent {
glog.V(4).Infof(ruleResponse.Message)
resp.PolicyResponse.Rules = append(resp.PolicyResponse.Rules, ruleResponse)
continue
}
// - overlay pattern does not match the resource conditions
if ruleResponse.Patches == nil {
glog.V(4).Infof(ruleResponse.Message)
continue
}
glog.Infof("Mutate overlay in rule '%s' successfully applied on %s/%s/%s", rule.Name, resource.GetKind(), resource.GetNamespace(), resource.GetName())
}

View file

@ -3,6 +3,7 @@ package engine
import (
"encoding/json"
"reflect"
"strings"
"testing"
kyverno "github.com/nirmata/kyverno/pkg/api/kyverno/v1"
@ -88,3 +89,115 @@ func Test_VariableSubstitutionOverlay(t *testing.T) {
t.Error("patches dont match")
}
}
func Test_variableSubstitutionPathNotExist(t *testing.T) {
resourceRaw := []byte(`{
"apiVersion": "v1",
"kind": "Pod",
"metadata": {
"name": "check-root-user"
},
"spec": {
"containers": [
{
"name": "check-root-user",
"image": "nginxinc/nginx-unprivileged",
"securityContext": {
"runAsNonRoot": true
}
}
]
}
}`)
policyraw := []byte(`{
"apiVersion": "kyverno.io/v1",
"kind": "ClusterPolicy",
"metadata": {
"name": "substitue-variable"
},
"spec": {
"rules": [
{
"name": "test-path-not-exist",
"match": {
"resources": {
"kinds": [
"Pod"
]
}
},
"mutate": {
"overlay": {
"spec": {
"name": "{{request.object.metadata.name1}}"
}
}
}
}
]
}
}`)
var policy kyverno.ClusterPolicy
json.Unmarshal(policyraw, &policy)
resourceUnstructured, err := utils.ConvertToUnstructured(resourceRaw)
assert.NilError(t, err)
ctx := context.NewContext()
ctx.AddResource(resourceRaw)
policyContext := PolicyContext{
Policy: policy,
Context: ctx,
NewResource: *resourceUnstructured}
er := Mutate(policyContext)
assert.Assert(t, er.PolicyResponse.Rules[0].PathNotPresent, true)
}
func Test_variableSubstitutionPathNotExist_InRuleInfo(t *testing.T) {
resourceRaw := []byte(`{
"apiVersion": "v1",
"kind": "Deployment",
"metadata": {
"name": "check-root-user"
}
}`)
policyraw := []byte(`{
"apiVersion": "kyverno.io/v1",
"kind": "ClusterPolicy",
"metadata": {
"name": "test-validate-variables"
},
"spec": {
"rules": [
{
"name": "test-match",
"match": {
"resources": {
"kinds": [
"{{request.kind}}"
]
}
}
}
]
}
}`)
var policy kyverno.ClusterPolicy
assert.NilError(t, json.Unmarshal(policyraw, &policy))
resourceUnstructured, err := utils.ConvertToUnstructured(resourceRaw)
assert.NilError(t, err)
ctx := context.NewContext()
ctx.AddResource(resourceRaw)
policyContext := PolicyContext{
Policy: policy,
Context: ctx,
NewResource: *resourceUnstructured}
er := Mutate(policyContext)
assert.Assert(t, strings.Contains(er.PolicyResponse.Rules[0].Message, "path not present in rule info"))
}

View file

@ -65,6 +65,8 @@ type RuleResponse struct {
Success bool `json:"success"`
// statistics
RuleStats `json:",inline"`
// PathNotPresent indicates whether referenced path in variable substitution exist
PathNotPresent bool `json:"pathNotPresent"`
}
//ToString ...
@ -119,3 +121,13 @@ func (er EngineResponse) getRules(success bool) []string {
}
return rules
}
// IsPathNotPresent checks if the referenced path(in variable substitution) exist
func (er EngineResponse) IsPathNotPresent() bool {
for _, r := range er.PolicyResponse.Rules {
if r.PathNotPresent {
return true
}
}
return false
}

View file

@ -9,7 +9,10 @@ import (
"github.com/minio/minio/pkg/wildcard"
kyverno "github.com/nirmata/kyverno/pkg/api/kyverno/v1"
"github.com/nirmata/kyverno/pkg/engine/context"
"github.com/nirmata/kyverno/pkg/engine/operator"
"github.com/nirmata/kyverno/pkg/engine/response"
"github.com/nirmata/kyverno/pkg/engine/variables"
"github.com/nirmata/kyverno/pkg/utils"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@ -228,3 +231,39 @@ type resourceInfo struct {
Resource unstructured.Unstructured
Gvk *metav1.GroupVersionKind
}
// validateGeneralRuleInfoVariables validate variable subtition defined in
// - MatchResources
// - ExcludeResources
// - Conditions
func validateGeneralRuleInfoVariables(ctx context.EvalInterface, rule kyverno.Rule) string {
var tempRule kyverno.Rule
var tempRulePattern interface{}
tempRule.MatchResources = rule.MatchResources
tempRule.ExcludeResources = rule.ExcludeResources
tempRule.Conditions = rule.Conditions
raw, err := json.Marshal(tempRule)
if err != nil {
glog.Infof("failed to serilize rule info while validating variable substitution: %v", err)
return ""
}
if err := json.Unmarshal(raw, &tempRulePattern); err != nil {
glog.Infof("failed to serilize rule info while validating variable substitution: %v", err)
return ""
}
return variables.ValidateVariables(ctx, tempRulePattern)
}
func newPathNotPresentRuleResponse(rname, rtype, msg string) response.RuleResponse {
return response.RuleResponse{
Name: rname,
Type: rtype,
Message: msg,
Success: true,
PathNotPresent: true,
}
}

View file

@ -1,11 +1,15 @@
package engine
import (
"encoding/json"
"fmt"
"testing"
kyverno "github.com/nirmata/kyverno/pkg/api/kyverno/v1"
context "github.com/nirmata/kyverno/pkg/engine/context"
"github.com/nirmata/kyverno/pkg/engine/utils"
"gotest.tools/assert"
authenticationv1 "k8s.io/api/authentication/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
@ -392,3 +396,106 @@ func TestResourceDescriptionExclude_Label_Expression_Match(t *testing.T) {
assert.Assert(t, !MatchesResourceDescription(*resource, rule))
}
func Test_validateGeneralRuleInfoVariables(t *testing.T) {
rawResource := []byte(`
{
"apiVersion": "v1",
"kind": "Pod",
"metadata": {
"name": "image-with-hostpath",
"labels": {
"app.type": "prod",
"namespace": "my-namespace"
}
},
"spec": {
"containers": [
{
"name": "image-with-hostpath",
"image": "docker.io/nautiker/curl",
"volumeMounts": [
{
"name": "var-lib-etcd",
"mountPath": "/var/lib"
}
]
}
],
"volumes": [
{
"name": "var-lib-etcd",
"emptyDir": {}
}
]
}
}
`)
policyRaw := []byte(`{
"apiVersion": "kyverno.io/v1",
"kind": "ClusterPolicy",
"metadata": {
"name": "test-validate-variables"
},
"spec": {
"rules": [
{
"name": "test-match",
"match": {
"Subjects": [
{
"kind": "User",
"name": "{{request.userInfo.username1}}}"
}
],
"resources": {
"kind": "{{request.object.kind}}"
}
}
},
{
"name": "test-exclude",
"match": {
"resources": {
"namespaces": [
"{{request.object.namespace}}"
]
}
}
},
{
"name": "test-condition",
"preconditions": [
{
"key": "{{serviceAccountName}}",
"operator": "NotEqual",
"value": "testuser"
}
]
}
]
}
}`)
userReqInfo := kyverno.RequestInfo{
AdmissionUserInfo: authenticationv1.UserInfo{
Username: "user1",
},
}
var policy kyverno.ClusterPolicy
assert.NilError(t, json.Unmarshal(policyRaw, &policy))
ctx := context.NewContext()
ctx.AddResource(rawResource)
ctx.AddUserInfo(userReqInfo)
ctx.AddSA("system:serviceaccount:test:testuser")
expectPaths := []string{"request.userInfo.username1", "request.object.namespace", ""}
for i, rule := range policy.Spec.Rules {
invalidPaths := validateGeneralRuleInfoVariables(ctx, rule)
assert.Assert(t, invalidPaths == expectPaths[i], fmt.Sprintf("result not match, got invalidPaths %s", invalidPaths))
}
}

View file

@ -7,6 +7,13 @@ import (
"github.com/nirmata/kyverno/pkg/engine/operator"
)
type ValidationFailureReason int
const (
PathNotPresent ValidationFailureReason = iota
Rulefailure
)
func isStringIsReference(str string) bool {
if len(str) < len(operator.ReferenceSign) {
return false
@ -66,3 +73,13 @@ func getRawKeyIfWrappedWithAttributes(str string) string {
return str
}
}
type ValidationError struct {
StatusCode ValidationFailureReason
ErrorMsg string
}
// newValidatePatternError returns an validation error using the ValidationFailureReason and errorMsg
func newValidatePatternError(reason ValidationFailureReason, msg string) ValidationError {
return ValidationError{StatusCode: reason, ErrorMsg: msg}
}

View file

@ -17,13 +17,22 @@ import (
// validateResourceWithPattern is a start of element-by-element validation process
// It assumes that validation is started from root, so "/" is passed
//TODO: for failure, we return the path at which it failed along with error
func ValidateResourceWithPattern(ctx context.EvalInterface, resource, pattern interface{}) (string, error) {
func ValidateResourceWithPattern(ctx context.EvalInterface, resource, pattern interface{}) (string, ValidationError) {
// if referenced path is not present, we skip processing the rule and report violation
if invalidPaths := variables.ValidateVariables(ctx, pattern); len(invalidPaths) != 0 {
return "", newValidatePatternError(PathNotPresent, invalidPaths)
}
// first pass we substitute all the JMESPATH substitution for the variable
// variable: {{<JMESPATH>}}
// if a JMESPATH fails, we dont return error but variable is substitured with nil and error log
pattern = variables.SubstituteVariables(ctx, pattern)
return validateResourceElement(resource, pattern, pattern, "/")
path, err := validateResourceElement(resource, pattern, pattern, "/")
if err != nil {
return path, newValidatePatternError(Rulefailure, err.Error())
}
return "", ValidationError{}
}
// validateResourceElement detects the element type (map, array, nil, string, int, bool, float)

View file

@ -1,6 +1,7 @@
package engine
import (
"errors"
"fmt"
"reflect"
"strings"
@ -17,29 +18,6 @@ import (
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
func startResultResponse(resp *response.EngineResponse, policy kyverno.ClusterPolicy, newR unstructured.Unstructured) {
// set policy information
resp.PolicyResponse.Policy = policy.Name
// resource details
resp.PolicyResponse.Resource.Name = newR.GetName()
resp.PolicyResponse.Resource.Namespace = newR.GetNamespace()
resp.PolicyResponse.Resource.Kind = newR.GetKind()
resp.PolicyResponse.Resource.APIVersion = newR.GetAPIVersion()
resp.PolicyResponse.ValidationFailureAction = policy.Spec.ValidationFailureAction
}
func endResultResponse(resp *response.EngineResponse, startTime time.Time) {
resp.PolicyResponse.ProcessingTime = time.Since(startTime)
glog.V(4).Infof("Finished applying validation rules policy %v (%v)", resp.PolicyResponse.Policy, resp.PolicyResponse.ProcessingTime)
glog.V(4).Infof("Validation Rules appplied succesfully count %v for policy %q", resp.PolicyResponse.RulesAppliedCount, resp.PolicyResponse.Policy)
}
func incrementAppliedCount(resp *response.EngineResponse) {
// rules applied succesfully count
resp.PolicyResponse.RulesAppliedCount++
}
//Validate applies validation rules from policy on the resource
func Validate(policyContext PolicyContext) (resp response.EngineResponse) {
startTime := time.Now()
@ -87,6 +65,29 @@ func Validate(policyContext PolicyContext) (resp response.EngineResponse) {
return response.EngineResponse{}
}
func startResultResponse(resp *response.EngineResponse, policy kyverno.ClusterPolicy, newR unstructured.Unstructured) {
// set policy information
resp.PolicyResponse.Policy = policy.Name
// resource details
resp.PolicyResponse.Resource.Name = newR.GetName()
resp.PolicyResponse.Resource.Namespace = newR.GetNamespace()
resp.PolicyResponse.Resource.Kind = newR.GetKind()
resp.PolicyResponse.Resource.APIVersion = newR.GetAPIVersion()
resp.PolicyResponse.ValidationFailureAction = policy.Spec.ValidationFailureAction
}
func endResultResponse(resp *response.EngineResponse, startTime time.Time) {
resp.PolicyResponse.ProcessingTime = time.Since(startTime)
glog.V(4).Infof("Finished applying validation rules policy %v (%v)", resp.PolicyResponse.Policy, resp.PolicyResponse.ProcessingTime)
glog.V(4).Infof("Validation Rules appplied succesfully count %v for policy %q", resp.PolicyResponse.RulesAppliedCount, resp.PolicyResponse.Policy)
}
func incrementAppliedCount(resp *response.EngineResponse) {
// rules applied succesfully count
resp.PolicyResponse.RulesAppliedCount++
}
func validateResource(ctx context.EvalInterface, policy kyverno.ClusterPolicy, resource unstructured.Unstructured, admissionInfo kyverno.RequestInfo) *response.EngineResponse {
resp := &response.EngineResponse{}
for _, rule := range policy.Spec.Rules {
@ -94,6 +95,14 @@ func validateResource(ctx context.EvalInterface, policy kyverno.ClusterPolicy, r
continue
}
startTime := time.Now()
if paths := validateGeneralRuleInfoVariables(ctx, rule); len(paths) != 0 {
glog.Infof("referenced path not present in rule %s/, resource %s/%s/%s, path: %s", rule.Name, resource.GetKind(), resource.GetNamespace(), resource.GetName(), paths)
resp.PolicyResponse.Rules = append(resp.PolicyResponse.Rules,
newPathNotPresentRuleResponse(rule.Name, utils.Validation.String(), fmt.Sprintf("path not present: %s", paths)))
continue
}
if !rbac.MatchAdmissionInfo(rule, admissionInfo) {
glog.V(3).Infof("rule '%s' cannot be applied on %s/%s/%s, admission permission: %v",
rule.Name, resource.GetKind(), resource.GetNamespace(), resource.GetName(), admissionInfo)
@ -178,14 +187,23 @@ func validatePatterns(ctx context.EvalInterface, resource unstructured.Unstructu
// either pattern or anyPattern can be specified in Validation rule
if rule.Validation.Pattern != nil {
path, err := validate.ValidateResourceWithPattern(ctx, resource.Object, rule.Validation.Pattern)
if err != nil {
// rule application failed
glog.V(4).Infof("Validation rule '%s' failed at '%s' for resource %s/%s/%s. %s: %v", rule.Name, path, resource.GetKind(), resource.GetNamespace(), resource.GetName(), rule.Validation.Message, err)
resp.Success = false
resp.Message = fmt.Sprintf("Validation error: %s; Validation rule '%s' failed at path '%s'",
rule.Validation.Message, rule.Name, path)
if !reflect.DeepEqual(err, validate.ValidationError{}) {
switch err.StatusCode {
case validate.PathNotPresent:
resp.Success = true
resp.PathNotPresent = true
resp.Message = fmt.Sprintf("referenced path not present: %s", err.ErrorMsg)
glog.V(4).Infof("Skip applying rule '%s' on resource '%s/%s/%s': %s", rule.Name, resource.GetKind(), resource.GetNamespace(), resource.GetName(), resp.Message)
case validate.Rulefailure:
// rule application failed
glog.V(4).Infof("Validation rule '%s' failed at '%s' for resource %s/%s/%s. %s: %v", rule.Name, path, resource.GetKind(), resource.GetNamespace(), resource.GetName(), rule.Validation.Message, err)
resp.Success = false
resp.Message = fmt.Sprintf("Validation error: %s; Validation rule '%s' failed at path '%s'",
rule.Validation.Message, rule.Name, path)
}
return resp
}
// rule application succesful
glog.V(4).Infof("rule %s pattern validated succesfully on resource %s/%s/%s", rule.Name, resource.GetKind(), resource.GetNamespace(), resource.GetName())
resp.Success = true
@ -194,39 +212,54 @@ func validatePatterns(ctx context.EvalInterface, resource unstructured.Unstructu
}
// using anyPattern we can define multiple patterns and only one of them has to be succesfully validated
// return directly if one pattern succeed
// if none succeed, report violation / policyerror(TODO)
if rule.Validation.AnyPattern != nil {
var errs []error
var failedPaths []string
var ruleFailureErrs []error
var failedPaths, invalidPaths []string
for index, pattern := range rule.Validation.AnyPattern {
path, err := validate.ValidateResourceWithPattern(ctx, resource.Object, pattern)
if err == nil {
// this pattern was succesfully validated
// this pattern was succesfully validated
if reflect.DeepEqual(err, validate.ValidationError{}) {
glog.V(4).Infof("anyPattern %v succesfully validated on resource %s/%s/%s", pattern, resource.GetKind(), resource.GetNamespace(), resource.GetName())
resp.Success = true
resp.Message = fmt.Sprintf("Validation rule '%s' anyPattern[%d] succeeded.", rule.Name, index)
return resp
}
if err != nil {
switch err.StatusCode {
case validate.PathNotPresent:
invalidPaths = append(invalidPaths, err.ErrorMsg)
case validate.Rulefailure:
glog.V(4).Infof("Validation error: %s; Validation rule %s anyPattern[%d] failed at path %s for %s/%s/%s",
rule.Validation.Message, rule.Name, index, path, resource.GetKind(), resource.GetNamespace(), resource.GetName())
errs = append(errs, err)
ruleFailureErrs = append(ruleFailureErrs, errors.New(err.ErrorMsg))
failedPaths = append(failedPaths, path)
}
}
// If none of the anyPatterns are validated
if len(errs) > 0 {
glog.V(4).Infof("none of anyPattern were processed: %v", errs)
resp.Success = false
var errorStr []string
for index, err := range errs {
glog.V(4).Infof("anyPattern[%d] failed at path %s: %v", index, failedPaths[index], err)
str := fmt.Sprintf("Validation rule %s anyPattern[%d] failed at path %s.", rule.Name, index, failedPaths[index])
errorStr = append(errorStr, str)
}
resp.Message = fmt.Sprintf("Validation error: %s; %s", rule.Validation.Message, strings.Join(errorStr, ";"))
// PathNotPresent
if len(invalidPaths) != 0 {
resp.Success = true
resp.PathNotPresent = true
resp.Message = fmt.Sprintf("referenced path not present: %s", strings.Join(invalidPaths, ";"))
glog.V(4).Infof("Skip applying rule '%s' on resource '%s/%s/%s': %s", rule.Name, resource.GetKind(), resource.GetNamespace(), resource.GetName(), resp.Message)
return resp
}
// none of the anyPatterns succeed: len(ruleFailureErrs) > 0
glog.V(4).Infof("none of anyPattern comply with resource: %v", ruleFailureErrs)
resp.Success = false
var errorStr []string
for index, err := range ruleFailureErrs {
glog.V(4).Infof("anyPattern[%d] failed at path %s: %v", index, failedPaths[index], err)
str := fmt.Sprintf("Validation rule %s anyPattern[%d] failed at path %s.", rule.Name, index, failedPaths[index])
errorStr = append(errorStr, str)
}
resp.Message = fmt.Sprintf("Validation error: %s; %s", rule.Validation.Message, strings.Join(errorStr, " "))
return resp
}
return response.RuleResponse{}
}

View file

@ -5,6 +5,7 @@ import (
"testing"
kyverno "github.com/nirmata/kyverno/pkg/api/kyverno/v1"
"github.com/nirmata/kyverno/pkg/engine/context"
"github.com/nirmata/kyverno/pkg/engine/utils"
"gotest.tools/assert"
)
@ -483,7 +484,7 @@ func TestValidate_Fail_anyPattern(t *testing.T) {
resourceUnstructured, err := utils.ConvertToUnstructured(rawResource)
assert.NilError(t, err)
er := Validate(PolicyContext{Policy: policy, NewResource: *resourceUnstructured})
msgs := []string{"Validation error: A namespace is required; Validation rule check-default-namespace anyPattern[0] failed at path /metadata/namespace/.;Validation rule check-default-namespace anyPattern[1] failed at path /metadata/namespace/."}
msgs := []string{"Validation error: A namespace is required; Validation rule check-default-namespace anyPattern[0] failed at path /metadata/namespace/. Validation rule check-default-namespace anyPattern[1] failed at path /metadata/namespace/."}
for index, r := range er.PolicyResponse.Rules {
assert.Equal(t, r.Message, msgs[index])
}
@ -1431,3 +1432,348 @@ func TestValidate_negationAnchor_pass(t *testing.T) {
}
assert.Assert(t, er.IsSuccesful())
}
func Test_VariableSubstitutionPathNotExistInPattern(t *testing.T) {
resourceRaw := []byte(`{
"apiVersion": "v1",
"kind": "Pod",
"metadata": {
"name": "check-root-user"
},
"spec": {
"containers": [
{
"name": "check-root-user-a",
"image": "nginxinc/nginx-unprivileged",
"securityContext": {
"runAsNonRoot": true
}
}
]
}
}`)
policyraw := []byte(`{
"apiVersion": "kyverno.io/v1",
"kind": "ClusterPolicy",
"metadata": {
"name": "substitue-variable"
},
"spec": {
"rules": [
{
"name": "test-path-not-exist",
"match": {
"resources": {
"kinds": [
"Pod"
]
}
},
"validate": {
"pattern": {
"spec": {
"containers": [
{
"name": "{{request.object.metadata.name1}}*"
}
]
}
}
}
}
]
}
}`)
var policy kyverno.ClusterPolicy
json.Unmarshal(policyraw, &policy)
resourceUnstructured, err := utils.ConvertToUnstructured(resourceRaw)
assert.NilError(t, err)
ctx := context.NewContext()
ctx.AddResource(resourceRaw)
policyContext := PolicyContext{
Policy: policy,
Context: ctx,
NewResource: *resourceUnstructured}
er := Validate(policyContext)
assert.Assert(t, er.PolicyResponse.Rules[0].Success, true)
assert.Assert(t, er.PolicyResponse.Rules[0].PathNotPresent, true)
}
func Test_VariableSubstitutionPathNotExistInAnyPattern_OnePatternStatisfies(t *testing.T) {
resourceRaw := []byte(`{
"apiVersion": "v1",
"kind": "Deployment",
"metadata": {
"name": "test"
},
"spec": {
"template": {
"spec": {
"containers": [
{
"name": "test-pod",
"image": "nginxinc/nginx-unprivileged"
}
]
}
}
}
}`)
policyraw := []byte(`{
"apiVersion": "kyverno.io/v1",
"kind": "ClusterPolicy",
"metadata": {
"name": "substitue-variable"
},
"spec": {
"rules": [
{
"name": "test-path-not-exist",
"match": {
"resources": {
"kinds": [
"Deployment"
]
}
},
"validate": {
"anyPattern": [
{
"spec": {
"template": {
"spec": {
"containers": [
{
"name": "{{request.object.metadata.name1}}*"
}
]
}
}
}
},
{
"spec": {
"template": {
"spec": {
"containers": [
{
"name": "{{request.object.metadata.name}}*"
}
]
}
}
}
}
]
}
}
]
}
}`)
var policy kyverno.ClusterPolicy
assert.NilError(t, json.Unmarshal(policyraw, &policy))
resourceUnstructured, err := utils.ConvertToUnstructured(resourceRaw)
assert.NilError(t, err)
ctx := context.NewContext()
ctx.AddResource(resourceRaw)
policyContext := PolicyContext{
Policy: policy,
Context: ctx,
NewResource: *resourceUnstructured}
er := Validate(policyContext)
assert.Assert(t, er.PolicyResponse.Rules[0].Success == true)
assert.Assert(t, er.PolicyResponse.Rules[0].PathNotPresent == false)
expectMsg := "Validation rule 'test-path-not-exist' anyPattern[1] succeeded."
assert.Assert(t, er.PolicyResponse.Rules[0].Message == expectMsg)
}
func Test_VariableSubstitutionPathNotExistInAnyPattern_AllPathNotPresent(t *testing.T) {
resourceRaw := []byte(`{
"apiVersion": "v1",
"kind": "Deployment",
"metadata": {
"name": "test"
},
"spec": {
"template": {
"spec": {
"containers": [
{
"name": "test-pod",
"image": "nginxinc/nginx-unprivileged"
}
]
}
}
}
}`)
policyraw := []byte(`{
"apiVersion": "kyverno.io/v1",
"kind": "ClusterPolicy",
"metadata": {
"name": "substitue-variable"
},
"spec": {
"rules": [
{
"name": "test-path-not-exist",
"match": {
"resources": {
"kinds": [
"Deployment"
]
}
},
"validate": {
"anyPattern": [
{
"spec": {
"template": {
"spec": {
"containers": [
{
"name": "{{request.object.metadata.name1}}*"
}
]
}
}
}
},
{
"spec": {
"template": {
"spec": {
"containers": [
{
"name": "{{request.object.metadata.name2}}*"
}
]
}
}
}
}
]
}
}
]
}
}`)
var policy kyverno.ClusterPolicy
assert.NilError(t, json.Unmarshal(policyraw, &policy))
resourceUnstructured, err := utils.ConvertToUnstructured(resourceRaw)
assert.NilError(t, err)
ctx := context.NewContext()
ctx.AddResource(resourceRaw)
policyContext := PolicyContext{
Policy: policy,
Context: ctx,
NewResource: *resourceUnstructured}
er := Validate(policyContext)
assert.Assert(t, er.PolicyResponse.Rules[0].Success, true)
assert.Assert(t, er.PolicyResponse.Rules[0].PathNotPresent, true)
}
func Test_VariableSubstitutionPathNotExistInAnyPattern_AllPathPresent_NonePatternSatisfy(t *testing.T) {
resourceRaw := []byte(`{
"apiVersion": "v1",
"kind": "Deployment",
"metadata": {
"name": "test"
},
"spec": {
"template": {
"spec": {
"containers": [
{
"name": "pod-test-pod",
"image": "nginxinc/nginx-unprivileged"
}
]
}
}
}
}`)
policyraw := []byte(`{
"apiVersion": "kyverno.io/v1",
"kind": "ClusterPolicy",
"metadata": {
"name": "substitue-variable"
},
"spec": {
"rules": [
{
"name": "test-path-not-exist",
"match": {
"resources": {
"kinds": [
"Deployment"
]
}
},
"validate": {
"anyPattern": [
{
"spec": {
"template": {
"spec": {
"containers": [
{
"name": "{{request.object.metadata.name}}*"
}
]
}
}
}
},
{
"spec": {
"template": {
"spec": {
"containers": [
{
"name": "{{request.object.metadata.name}}*"
}
]
}
}
}
}
]
}
}
]
}
}`)
var policy kyverno.ClusterPolicy
assert.NilError(t, json.Unmarshal(policyraw, &policy))
resourceUnstructured, err := utils.ConvertToUnstructured(resourceRaw)
assert.NilError(t, err)
ctx := context.NewContext()
ctx.AddResource(resourceRaw)
policyContext := PolicyContext{
Policy: policy,
Context: ctx,
NewResource: *resourceUnstructured}
er := Validate(policyContext)
expectedMsg := "Validation error: ; Validation rule test-path-not-exist anyPattern[0] failed at path /spec/template/spec/containers/0/name/. Validation rule test-path-not-exist anyPattern[1] failed at path /spec/template/spec/containers/0/name/."
assert.Assert(t, er.PolicyResponse.Rules[0].Success == false)
assert.Assert(t, er.PolicyResponse.Rules[0].PathNotPresent == false)
assert.Assert(t, er.PolicyResponse.Rules[0].Message == expectedMsg)
}

View file

@ -0,0 +1,82 @@
package variables
import (
"regexp"
"strings"
"github.com/golang/glog"
"github.com/nirmata/kyverno/pkg/engine/context"
)
// ValidateVariables validates if referenced path is present
// return empty string if all paths are valid, otherwise return invalid path
func ValidateVariables(ctx context.EvalInterface, pattern interface{}) string {
var pathsNotPresent []string
variableList := extractVariables(pattern)
for i := 0; i < len(variableList)-1; i = i + 2 {
p := variableList[i+1]
glog.V(3).Infof("validating variables %s", p)
val, err := ctx.Query(p)
// reference path is not present
if err == nil && val == nil {
pathsNotPresent = append(pathsNotPresent, p)
}
}
if len(variableList) != 0 && len(pathsNotPresent) != 0 {
return strings.Join(pathsNotPresent, ";")
}
return ""
}
// extractVariables extracts variables in the pattern
func extractVariables(pattern interface{}) []string {
switch typedPattern := pattern.(type) {
case map[string]interface{}:
return extractMap(typedPattern)
case []interface{}:
return extractArray(typedPattern)
case string:
return extractValue(typedPattern)
default:
return nil
}
}
func extractMap(patternMap map[string]interface{}) []string {
var variableList []string
for _, patternElement := range patternMap {
if vars := extractVariables(patternElement); vars != nil {
variableList = append(variableList, vars...)
}
}
return variableList
}
func extractArray(patternList []interface{}) []string {
var variableList []string
for _, patternElement := range patternList {
if vars := extractVariables(patternElement); vars != nil {
variableList = append(variableList, vars...)
}
}
return variableList
}
func extractValue(valuePattern string) []string {
operatorVariable := getOperator(valuePattern)
variable := valuePattern[len(operatorVariable):]
return extractValueVariable(variable)
}
func extractValueVariable(valuePattern string) []string {
variableRegex := regexp.MustCompile(variableRegex)
groups := variableRegex.FindStringSubmatch(valuePattern)
if len(groups)%2 != 0 {
return nil
}
return groups
}

View file

@ -0,0 +1,158 @@
package variables
import (
"encoding/json"
"fmt"
"testing"
kyverno "github.com/nirmata/kyverno/pkg/api/kyverno/v1"
"github.com/nirmata/kyverno/pkg/engine/context"
"gotest.tools/assert"
authenticationv1 "k8s.io/api/authentication/v1"
)
func Test_ExtractVariables(t *testing.T) {
patternRaw := []byte(`
{
"name": "ns-owner-{{request.userInfo.username}}",
"data": {
"rules": [
{
"apiGroups": [
""
],
"resources": [
"namespaces"
],
"verbs": [
"*"
],
"resourceNames": [
"{{request.object.metadata.name}}"
]
}
]
}
}
`)
var pattern interface{}
json.Unmarshal(patternRaw, &pattern)
vars := extractVariables(pattern)
result := []string{"{{request.userInfo.username}}", "request.userInfo.username", "{{request.object.metadata.name}}", "request.object.metadata.name"}
assert.Assert(t, len(vars) == len(result), fmt.Sprintf("result does not match, var: %s", vars))
}
func Test_ValidateVariables_NoVariable(t *testing.T) {
patternRaw := []byte(`
{
"name": "ns-owner",
"data": {
"rules": [
{
"apiGroups": [
""
],
"resources": [
"namespaces"
],
"verbs": [
"*"
],
"resourceNames": [
"Pod"
]
}
]
}
}
`)
resourceRaw := []byte(`
{
"metadata": {
"name": "temp",
"namespace": "n1"
},
"spec": {
"namespace": "n1",
"name": "temp1"
}
}
`)
// userInfo
userReqInfo := kyverno.RequestInfo{
AdmissionUserInfo: authenticationv1.UserInfo{
Username: "user1",
},
}
var pattern, resource interface{}
assert.NilError(t, json.Unmarshal(patternRaw, &pattern))
assert.NilError(t, json.Unmarshal(resourceRaw, &resource))
ctx := context.NewContext()
ctx.AddResource(resourceRaw)
ctx.AddUserInfo(userReqInfo)
invalidPaths := ValidateVariables(ctx, pattern)
assert.Assert(t, len(invalidPaths) == 0)
}
func Test_ValidateVariables(t *testing.T) {
patternRaw := []byte(`
{
"name": "ns-owner-{{request.userInfo.username}}",
"data": {
"rules": [
{
"apiGroups": [
""
],
"resources": [
"namespaces"
],
"verbs": [
"*"
],
"resourceNames": [
"{{request.object.metadata.name1}}"
]
}
]
}
}
`)
resourceRaw := []byte(`
{
"metadata": {
"name": "temp",
"namespace": "n1"
},
"spec": {
"namespace": "n1",
"name": "temp1"
}
}
`)
// userInfo
userReqInfo := kyverno.RequestInfo{
AdmissionUserInfo: authenticationv1.UserInfo{
Username: "user1",
},
}
var pattern, resource interface{}
assert.NilError(t, json.Unmarshal(patternRaw, &pattern))
assert.NilError(t, json.Unmarshal(resourceRaw, &resource))
ctx := context.NewContext()
ctx.AddResource(resourceRaw)
ctx.AddUserInfo(userReqInfo)
invalidPaths := ValidateVariables(ctx, pattern)
assert.Assert(t, len(invalidPaths) > 0)
}

View file

@ -9,6 +9,8 @@ import (
"github.com/nirmata/kyverno/pkg/engine/operator"
)
const variableRegex = `\{\{([^{}]*)\}\}`
//SubstituteVariables substitutes the JMESPATH with variable substitution
// supported substitutions
// - no operator + variable(string,object)
@ -73,7 +75,7 @@ func substituteValue(ctx context.EvalInterface, valuePattern string) interface{}
func getValueQuery(ctx context.EvalInterface, valuePattern string) interface{} {
var emptyInterface interface{}
// extract variable {{<variable>}}
validRegex := regexp.MustCompile(`\{\{([^{}]*)\}\}`)
validRegex := regexp.MustCompile(variableRegex)
groups := validRegex.FindAllStringSubmatch(valuePattern, -1)
// can have multiple variables in a single value pattern
// var Map <variable,value>

View file

@ -49,7 +49,7 @@ func checkValue(valuePattern string, variables []string, path string) error {
}
func checkValueVariable(valuePattern string, variables []string) bool {
variableRegex := regexp.MustCompile("^{{(.*)}}$")
variableRegex := regexp.MustCompile(variableRegex)
groups := variableRegex.FindStringSubmatch(valuePattern)
if len(groups) < 2 {
return false

View file

@ -1,7 +1,9 @@
package generate
import (
"errors"
"fmt"
"reflect"
"github.com/golang/glog"
kyverno "github.com/nirmata/kyverno/pkg/api/kyverno/v1"
@ -11,7 +13,7 @@ import (
"github.com/nirmata/kyverno/pkg/engine/validate"
"github.com/nirmata/kyverno/pkg/engine/variables"
"github.com/nirmata/kyverno/pkg/policyviolation"
"k8s.io/apimachinery/pkg/api/errors"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
)
@ -83,6 +85,13 @@ func (c *Controller) applyGenerate(resource unstructured.Unstructured, gr kyvern
return nil, fmt.Errorf("policy %s, dont not apply to resource %v", gr.Spec.Policy, gr.Spec.Resource)
}
if pv := buildPathNotPresentPV(engineResponse); pv != nil {
c.pvGenerator.Add(pv...)
// variable substitiution fails in ruleInfo (match,exclude,condition)
// the overall policy should not apply to resource
return nil, fmt.Errorf("referenced path not present in generate policy %s", policy.Name)
}
// Apply the generate rule on resource
return applyGeneratePolicy(c.client, policyContext, gr.Status.State)
}
@ -130,6 +139,10 @@ func applyRule(client *dclient.Client, rule kyverno.Rule, resource unstructured.
var err error
var noGenResource kyverno.ResourceSpec
if invalidPaths := variables.ValidateVariables(ctx, rule.Generation.ResourceSpec); len(invalidPaths) != 0 {
return noGenResource, NewViolation(rule.Name, fmt.Errorf("path not present in generate resource spec: %s", invalidPaths))
}
// variable substitution
// - name
// - namespace
@ -165,7 +178,7 @@ func applyRule(client *dclient.Client, rule kyverno.Rule, resource unstructured.
}
// CLONE
if gen.Clone != (kyverno.CloneFrom{}) {
if rdata, err = handleClone(gen, client, resource, ctx, state); err != nil {
if rdata, err = handleClone(rule.Name, gen, client, resource, ctx, state); err != nil {
glog.V(4).Info(err)
switch e := err.(type) {
case *NotFound:
@ -238,12 +251,16 @@ func variableSubsitutionForAttributes(gen kyverno.Generation, ctx context.EvalIn
}
func handleData(ruleName string, generateRule kyverno.Generation, client *dclient.Client, resource unstructured.Unstructured, ctx context.EvalInterface, state kyverno.GenerateRequestState) (map[string]interface{}, error) {
if invalidPaths := variables.ValidateVariables(ctx, generateRule.Data); len(invalidPaths) != 0 {
return nil, NewViolation(ruleName, fmt.Errorf("path not present in generate data: %s", invalidPaths))
}
newData := variables.SubstituteVariables(ctx, generateRule.Data)
// check if resource exists
obj, err := client.GetResource(generateRule.Kind, generateRule.Namespace, generateRule.Name)
glog.V(4).Info(err)
if errors.IsNotFound(err) {
if apierrors.IsNotFound(err) {
glog.V(4).Info(string(state))
// Resource does not exist
if state == "" {
@ -279,21 +296,25 @@ func handleData(ruleName string, generateRule kyverno.Generation, client *dclien
return nil, nil
}
func handleClone(generateRule kyverno.Generation, client *dclient.Client, resource unstructured.Unstructured, ctx context.EvalInterface, state kyverno.GenerateRequestState) (map[string]interface{}, error) {
func handleClone(ruleName string, generateRule kyverno.Generation, client *dclient.Client, resource unstructured.Unstructured, ctx context.EvalInterface, state kyverno.GenerateRequestState) (map[string]interface{}, error) {
if invalidPaths := variables.ValidateVariables(ctx, generateRule.Clone); len(invalidPaths) != 0 {
return nil, NewViolation(ruleName, fmt.Errorf("path not present in generate clone: %s", invalidPaths))
}
// check if resource exists
_, err := client.GetResource(generateRule.Kind, generateRule.Namespace, generateRule.Name)
if err == nil {
// resource exists
return nil, nil
}
if !errors.IsNotFound(err) {
if !apierrors.IsNotFound(err) {
//something wrong while fetching resource
return nil, err
}
// get reference clone resource
obj, err := client.GetResource(generateRule.Kind, generateRule.Clone.Namespace, generateRule.Clone.Name)
if errors.IsNotFound(err) {
if apierrors.IsNotFound(err) {
return nil, NewNotFound(generateRule.Kind, generateRule.Clone.Namespace, generateRule.Clone.Name)
}
if err != nil {
@ -306,9 +327,9 @@ func handleClone(generateRule kyverno.Generation, client *dclient.Client, resour
func checkResource(ctx context.EvalInterface, newResourceSpec interface{}, resource *unstructured.Unstructured) (bool, error) {
// check if the resource spec if a subset of the resource
path, err := validate.ValidateResourceWithPattern(ctx, resource.Object, newResourceSpec)
if err != nil {
if !reflect.DeepEqual(err, validate.ValidationError{}) {
glog.V(4).Infof("config not a subset of resource. failed at path %s: %v", path, err)
return false, err
return false, errors.New(err.ErrorMsg)
}
return true, nil
}
@ -316,7 +337,6 @@ func checkResource(ctx context.EvalInterface, newResourceSpec interface{}, resou
func generatePV(gr kyverno.GenerateRequest, resource unstructured.Unstructured, err *Violation) policyviolation.Info {
info := policyviolation.Info{
Blocked: false,
PolicyName: gr.Spec.Policy,
Resource: resource,
Rules: []kyverno.ViolatedRule{kyverno.ViolatedRule{

View file

@ -5,7 +5,9 @@ import (
"github.com/golang/glog"
kyverno "github.com/nirmata/kyverno/pkg/api/kyverno/v1"
"github.com/nirmata/kyverno/pkg/engine/response"
"github.com/nirmata/kyverno/pkg/event"
"github.com/nirmata/kyverno/pkg/policyviolation"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
@ -108,3 +110,13 @@ func successEvents(gr kyverno.GenerateRequest, resource unstructured.Unstructure
return events
}
// buildPathNotPresentPV build violation info when referenced path not found
func buildPathNotPresentPV(er response.EngineResponse) []policyviolation.Info {
for _, rr := range er.PolicyResponse.Rules {
if rr.PathNotPresent {
return policyviolation.GeneratePVsFromEngineResponse([]response.EngineResponse{er})
}
}
return nil
}

View file

@ -4,7 +4,6 @@ import (
"fmt"
"github.com/golang/glog"
kyverno "github.com/nirmata/kyverno/pkg/api/kyverno/v1"
"github.com/nirmata/kyverno/pkg/engine/response"
"github.com/nirmata/kyverno/pkg/event"
"github.com/nirmata/kyverno/pkg/policyviolation"
@ -15,55 +14,10 @@ func (nsc *NamespaceController) report(engineResponses []response.EngineResponse
eventInfos := generateEvents(engineResponses)
nsc.eventGen.Add(eventInfos...)
// generate policy violations
pvInfos := generatePVs(engineResponses)
pvInfos := policyviolation.GeneratePVsFromEngineResponse(engineResponses)
nsc.pvGenerator.Add(pvInfos...)
}
func generatePVs(ers []response.EngineResponse) []policyviolation.Info {
var pvInfos []policyviolation.Info
for _, er := range ers {
// ignore creation of PV for resoruces that are yet to be assigned a name
if er.PolicyResponse.Resource.Name == "" {
glog.V(4).Infof("resource %v, has not been assigned a name, not creating a policy violation for it", er.PolicyResponse.Resource)
continue
}
if er.IsSuccesful() {
continue
}
glog.V(4).Infof("Building policy violation for engine response %v", er)
// build policy violation info
pvInfos = append(pvInfos, buildPVInfo(er))
}
return pvInfos
}
func buildPVInfo(er response.EngineResponse) policyviolation.Info {
info := policyviolation.Info{
Blocked: false,
PolicyName: er.PolicyResponse.Policy,
Resource: er.PatchedResource,
Rules: buildViolatedRules(er),
}
return info
}
func buildViolatedRules(er response.EngineResponse) []kyverno.ViolatedRule {
var violatedRules []kyverno.ViolatedRule
for _, rule := range er.PolicyResponse.Rules {
if rule.Success {
continue
}
vrule := kyverno.ViolatedRule{
Name: rule.Name,
Type: rule.Type,
Message: rule.Message,
}
violatedRules = append(violatedRules, vrule)
}
return violatedRules
}
func generateEvents(ers []response.EngineResponse) []event.Info {
var eventInfos []event.Info
for _, er := range ers {

View file

@ -7,84 +7,52 @@ import (
"github.com/golang/glog"
kyverno "github.com/nirmata/kyverno/pkg/api/kyverno/v1"
kyvernolister "github.com/nirmata/kyverno/pkg/client/listers/kyverno/v1"
dclient "github.com/nirmata/kyverno/pkg/dclient"
"github.com/nirmata/kyverno/pkg/engine/response"
"github.com/nirmata/kyverno/pkg/policyviolation"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
)
func (pc *PolicyController) cleanUpPolicyViolation(pResponse response.PolicyResponse) {
// 1- check if there is violation on resource (label:Selector)
// 2- check if there is violation on owner
// - recursively get owner by queries the api server for owner information of the resource
// there can be multiple violations as a resource can have multiple owners
// - check if there is violation on resource (label:Selector)
if pResponse.Resource.Namespace == "" {
pvs, err := getClusterPVs(pc.cpvLister, pc.client, pResponse.Policy, pResponse.Resource.Kind, pResponse.Resource.Name)
pv, err := getClusterPV(pc.cpvLister, pResponse.Policy, pResponse.Resource.Kind, pResponse.Resource.Name)
if err != nil {
glog.Errorf("failed to cleanUp violations: %v", err)
return
}
for _, pv := range pvs {
if reflect.DeepEqual(pv, kyverno.ClusterPolicyViolation{}) {
continue
}
glog.V(4).Infof("cleanup cluster violation %s on %s", pv.Name, pv.Spec.ResourceSpec.ToKey())
if err := pc.pvControl.DeleteClusterPolicyViolation(pv.Name); err != nil {
glog.Errorf("failed to delete cluster policy violation %s on %s: %v", pv.Name, pv.Spec.ResourceSpec.ToKey(), err)
continue
}
if reflect.DeepEqual(pv, kyverno.ClusterPolicyViolation{}) {
return
}
glog.V(4).Infof("cleanup cluster violation %s on %s", pv.Name, pv.Spec.ResourceSpec.ToKey())
if err := pc.pvControl.DeleteClusterPolicyViolation(pv.Name); err != nil {
glog.Errorf("failed to delete cluster policy violation %s on %s: %v", pv.Name, pv.Spec.ResourceSpec.ToKey(), err)
}
return
}
nspvs, err := getNamespacedPVs(pc.nspvLister, pc.client, pResponse.Policy, pResponse.Resource.Kind, pResponse.Resource.Namespace, pResponse.Resource.Name)
// namespace policy violation
nspv, err := getNamespacedPV(pc.nspvLister, pResponse.Policy, pResponse.Resource.Kind, pResponse.Resource.Namespace, pResponse.Resource.Name)
if err != nil {
glog.Error(err)
return
}
for _, pv := range nspvs {
if reflect.DeepEqual(pv, kyverno.PolicyViolation{}) {
continue
}
glog.V(4).Infof("cleanup namespaced violation %s on %s.%s", pv.Name, pResponse.Resource.Namespace, pv.Spec.ResourceSpec.ToKey())
if err := pc.pvControl.DeleteNamespacedPolicyViolation(pv.Namespace, pv.Name); err != nil {
glog.Errorf("failed to delete namespaced policy violation %s on %s: %v", pv.Name, pv.Spec.ResourceSpec.ToKey(), err)
continue
}
if reflect.DeepEqual(nspv, kyverno.PolicyViolation{}) {
return
}
glog.V(4).Infof("cleanup namespaced violation %s on %s.%s", nspv.Name, pResponse.Resource.Namespace, nspv.Spec.ResourceSpec.ToKey())
if err := pc.pvControl.DeleteNamespacedPolicyViolation(nspv.Namespace, nspv.Name); err != nil {
glog.Errorf("failed to delete namespaced policy violation %s on %s: %v", nspv.Name, nspv.Spec.ResourceSpec.ToKey(), err)
}
}
func getClusterPVs(pvLister kyvernolister.ClusterPolicyViolationLister, client *dclient.Client, policyName, kind, name string) ([]kyverno.ClusterPolicyViolation, error) {
var pvs []kyverno.ClusterPolicyViolation
// Wont do the claiming of objects, just lookup based on selectors
func getClusterPV(pvLister kyvernolister.ClusterPolicyViolationLister, policyName, rkind, rname string) (kyverno.ClusterPolicyViolation, error) {
var err error
// Check Violation on resource
pv, err := getClusterPVOnResource(pvLister, policyName, kind, name)
if err != nil {
glog.V(4).Infof("error while fetching violation on existing resource: %v", err)
return nil, err
}
if !reflect.DeepEqual(pv, kyverno.ClusterPolicyViolation{}) {
// found a violation on resource
pvs = append(pvs, pv)
return pvs, nil
}
// Check Violations on owner
pvs, err = getClusterPVonOwnerRef(pvLister, client, policyName, kind, name)
if err != nil {
glog.V(4).Infof("error while fetching pv: %v", err)
return nil, err
}
return pvs, nil
}
// Wont do the claiming of objects, just lookup based on selectors and owner references
func getClusterPVOnResource(pvLister kyvernolister.ClusterPolicyViolationLister, policyName, kind, name string) (kyverno.ClusterPolicyViolation, error) {
pvs, err := pvLister.List(labels.Everything())
if err != nil {
glog.V(2).Infof("unable to list policy violations : %v", err)
@ -94,65 +62,16 @@ func getClusterPVOnResource(pvLister kyvernolister.ClusterPolicyViolationLister,
for _, pv := range pvs {
// find a policy on same resource and policy combination
if pv.Spec.Policy == policyName &&
pv.Spec.ResourceSpec.Kind == kind &&
pv.Spec.ResourceSpec.Name == name {
pv.Spec.ResourceSpec.Kind == rkind &&
pv.Spec.ResourceSpec.Name == rname {
return *pv, nil
}
}
return kyverno.ClusterPolicyViolation{}, nil
}
func getClusterPVonOwnerRef(pvLister kyvernolister.ClusterPolicyViolationLister, dclient *dclient.Client, policyName, kind, name string) ([]kyverno.ClusterPolicyViolation, error) {
var pvs []kyverno.ClusterPolicyViolation
// get resource
resource, err := dclient.GetResource(kind, "", name)
if err != nil {
glog.V(4).Infof("error while fetching the resource: %v", err)
return pvs, fmt.Errorf("error while fetching the resource: %v", err)
}
// getOwners returns nil if there is any error
owners := map[kyverno.ResourceSpec]interface{}{}
policyviolation.GetOwner(dclient, owners, *resource)
// as we can have multiple top level owners to a resource
// check if pv exists on each one
for owner := range owners {
pv, err := getClusterPVOnResource(pvLister, policyName, owner.Kind, owner.Name)
if err != nil {
glog.Errorf("error while fetching resource owners: %v", err)
continue
}
pvs = append(pvs, pv)
}
return pvs, nil
}
func getNamespacedPVs(nspvLister kyvernolister.PolicyViolationLister, client *dclient.Client, policyName, kind, namespace, name string) ([]kyverno.PolicyViolation, error) {
var pvs []kyverno.PolicyViolation
var err error
pv, err := getNamespacedPVOnResource(nspvLister, policyName, kind, namespace, name)
if err != nil {
glog.V(4).Infof("error while fetching violation on existing resource: %v", err)
return nil, err
}
if !reflect.DeepEqual(pv, kyverno.PolicyViolation{}) {
// found a violation on resource
pvs = append(pvs, pv)
return pvs, nil
}
// Check Violations on owner
pvs, err = getNamespacedPVonOwnerRef(nspvLister, client, policyName, kind, namespace, name)
if err != nil {
glog.V(4).Infof("error while fetching pv: %v", err)
return nil, err
}
return pvs, nil
}
func getNamespacedPVOnResource(nspvLister kyvernolister.PolicyViolationLister, policyName, kind, namespace, name string) (kyverno.PolicyViolation, error) {
nspvs, err := nspvLister.PolicyViolations(namespace).List(labels.Everything())
func getNamespacedPV(nspvLister kyvernolister.PolicyViolationLister, policyName, rkind, rnamespace, rname string) (kyverno.PolicyViolation, error) {
nspvs, err := nspvLister.PolicyViolations(rnamespace).List(labels.Everything())
if err != nil {
glog.V(2).Infof("failed to list namespaced pv: %v", err)
return kyverno.PolicyViolation{}, fmt.Errorf("failed to list namespaced pv: %v", err)
@ -161,39 +80,15 @@ func getNamespacedPVOnResource(nspvLister kyvernolister.PolicyViolationLister, p
for _, nspv := range nspvs {
// find a policy on same resource and policy combination
if nspv.Spec.Policy == policyName &&
nspv.Spec.ResourceSpec.Kind == kind &&
nspv.Spec.ResourceSpec.Name == name {
nspv.Spec.ResourceSpec.Kind == rkind &&
nspv.Spec.ResourceSpec.Name == rname {
return *nspv, nil
}
}
return kyverno.PolicyViolation{}, nil
}
func getNamespacedPVonOwnerRef(nspvLister kyvernolister.PolicyViolationLister, dclient *dclient.Client, policyName, kind, namespace, name string) ([]kyverno.PolicyViolation, error) {
var pvs []kyverno.PolicyViolation
// get resource
resource, err := dclient.GetResource(kind, namespace, name)
if err != nil {
glog.V(4).Infof("error while fetching the resource: %v", err)
return pvs, err
}
// getOwners returns nil if there is any error
owners := map[kyverno.ResourceSpec]interface{}{}
policyviolation.GetOwner(dclient, owners, *resource)
// as we can have multiple top level owners to a resource
// check if pv exists on each one
for owner := range owners {
pv, err := getNamespacedPVOnResource(nspvLister, policyName, owner.Kind, namespace, owner.Name)
if err != nil {
glog.Errorf("error while fetching resource owners: %v", err)
continue
}
pvs = append(pvs, pv)
}
return pvs, nil
}
func converLabelToSelector(labelMap map[string]string) (labels.Selector, error) {
ls := &metav1.LabelSelector{}
err := metav1.Convert_Map_string_To_string_To_v1_LabelSelector(&labelMap, ls, nil)

View file

@ -4,7 +4,6 @@ import (
"fmt"
"github.com/golang/glog"
kyverno "github.com/nirmata/kyverno/pkg/api/kyverno/v1"
"github.com/nirmata/kyverno/pkg/engine/response"
"github.com/nirmata/kyverno/pkg/event"
"github.com/nirmata/kyverno/pkg/policyviolation"
@ -12,13 +11,13 @@ import (
// for each policy-resource response
// - has violation -> report
// - no violation -> cleanup policy violations(resource or resource owner)
// - no violation -> cleanup policy violations
func (pc *PolicyController) cleanupAndReport(engineResponses []response.EngineResponse) {
// generate Events
eventInfos := generateEvents(engineResponses)
pc.eventGen.Add(eventInfos...)
// create policy violation
pvInfos := generatePVs(engineResponses)
pvInfos := policyviolation.GeneratePVsFromEngineResponse(engineResponses)
pc.pvGenerator.Add(pvInfos...)
// cleanup existing violations if any
// if there is any error in clean up, we dont re-queue the resource
@ -39,51 +38,6 @@ func (pc *PolicyController) cleanUp(ers []response.EngineResponse) {
}
}
func generatePVs(ers []response.EngineResponse) []policyviolation.Info {
var pvInfos []policyviolation.Info
for _, er := range ers {
// ignore creation of PV for resoruces that are yet to be assigned a name
if er.PolicyResponse.Resource.Name == "" {
glog.V(4).Infof("resource %v, has not been assigned a name, not creating a policy violation for it", er.PolicyResponse.Resource)
continue
}
if er.IsSuccesful() {
continue
}
glog.V(4).Infof("Building policy violation for engine response %v", er)
// build policy violation info
pvInfos = append(pvInfos, buildPVInfo(er))
}
return pvInfos
}
func buildPVInfo(er response.EngineResponse) policyviolation.Info {
info := policyviolation.Info{
Blocked: false,
PolicyName: er.PolicyResponse.Policy,
Resource: er.PatchedResource,
Rules: buildViolatedRules(er),
}
return info
}
func buildViolatedRules(er response.EngineResponse) []kyverno.ViolatedRule {
var violatedRules []kyverno.ViolatedRule
for _, rule := range er.PolicyResponse.Rules {
if rule.Success {
continue
}
vrule := kyverno.ViolatedRule{
Name: rule.Name,
Type: rule.Type,
Message: rule.Message,
}
violatedRules = append(violatedRules, vrule)
}
return violatedRules
}
func generateEvents(ers []response.EngineResponse) []event.Info {
var eventInfos []event.Info
for _, er := range ers {

View file

@ -3,55 +3,46 @@ package policyviolation
import (
"fmt"
"github.com/golang/glog"
kyverno "github.com/nirmata/kyverno/pkg/api/kyverno/v1"
client "github.com/nirmata/kyverno/pkg/dclient"
"github.com/nirmata/kyverno/pkg/engine/response"
)
func GeneratePVsFromEngineResponse(ers []response.EngineResponse) (pvInfos []Info) {
for _, er := range ers {
// ignore creation of PV for resoruces that are yet to be assigned a name
if er.PolicyResponse.Resource.Name == "" {
glog.V(4).Infof("resource %v, has not been assigned a name, not creating a policy violation for it", er.PolicyResponse.Resource)
continue
}
// skip when response succeed AND referenced paths exist
if er.IsSuccesful() && !er.IsPathNotPresent() {
continue
}
glog.V(4).Infof("Building policy violation for engine response %v", er)
// build policy violation info
pvInfos = append(pvInfos, buildPVInfo(er))
}
return pvInfos
}
// Builder builds Policy Violation struct
// this is base type of namespaced and cluster policy violation
type Builder interface {
generate(info Info) []kyverno.PolicyViolationTemplate
generate(info Info) kyverno.PolicyViolationTemplate
build(policy, kind, namespace, name string, rules []kyverno.ViolatedRule) *kyverno.PolicyViolationTemplate
}
type pvBuilder struct {
// dynamic client
dclient *client.Client
type pvBuilder struct{}
func newPvBuilder() *pvBuilder {
return &pvBuilder{}
}
func newPvBuilder(dclient *client.Client) *pvBuilder {
pvb := pvBuilder{
dclient: dclient,
}
return &pvb
}
func (pvb *pvBuilder) generate(info Info) []kyverno.PolicyViolationTemplate {
var owners []kyverno.ResourceSpec
// get the owners if the resource is blocked or
// TODO: https://github.com/nirmata/kyverno/issues/535
if info.Blocked {
// get resource owners
owners = GetOwners(pvb.dclient, info.Resource)
}
pvs := pvb.buildPolicyViolations(owners, info)
return pvs
}
func (pvb *pvBuilder) buildPolicyViolations(owners []kyverno.ResourceSpec, info Info) []kyverno.PolicyViolationTemplate {
var pvs []kyverno.PolicyViolationTemplate
if len(owners) != 0 {
// there are resource owners
// generate PV on them
for _, resource := range owners {
pv := pvb.build(info.PolicyName, resource.Kind, resource.Namespace, resource.Name, info.Rules)
pvs = append(pvs, *pv)
}
} else {
// generate PV on resource
pv := pvb.build(info.PolicyName, info.Resource.GetKind(), info.Resource.GetNamespace(), info.Resource.GetName(), info.Rules)
pvs = append(pvs, *pv)
}
return pvs
func (pvb *pvBuilder) generate(info Info) kyverno.PolicyViolationTemplate {
pv := pvb.build(info.PolicyName, info.Resource.GetKind(), info.Resource.GetNamespace(), info.Resource.GetName(), info.Rules)
return *pv
}
func (pvb *pvBuilder) build(policy, kind, namespace, name string, rules []kyverno.ViolatedRule) *kyverno.PolicyViolationTemplate {
@ -77,3 +68,28 @@ func (pvb *pvBuilder) build(policy, kind, namespace, name string, rules []kyvern
pv.SetGenerateName(fmt.Sprintf("%s-", policy))
return pv
}
func buildPVInfo(er response.EngineResponse) Info {
info := Info{
PolicyName: er.PolicyResponse.Policy,
Resource: er.PatchedResource,
Rules: buildViolatedRules(er),
}
return info
}
func buildViolatedRules(er response.EngineResponse) []kyverno.ViolatedRule {
var violatedRules []kyverno.ViolatedRule
for _, rule := range er.PolicyResponse.Rules {
if rule.Success && !rule.PathNotPresent {
continue
}
vrule := kyverno.ViolatedRule{
Name: rule.Name,
Type: rule.Type,
Message: rule.Message,
}
violatedRules = append(violatedRules, vrule)
}
return violatedRules
}

View file

@ -0,0 +1,60 @@
package policyviolation
import (
"testing"
"github.com/nirmata/kyverno/pkg/engine/response"
"gotest.tools/assert"
)
func Test_GeneratePVsFromEngineResponse_PathNotExist(t *testing.T) {
ers := []response.EngineResponse{
response.EngineResponse{
PolicyResponse: response.PolicyResponse{
Policy: "test-substitue-variable",
Resource: response.ResourceSpec{
Kind: "Pod",
Name: "test",
Namespace: "test",
},
Rules: []response.RuleResponse{
response.RuleResponse{
Name: "test-path-not-exist",
Type: "Mutation",
Message: "referenced paths are not present: request.object.metadata.name1",
Success: true,
PathNotPresent: true,
},
response.RuleResponse{
Name: "test-path-exist",
Type: "Mutation",
Success: true,
PathNotPresent: false,
},
},
},
},
response.EngineResponse{
PolicyResponse: response.PolicyResponse{
Policy: "test-substitue-variable2",
Resource: response.ResourceSpec{
Kind: "Pod",
Name: "test",
Namespace: "test",
},
Rules: []response.RuleResponse{
response.RuleResponse{
Name: "test-path-not-exist-accross-policy",
Type: "Mutation",
Message: "referenced paths are not present: request.object.metadata.name1",
Success: true,
PathNotPresent: true,
},
},
},
},
}
pvInfos := GeneratePVsFromEngineResponse(ers)
assert.Assert(t, len(pvInfos) == 2)
}

View file

@ -56,50 +56,6 @@ func retryGetResource(client *client.Client, rspec kyverno.ResourceSpec) (*unstr
return obj, nil
}
// GetOwners returns a list of owners
func GetOwners(dclient *client.Client, resource unstructured.Unstructured) []kyverno.ResourceSpec {
ownerMap := map[kyverno.ResourceSpec]interface{}{}
GetOwner(dclient, ownerMap, resource)
var owners []kyverno.ResourceSpec
for owner := range ownerMap {
owners = append(owners, owner)
}
return owners
}
// GetOwner of a resource by iterating over ownerReferences
func GetOwner(dclient *client.Client, ownerMap map[kyverno.ResourceSpec]interface{}, resource unstructured.Unstructured) {
var emptyInterface interface{}
resourceSpec := kyverno.ResourceSpec{
Kind: resource.GetKind(),
Namespace: resource.GetNamespace(),
Name: resource.GetName(),
}
if _, ok := ownerMap[resourceSpec]; ok {
// owner seen before
// breaking loop
return
}
rOwners := resource.GetOwnerReferences()
// if there are no resource owners then its top level resource
if len(rOwners) == 0 {
// add resource to map
ownerMap[resourceSpec] = emptyInterface
return
}
for _, rOwner := range rOwners {
// lookup resource via client
// owner has to be in same namespace
owner, err := dclient.GetResource(rOwner.Kind, resource.GetNamespace(), rOwner.Name)
if err != nil {
glog.Errorf("Failed to get resource owner for %s/%s/%s, err: %v", rOwner.Kind, resource.GetNamespace(), rOwner.Name, err)
// as we want to process other owners
continue
}
GetOwner(dclient, ownerMap, *owner)
}
}
func converLabelToSelector(labelMap map[string]string) (labels.Selector, error) {
ls := &metav1.LabelSelector{}
err := metav1.Convert_Map_string_To_string_To_v1_LabelSelector(&labelMap, ls, nil)

View file

@ -76,7 +76,6 @@ func (ds *dataStore) delete(keyHash string) {
//Info is a request to create PV
type Info struct {
Blocked bool
PolicyName string
Resource unstructured.Unstructured
Rules []kyverno.ViolatedRule
@ -84,7 +83,6 @@ type Info struct {
func (i Info) toKey() string {
keys := []string{
strconv.FormatBool(i.Blocked),
i.PolicyName,
i.Resource.GetKind(),
i.Resource.GetNamespace(),
@ -219,7 +217,7 @@ func (gen *Generator) syncHandler(info Info) error {
glog.V(4).Infof("recieved info:%v", info)
var handler pvGenerator
var builder Builder
builder = newPvBuilder(gen.dclient)
builder = newPvBuilder()
if info.Resource.GetNamespace() == "" {
// cluster scope resource generate a clusterpolicy violation
handler = newClusterPV(gen.dclient, gen.cpvLister, gen.kyvernoInterface)
@ -229,20 +227,17 @@ func (gen *Generator) syncHandler(info Info) error {
}
failure := false
// Generate Policy Violations
// as there can be multiple owners we can have multiple violations
pvs := builder.generate(info)
for _, pv := range pvs {
// Create Policy Violations
glog.V(3).Infof("Creating policy violation: %s", info.toKey())
err := handler.create(pv)
if err != nil {
failure = true
glog.V(3).Infof("Failed to create policy violation: %v", err)
} else {
glog.V(3).Infof("Policy violation created: %s", info.toKey())
}
pv := builder.generate(info)
// Create Policy Violations
glog.V(3).Infof("Creating policy violation: %s", info.toKey())
if err := handler.create(pv); err != nil {
failure = true
glog.V(3).Infof("Failed to create policy violation: %v", err)
} else {
glog.V(3).Infof("Policy violation created: %s", info.toKey())
}
if failure {
// even if there is a single failure we requeue the request
return errors.New("Failed to process some policy violations, re-queuing")

View file

@ -27,13 +27,13 @@ type TlsPemPair struct {
PrivateKey []byte
}
//TlsGeneratePrivateKey Generates RSA private key
func TlsGeneratePrivateKey() (*rsa.PrivateKey, error) {
//TLSGeneratePrivateKey Generates RSA private key
func TLSGeneratePrivateKey() (*rsa.PrivateKey, error) {
return rsa.GenerateKey(rand.Reader, 2048)
}
//TlsPrivateKeyToPem Creates PEM block from private key object
func TlsPrivateKeyToPem(rsaKey *rsa.PrivateKey) []byte {
//TLSPrivateKeyToPem Creates PEM block from private key object
func TLSPrivateKeyToPem(rsaKey *rsa.PrivateKey) []byte {
privateKey := &pem.Block{
Type: "PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(rsaKey),
@ -43,7 +43,7 @@ func TlsPrivateKeyToPem(rsaKey *rsa.PrivateKey) []byte {
}
//TlsCertificateRequestToPem Creates PEM block from raw certificate request
func TlsCertificateRequestToPem(csrRaw []byte) []byte {
func certificateRequestToPem(csrRaw []byte) []byte {
csrBlock := &pem.Block{
Type: "CERTIFICATE REQUEST",
Bytes: csrRaw,
@ -52,26 +52,30 @@ func TlsCertificateRequestToPem(csrRaw []byte) []byte {
return pem.EncodeToMemory(csrBlock)
}
//TlsCertificateGenerateRequest Generates raw certificate signing request
func TlsCertificateGenerateRequest(privateKey *rsa.PrivateKey, props TlsCertificateProps) (*certificates.CertificateSigningRequest, error) {
//CertificateGenerateRequest Generates raw certificate signing request
func CertificateGenerateRequest(privateKey *rsa.PrivateKey, props TlsCertificateProps, fqdncn bool) (*certificates.CertificateSigningRequest, error) {
dnsNames := make([]string, 3)
dnsNames[0] = props.Service
dnsNames[1] = props.Service + "." + props.Namespace
// The full service name is the CommonName for the certificate
commonName := GenerateInClusterServiceName(props)
dnsNames[2] = commonName
csCommonName := props.Service
if fqdncn {
// use FQDN as CommonName as a workaournd for https://github.com/nirmata/kyverno/issues/542
csCommonName = commonName
}
var ips []net.IP
apiServerIp := net.ParseIP(props.ApiServerHost)
if apiServerIp != nil {
ips = append(ips, apiServerIp)
apiServerIP := net.ParseIP(props.ApiServerHost)
if apiServerIP != nil {
ips = append(ips, apiServerIP)
} else {
dnsNames = append(dnsNames, props.ApiServerHost)
}
csrTemplate := x509.CertificateRequest{
Subject: pkix.Name{
CommonName: props.Service, //commonName,
CommonName: csCommonName,
},
SignatureAlgorithm: x509.SHA256WithRSA,
DNSNames: dnsNames,
@ -92,7 +96,7 @@ func TlsCertificateGenerateRequest(privateKey *rsa.PrivateKey, props TlsCertific
Name: props.Service + "." + props.Namespace + ".cert-request",
},
Spec: certificates.CertificateSigningRequestSpec{
Request: TlsCertificateRequestToPem(csrBytes),
Request: certificateRequestToPem(csrBytes),
Groups: []string{"system:masters", "system:authenticated"},
Usages: []certificates.KeyUsage{
certificates.UsageDigitalSignature,
@ -110,7 +114,7 @@ func GenerateInClusterServiceName(props TlsCertificateProps) string {
}
//TlsCertificateGetExpirationDate Gets NotAfter property from raw certificate
func TlsCertificateGetExpirationDate(certData []byte) (*time.Time, error) {
func tlsCertificateGetExpirationDate(certData []byte) (*time.Time, error) {
block, _ := pem.Decode(certData)
if block == nil {
return nil, errors.New("Failed to decode PEM")
@ -127,13 +131,13 @@ func TlsCertificateGetExpirationDate(certData []byte) (*time.Time, error) {
// an expired certificate in a controller that has been running for a long time
const timeReserveBeforeCertificateExpiration time.Duration = time.Hour * 24 * 30 * 6 // About half a year
//IsTlsPairShouldBeUpdated checks if TLS pair has expited and needs to be updated
func IsTlsPairShouldBeUpdated(tlsPair *TlsPemPair) bool {
//IsTLSPairShouldBeUpdated checks if TLS pair has expited and needs to be updated
func IsTLSPairShouldBeUpdated(tlsPair *TlsPemPair) bool {
if tlsPair == nil {
return true
}
expirationDate, err := TlsCertificateGetExpirationDate(tlsPair.Certificate)
expirationDate, err := tlsCertificateGetExpirationDate(tlsPair.Certificate)
if err != nil {
return true
}

View file

@ -26,7 +26,7 @@ func isResponseSuccesful(engineReponses []response.EngineResponse) bool {
// returns false -> if all the policies are meant to report only, we dont block resource request
func toBlockResource(engineReponses []response.EngineResponse) bool {
for _, er := range engineReponses {
if er.PolicyResponse.ValidationFailureAction == Enforce {
if !er.IsSuccesful() && er.PolicyResponse.ValidationFailureAction == Enforce {
glog.V(4).Infof("ValidationFailureAction set to enforce for policy %s , blocking resource request ", er.PolicyResponse.Policy)
return true
}
@ -43,7 +43,7 @@ func getErrorMsg(engineReponses []response.EngineResponse) string {
if !er.IsSuccesful() {
// resource in engineReponses is identical as this was called per admission request
resourceInfo = fmt.Sprintf("%s/%s/%s", er.PolicyResponse.Resource.Kind, er.PolicyResponse.Resource.Namespace, er.PolicyResponse.Resource.Name)
str = append(str, fmt.Sprintf("failed policy %s", er.PolicyResponse.Policy))
str = append(str, fmt.Sprintf("failed policy %s:", er.PolicyResponse.Policy))
for _, rule := range er.PolicyResponse.Rules {
if !rule.Success {
str = append(str, rule.ToString())
@ -51,7 +51,7 @@ func getErrorMsg(engineReponses []response.EngineResponse) string {
}
}
}
return fmt.Sprintf("Resource %s: %s", resourceInfo, strings.Join(str, "\n"))
return fmt.Sprintf("Resource %s %s", resourceInfo, strings.Join(str, ";"))
}
//ArrayFlags to store filterkinds

View file

@ -41,6 +41,7 @@ func (ws *WebhookServer) HandleGenerate(request *v1beta1.AdmissionRequest, polic
policyContext := engine.PolicyContext{
NewResource: *resource,
AdmissionInfo: userRequestInfo,
Context: ctx,
}
// engine.Generate returns a list of rules that are applicable on this resource

View file

@ -8,6 +8,7 @@ import (
"github.com/nirmata/kyverno/pkg/engine/response"
engineutils "github.com/nirmata/kyverno/pkg/engine/utils"
policyctr "github.com/nirmata/kyverno/pkg/policy"
"github.com/nirmata/kyverno/pkg/policyviolation"
"github.com/nirmata/kyverno/pkg/utils"
v1beta1 "k8s.io/api/admission/v1beta1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@ -70,6 +71,7 @@ func (ws *WebhookServer) HandleMutation(request *v1beta1.AdmissionRequest, resou
policyContext := engine.PolicyContext{
NewResource: resource,
AdmissionInfo: userRequestInfo,
Context: ctx,
}
for _, policy := range policies {
@ -96,6 +98,10 @@ func (ws *WebhookServer) HandleMutation(request *v1beta1.AdmissionRequest, resou
patches = append(patches, annPatches)
}
// generate violation when referenced path does not exist
pvInfos := policyviolation.GeneratePVsFromEngineResponse(engineResponses)
ws.pvGenerator.Add(pvInfos...)
// ADD EVENTS
events := generateEvents(engineResponses, (request.Operation == v1beta1.Update))
ws.eventGen.Add(events...)

View file

@ -136,7 +136,7 @@ func defaultvalidationFailureAction(policy *kyverno.ClusterPolicy) ([]byte, stri
// scenario A: not exist, set default to "all", which generates on all pod controllers
// - if name / selector exist in resource description -> skip
// as these fields may not be applicable to pod controllers
// scenario B: "null", user explicitely disable this feature -> skip
// scenario B: "none", user explicitely disable this feature -> skip
// scenario C: some certain controllers that user set -> generate on defined controllers
// copy entrie match / exclude block, it's users' responsibility to
// make sure all fields are applicable to pod cotrollers
@ -158,7 +158,7 @@ func generatePodControllerRule(policy kyverno.ClusterPolicy) (patches [][]byte,
}
// scenario B
if controllers == "null" {
if controllers == "none" {
return nil, nil
}

View file

@ -66,6 +66,187 @@ func TestGeneratePodControllerRule_PredefinedAnnotation(t *testing.T) {
assert.Assert(t, len(patches) == 0)
}
func TestGeneratePodControllerRule_DisableFeature(t *testing.T) {
policyRaw := []byte(`{
"apiVersion": "kyverno.io/v1",
"kind": "ClusterPolicy",
"metadata": {
"annotations": {
"a": "b",
"pod-policies.kyverno.io/autogen-controllers": "none"
},
"name": "add-safe-to-evict"
},
"spec": {
"rules": [
{
"name": "annotate-empty-dir",
"match": {
"resources": {
"kinds": [
"Pod"
]
}
},
"mutate": {
"overlay": {
"metadata": {
"annotations": {
"+(cluster-autoscaler.kubernetes.io/safe-to-evict)": "true"
}
},
"spec": {
"volumes": [
{
"(emptyDir)": {
}
}
]
}
}
}
}
]
}
}`)
var policy kyverno.ClusterPolicy
assert.Assert(t, json.Unmarshal(policyRaw, &policy))
patches, errs := generatePodControllerRule(policy)
assert.Assert(t, len(errs) == 0)
assert.Assert(t, len(patches) == 0)
}
func TestGeneratePodControllerRule_Mutate(t *testing.T) {
policyRaw := []byte(`{
"apiVersion": "kyverno.io/v1",
"kind": "ClusterPolicy",
"metadata": {
"annotations": {
"a": "b",
"pod-policies.kyverno.io/autogen-controllers": "all"
},
"name": "add-safe-to-evict"
},
"spec": {
"rules": [
{
"name": "annotate-empty-dir",
"match": {
"resources": {
"kinds": [
"Pod"
]
}
},
"mutate": {
"overlay": {
"metadata": {
"annotations": {
"+(cluster-autoscaler.kubernetes.io/safe-to-evict)": "true"
}
},
"spec": {
"volumes": [
{
"(emptyDir)": {
}
}
]
}
}
}
}
]
}
}`)
var policy kyverno.ClusterPolicy
assert.Assert(t, json.Unmarshal(policyRaw, &policy))
patches, errs := generatePodControllerRule(policy)
assert.Assert(t, len(errs) == 0)
p, err := utils.ApplyPatches(policyRaw, patches)
assert.NilError(t, err)
expectedPolicy := []byte(`{
"apiVersion": "kyverno.io/v1",
"kind": "ClusterPolicy",
"metadata": {
"annotations": {
"a": "b",
"pod-policies.kyverno.io/autogen-controllers": "all"
},
"name": "add-safe-to-evict"
},
"spec": {
"rules": [
{
"name": "annotate-empty-dir",
"match": {
"resources": {
"kinds": [
"Pod"
]
}
},
"mutate": {
"overlay": {
"metadata": {
"annotations": {
"+(cluster-autoscaler.kubernetes.io/safe-to-evict)": "true"
}
},
"spec": {
"volumes": [
{
"(emptyDir)": {
}
}
]
}
}
}
},
{
"name": "autogen-annotate-empty-dir",
"match": {
"resources": {
"kinds": [
"DaemonSet",
"Deployment",
"Job",
"StatefulSet"
]
}
},
"mutate": {
"overlay": {
"spec": {
"template": {
"metadata": {
"annotations": {
"+(cluster-autoscaler.kubernetes.io/safe-to-evict)": "true"
}
},
"spec": {
"volumes": [
{
"(emptyDir)": {
}
}
]
}
}
}
}
}
}
]
}
}`)
compareJSONAsMap(t, p, expectedPolicy)
}
func TestGeneratePodControllerRule_ExistOtherAnnotation(t *testing.T) {
policyRaw := []byte(`{
"apiVersion": "kyverno.io/v1",
@ -100,133 +281,55 @@ func TestGeneratePodControllerRule_ExistOtherAnnotation(t *testing.T) {
compareJSONAsMap(t, p, expectedPolicy)
}
func TestGeneratePodControllerRule(t *testing.T) {
func TestGeneratePodControllerRule_ValidateAnyPattern(t *testing.T) {
policyRaw := []byte(`{
"apiVersion": "kyverno.io/v1",
"kind": "ClusterPolicy",
"metadata": {
"name": "add-safe-to-evict",
"annotations": {
"a": "b"
}
"annotations": {
"pod-policies.kyverno.io/autogen-controllers": "Deployment"
},
"name": "add-safe-to-evict"
},
"spec": {
"rules": [
{
"name": "annotate-empty-dir",
"match": {
"resources": {
"kinds": [
"Pod"
]
}
},
"mutate": {
"overlay": {
"metadata": {
"annotations": {
"+(cluster-autoscaler.kubernetes.io/safe-to-evict)": "true"
}
},
"spec": {
"volumes": [
{
"(emptyDir)": {}
}
]
}
}
}
"rules": [
{
"name": "validate-runAsNonRoot",
"match": {
"resources": {
"kinds": [
"Pod"
]
}
},
{
"name": "annotate-host-path",
"match": {
"resources": {
"kinds": [
"Pod"
]
"validate": {
"message": "Running as root user is not allowed. Set runAsNonRoot to true",
"anyPattern": [
{
"spec": {
"securityContext": {
"runAsNonRoot": true
}
}
},
"mutate": {
"overlay": {
"metadata": {
"annotations": {
"+(cluster-autoscaler.kubernetes.io/safe-to-evict)": "true"
},
{
"spec": {
"containers": [
{
"name": "*",
"securityContext": {
"runAsNonRoot": true
}
},
"spec": {
"volumes": [
{
"(hostPath)": {
"path": "*"
}
}
]
}
}
]
}
}
},
{
"name": "validate-runAsNonRoot",
"match": {
"resources": {
"kinds": [
"Pod"
]
}
},
"validate": {
"message": "Running as root user is not allowed. Set runAsNonRoot to true",
"anyPattern": [
{
"spec": {
"securityContext": {
"runAsNonRoot": true
}
}
},
{
"spec": {
"containers": [
{
"name": "*",
"securityContext": {
"runAsNonRoot": true
}
}
]
}
}
]
}
},
{
"name": "validate-docker-sock-mount",
"match": {
"resources": {
"kinds": [
"Pod"
]
}
},
"validate": {
"message": "Use of the Docker Unix socket is not allowed",
"pattern": {
"spec": {
"=(volumes)": [
{
"=(hostPath)": {
"path": "!/var/run/docker.sock"
}
}
]
}
}
}
}
]
}
]
}
]
}
}`)
}`)
var policy kyverno.ClusterPolicy
assert.Assert(t, json.Unmarshal(policyRaw, &policy))
@ -236,278 +339,212 @@ func TestGeneratePodControllerRule(t *testing.T) {
p, err := utils.ApplyPatches(policyRaw, patches)
assert.NilError(t, err)
expectPolicy := []byte(`{
expectedPolicy := []byte(`{
"apiVersion": "kyverno.io/v1",
"kind": "ClusterPolicy",
"metadata": {
"annotations": {
"a": "b",
"pod-policies.kyverno.io/autogen-controllers": "all"
},
"name": "add-safe-to-evict"
"annotations": {
"pod-policies.kyverno.io/autogen-controllers": "Deployment"
},
"name": "add-safe-to-evict"
},
"spec": {
"rules": [
{
"name": "annotate-empty-dir",
"match": {
"resources": {
"kinds": [
"Pod"
]
}
},
"mutate": {
"overlay": {
"metadata": {
"annotations": {
"+(cluster-autoscaler.kubernetes.io/safe-to-evict)": "true"
}
},
"spec": {
"volumes": [
{
"(emptyDir)": {}
}
]
}
}
}
"rules": [
{
"name": "validate-runAsNonRoot",
"match": {
"resources": {
"kinds": [
"Pod"
]
}
},
{
"name": "annotate-host-path",
"match": {
"resources": {
"kinds": [
"Pod"
]
"validate": {
"message": "Running as root user is not allowed. Set runAsNonRoot to true",
"anyPattern": [
{
"spec": {
"securityContext": {
"runAsNonRoot": true
}
}
},
"mutate": {
"overlay": {
"metadata": {
"annotations": {
"+(cluster-autoscaler.kubernetes.io/safe-to-evict)": "true"
},
{
"spec": {
"containers": [
{
"name": "*",
"securityContext": {
"runAsNonRoot": true
}
},
"spec": {
"volumes": [
{
"(hostPath)": {
"path": "*"
}
}
]
}
}
]
}
}
},
{
"name": "validate-runAsNonRoot",
"match": {
"resources": {
"kinds": [
"Pod"
]
}
},
"validate": {
"message": "Running as root user is not allowed. Set runAsNonRoot to true",
"anyPattern": [
{
"spec": {
"securityContext": {
"runAsNonRoot": true
}
}
},
{
"spec": {
"containers": [
{
"name": "*",
"securityContext": {
"runAsNonRoot": true
}
}
]
}
}
]
}
},
{
"name": "validate-docker-sock-mount",
"match": {
"resources": {
"kinds": [
"Pod"
]
}
},
"validate": {
"message": "Use of the Docker Unix socket is not allowed",
"pattern": {
"spec": {
"=(volumes)": [
{
"=(hostPath)": {
"path": "!/var/run/docker.sock"
}
}
]
}
}
}
},
{
"name": "autogen-annotate-empty-dir",
"match": {
"resources": {
"kinds": [
"DaemonSet",
"Deployment",
"Job",
"StatefulSet"
]
}
},
"mutate": {
"overlay": {
"spec": {
"template": {
"metadata": {
"annotations": {
"+(cluster-autoscaler.kubernetes.io/safe-to-evict)": "true"
}
},
"spec": {
"volumes": [
{
"(emptyDir)": {}
}
]
}
}
}
}
}
},
{
"name": "autogen-annotate-host-path",
"match": {
"resources": {
"kinds": [
"DaemonSet",
"Deployment",
"Job",
"StatefulSet"
]
}
},
"mutate": {
"overlay": {
"spec": {
"template": {
"metadata": {
"annotations": {
"+(cluster-autoscaler.kubernetes.io/safe-to-evict)": "true"
}
},
"spec": {
"volumes": [
{
"(hostPath)": {
"path": "*"
}
}
]
}
}
}
}
}
},
{
"name": "autogen-validate-runAsNonRoot",
"match": {
"resources": {
"kinds": [
"DaemonSet",
"Deployment",
"Job",
"StatefulSet"
]
}
},
"validate": {
"message": "Running as root user is not allowed. Set runAsNonRoot to true",
"anyPattern": [
{
"spec": {
"template": {
"spec": {
"securityContext": {
"runAsNonRoot": true
}
}
}
}
},
{
"spec": {
"template": {
"spec": {
"containers": [
{
"name": "*",
"securityContext": {
"runAsNonRoot": true
}
}
]
}
}
}
}
]
}
},
{
"name": "autogen-validate-docker-sock-mount",
"match": {
"resources": {
"kinds": [
"DaemonSet",
"Deployment",
"Job",
"StatefulSet"
]
}
},
"validate": {
"message": "Use of the Docker Unix socket is not allowed",
"pattern": {
"spec": {
"template": {
"spec": {
"=(volumes)": [
{
"=(hostPath)": {
"path": "!/var/run/docker.sock"
}
}
]
}
}
}
}
}
}
]
}
]
},
{
"name": "autogen-validate-runAsNonRoot",
"match": {
"resources": {
"kinds": [
"Deployment"
]
}
},
"validate": {
"message": "Running as root user is not allowed. Set runAsNonRoot to true",
"anyPattern": [
{
"spec": {
"template": {
"spec": {
"securityContext": {
"runAsNonRoot": true
}
}
}
}
},
{
"spec": {
"template": {
"spec": {
"containers": [
{
"name": "*",
"securityContext": {
"runAsNonRoot": true
}
}
]
}
}
}
}
]
}
}
]
}
}`)
t.Log(string(expectPolicy))
t.Log(string(p))
compareJSONAsMap(t, expectPolicy, p)
}`)
compareJSONAsMap(t, p, expectedPolicy)
}
func TestGeneratePodControllerRule_ValidatePattern(t *testing.T) {
policyRaw := []byte(`{
"apiVersion": "kyverno.io/v1",
"kind": "ClusterPolicy",
"metadata": {
"name": "add-safe-to-evict"
},
"spec": {
"rules": [
{
"name": "validate-docker-sock-mount",
"match": {
"resources": {
"kinds": [
"Pod"
]
}
},
"validate": {
"message": "Use of the Docker Unix socket is not allowed",
"pattern": {
"spec": {
"=(volumes)": [
{
"=(hostPath)": {
"path": "!/var/run/docker.sock"
}
}
]
}
}
}
}
]
}
}`)
var policy kyverno.ClusterPolicy
assert.Assert(t, json.Unmarshal(policyRaw, &policy))
patches, errs := generatePodControllerRule(policy)
assert.Assert(t, len(errs) == 0)
p, err := utils.ApplyPatches(policyRaw, patches)
assert.NilError(t, err)
expectedPolicy := []byte(`{
"apiVersion": "kyverno.io/v1",
"kind": "ClusterPolicy",
"metadata": {
"annotations": {
"pod-policies.kyverno.io/autogen-controllers": "all"
},
"name": "add-safe-to-evict"
},
"spec": {
"rules": [
{
"name": "validate-docker-sock-mount",
"match": {
"resources": {
"kinds": [
"Pod"
]
}
},
"validate": {
"message": "Use of the Docker Unix socket is not allowed",
"pattern": {
"spec": {
"=(volumes)": [
{
"=(hostPath)": {
"path": "!/var/run/docker.sock"
}
}
]
}
}
}
},
{
"name": "autogen-validate-docker-sock-mount",
"match": {
"resources": {
"kinds": [
"DaemonSet",
"Deployment",
"Job",
"StatefulSet"
]
}
},
"validate": {
"message": "Use of the Docker Unix socket is not allowed",
"pattern": {
"spec": {
"template": {
"spec": {
"=(volumes)": [
{
"=(hostPath)": {
"path": "!/var/run/docker.sock"
}
}
]
}
}
}
}
}
}
]
}
}`)
compareJSONAsMap(t, p, expectedPolicy)
}

View file

@ -1,12 +1,10 @@
package webhooks
import (
"fmt"
"strings"
kyverno "github.com/nirmata/kyverno/pkg/api/kyverno/v1"
"github.com/nirmata/kyverno/pkg/engine/response"
"github.com/nirmata/kyverno/pkg/policyviolation"
"github.com/golang/glog"
"github.com/nirmata/kyverno/pkg/event"
@ -105,57 +103,3 @@ func generateEvents(engineResponses []response.EngineResponse, onUpdate bool) []
}
return events
}
func generatePV(ers []response.EngineResponse, blocked bool) []policyviolation.Info {
var pvInfos []policyviolation.Info
// generate PV for each
for _, er := range ers {
// ignore creation of PV for resoruces that are yet to be assigned a name
if er.IsSuccesful() {
continue
}
glog.V(4).Infof("Building policy violation for engine response %v", er)
// build policy violation info
pvInfos = append(pvInfos, buildPVInfo(er, blocked))
}
return pvInfos
}
func buildPVInfo(er response.EngineResponse, blocked bool) policyviolation.Info {
info := policyviolation.Info{
Blocked: blocked,
PolicyName: er.PolicyResponse.Policy,
Resource: er.PatchedResource,
Rules: buildViolatedRules(er, blocked),
}
return info
}
func buildViolatedRules(er response.EngineResponse, blocked bool) []kyverno.ViolatedRule {
blockMsg := fmt.Sprintf("Request Blocked for resource %s/%s; ", er.PolicyResponse.Resource.Namespace, er.PolicyResponse.Resource.Kind)
var violatedRules []kyverno.ViolatedRule
// if resource was blocked we create dependent
dependant := kyverno.ManagedResourceSpec{
Kind: er.PolicyResponse.Resource.Kind,
CreationBlocked: true,
}
for _, rule := range er.PolicyResponse.Rules {
if rule.Success {
continue
}
vrule := kyverno.ViolatedRule{
Name: rule.Name,
Type: rule.Type,
}
if blocked {
vrule.Message = blockMsg + rule.Message
vrule.ManagedResource = dependant
} else {
vrule.Message = rule.Message
}
violatedRules = append(violatedRules, vrule)
}
return violatedRules
}

View file

@ -10,6 +10,7 @@ import (
"github.com/nirmata/kyverno/pkg/engine/context"
"github.com/nirmata/kyverno/pkg/engine/response"
policyctr "github.com/nirmata/kyverno/pkg/policy"
"github.com/nirmata/kyverno/pkg/policyviolation"
"github.com/nirmata/kyverno/pkg/utils"
v1beta1 "k8s.io/api/admission/v1beta1"
)
@ -100,25 +101,18 @@ func (ws *WebhookServer) HandleValidation(request *v1beta1.AdmissionRequest, pol
reportTime := time.Now()
// If Validation fails then reject the request
// violations are created with resource owner(if exist) on "enforce"
// and if there are any then we dont block the resource creation
// Even if one the policy being applied
// no violations will be created on "enforce"
// the event will be reported on owner by k8s
blocked := toBlockResource(engineResponses)
if !isResponseSuccesful(engineResponses) && blocked {
if blocked {
glog.V(4).Infof("resource %s/%s/%s is blocked\n", newR.GetKind(), newR.GetNamespace(), newR.GetName())
pvInfos := generatePV(engineResponses, true)
ws.pvGenerator.Add(pvInfos...)
// ADD EVENTS
events := generateEvents(engineResponses, (request.Operation == v1beta1.Update))
ws.eventGen.Add(events...)
sendStat(true)
return false, getErrorMsg(engineResponses)
}
// ADD POLICY VIOLATIONS
// violations are created with resource on "audit"
pvInfos := generatePV(engineResponses, blocked)
pvInfos := policyviolation.GeneratePVsFromEngineResponse(engineResponses)
ws.pvGenerator.Add(pvInfos...)
// ADD EVENTS
events := generateEvents(engineResponses, (request.Operation == v1beta1.Update))

View file

@ -10,7 +10,6 @@ metadata:
to a specific host and data persisted in the `hostPath` volume is coupled to the life of the
node leading to potential pod scheduling failures. It is highly recommended that applications
are designed to be decoupled from the underlying infrastructure (in this case, nodes).
spec:
rules:
- name: validate-hostPath

View file

@ -4,7 +4,8 @@ metadata:
name: disallow-helm-tiller
annotations:
policies.kyverno.io/category: Security
policies.kyverno.io/description: Tiller has known security challenges. It requires adminstrative privileges and acts as a shared resource accessible to any authenticated user. Tiller can lead to privilge escalation as restricted users can impact other users.
policies.kyverno.io/description: Tiller has known security challenges. It requires adminstrative privileges and acts as a shared
resource accessible to any authenticated user. Tiller can lead to privilge escalation as restricted users can impact other users.
spec:
rules:
- name: validate-helm-tiller