From f25a336eeb7a1bec2bb73adf0e6ace84e57d3873 Mon Sep 17 00:00:00 2001 From: Shuting Zhao Date: Wed, 30 Oct 2019 12:29:57 -0700 Subject: [PATCH 01/12] - update doc; -remove duplicate policy --- samples/README.md | 5 ++--- .../policy_validate_deny_runasrootuser.yaml | 22 ------------------- 2 files changed, 2 insertions(+), 25 deletions(-) delete mode 100644 samples/best_practices/policy_validate_deny_runasrootuser.yaml diff --git a/samples/README.md b/samples/README.md index 948a7d016f..6cfa0f3a49 100644 --- a/samples/README.md +++ b/samples/README.md @@ -13,10 +13,9 @@ kubectl create -f https://github.com/nirmata/kyverno/raw/master/definitions/inst ````bash -kubectl create -f https://github.com/nirmata/kyverno/raw/master/samples/best_practices/ - -kubectl create -f https://github.com/nirmata/kyverno/raw/master/samples/more/ +kubectl create -f [samples/best_practices/](best_practices/) +kubectl create -f [samples/more/](more/) ```` The policies are mostly validation rules in `audit` mode i.e. your existing workloads will not be impacted, but will be audited for policy complaince. diff --git a/samples/best_practices/policy_validate_deny_runasrootuser.yaml b/samples/best_practices/policy_validate_deny_runasrootuser.yaml deleted file mode 100644 index 3938e33998..0000000000 --- a/samples/best_practices/policy_validate_deny_runasrootuser.yaml +++ /dev/null @@ -1,22 +0,0 @@ -apiVersion: kyverno.io/v1alpha1 -kind: ClusterPolicy -metadata: - name: validate-deny-runasrootuser -spec: - rules: - - name: deny-runasrootuser - match: - resources: - kinds: - - Pod - validate: - message: "Root user is not allowed. Set runAsNonRoot to true." - anyPattern: - - spec: - securityContext: - runAsNonRoot: true - - spec: - containers: - - name: "*" - securityContext: - runAsNonRoot: true \ No newline at end of file From 65016ff4552cc609d37c56654ed4bc06d70f4c3a Mon Sep 17 00:00:00 2001 From: Shuting Zhao Date: Wed, 30 Oct 2019 12:31:09 -0700 Subject: [PATCH 02/12] correct link --- samples/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/samples/README.md b/samples/README.md index 6cfa0f3a49..c4519e15db 100644 --- a/samples/README.md +++ b/samples/README.md @@ -13,9 +13,9 @@ kubectl create -f https://github.com/nirmata/kyverno/raw/master/definitions/inst ````bash -kubectl create -f [samples/best_practices/](best_practices/) +kubectl create -f [samples/best_practices/](best_practices) -kubectl create -f [samples/more/](more/) +kubectl create -f [samples/more/](more) ```` The policies are mostly validation rules in `audit` mode i.e. your existing workloads will not be impacted, but will be audited for policy complaince. From 3c26b607279cdc278bfae7c49bb943eb48e7ae23 Mon Sep 17 00:00:00 2001 From: Shuting Zhao Date: Wed, 30 Oct 2019 12:36:00 -0700 Subject: [PATCH 03/12] update doc --- samples/README.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/samples/README.md b/samples/README.md index c4519e15db..88824f8087 100644 --- a/samples/README.md +++ b/samples/README.md @@ -11,11 +11,16 @@ kubectl create -f https://github.com/nirmata/kyverno/raw/master/definitions/inst **Apply Kyverno Policies** +Import best_practices from [here](best_pratices): + ````bash +kubectl create -f samples/best_practices +```` -kubectl create -f [samples/best_practices/](best_practices) +Import addition policies from [here](more): -kubectl create -f [samples/more/](more) +````bash +kubectl create -f samples/more/ ```` The policies are mostly validation rules in `audit` mode i.e. your existing workloads will not be impacted, but will be audited for policy complaince. From 9f21e5898c61275e33f03ee5b87074ab547c9805 Mon Sep 17 00:00:00 2001 From: Jim Bugwadia Date: Wed, 30 Oct 2019 12:51:07 -0700 Subject: [PATCH 04/12] fix for https://github.com/nirmata/kyverno/issues/409 --- samples/DisallowHostFS.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/samples/DisallowHostFS.md b/samples/DisallowHostFS.md index 816c32fc52..a1a407e42f 100644 --- a/samples/DisallowHostFS.md +++ b/samples/DisallowHostFS.md @@ -1,6 +1,6 @@ -# Disallow use of host filesystem +# Disallow use of bind mounts (`hostPath` volumes) -The volume of type `hostpath` allows pods to use host directories and volume mounted to a host path. This binds pods to a specific host, and data persisted in the volume is coupled to the life of the node. It is highly recommeded that applications are designed to be decoupled from the underlying infrstructure (in this case, nodes). +The volume of type `hostPath` allows pods to use host bind mounts (i.e. directories and volumes mounted to a host path) in containers. Using host resources can be used to access shared data or escalate priviliges. Also, this couples pods 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 recommeded that applications are designed to be decoupled from the underlying infrstructure (in this case, nodes). ## Policy YAML From 1dfccf99d4e5c16e3eec0659ab839281ec153613 Mon Sep 17 00:00:00 2001 From: Jim Bugwadia Date: Wed, 30 Oct 2019 12:52:45 -0700 Subject: [PATCH 05/12] update README.md --- samples/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/README.md b/samples/README.md index 948a7d016f..7676a2ae34 100644 --- a/samples/README.md +++ b/samples/README.md @@ -28,7 +28,7 @@ These policies are highly recommended. 1. [Run as non-root user](RunAsNonRootUser.md) 2. [Disable privileged containers and disallow privilege escalation](DisablePrivilegedContainers.md) 3. [Require Read-only root filesystem](RequireReadOnlyFS.md) -4. [Disallow use of host filesystem](DisallowHostFS.md) +4. [Disallow use of bind mounts (`hostPath` volumes)](DisallowHostFS.md) 5. [Disallow `hostNetwork` and `hostPort`](DisallowHostNetworkPort.md) 6. [Disallow `hostPID` and `hostIPC`](DisallowHostPIDIPC.md) 7. [Disallow unknown image registries](DisallowUnknownRegistries.md) From 40c982478124da65871e497979ebe0802cd4c840 Mon Sep 17 00:00:00 2001 From: Shuting Zhao Date: Wed, 30 Oct 2019 12:58:14 -0700 Subject: [PATCH 06/12] fix test --- .../samples/best_practices/scenario_validate_nonRootUser.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/scenarios/samples/best_practices/scenario_validate_nonRootUser.yaml b/test/scenarios/samples/best_practices/scenario_validate_nonRootUser.yaml index 6f6e9497b8..ae6e8116a3 100644 --- a/test/scenarios/samples/best_practices/scenario_validate_nonRootUser.yaml +++ b/test/scenarios/samples/best_practices/scenario_validate_nonRootUser.yaml @@ -1,6 +1,6 @@ # file path relative to project root input: - policy: samples/best_practices/policy_validate_deny_runasrootuser.yaml + policy: samples/best_practices/deny_runasrootuser.yaml resource: test/resources/resource_validate_nonRootUser.yaml expected: validation: From 3783ae049572717d822deebdb4ffe2ad01bdf15f Mon Sep 17 00:00:00 2001 From: Jim Bugwadia Date: Wed, 30 Oct 2019 12:59:51 -0700 Subject: [PATCH 07/12] update policy --- samples/best_practices/disallow_host_filesystem.yaml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/samples/best_practices/disallow_host_filesystem.yaml b/samples/best_practices/disallow_host_filesystem.yaml index a9b6b988f6..27268b2694 100644 --- a/samples/best_practices/disallow_host_filesystem.yaml +++ b/samples/best_practices/disallow_host_filesystem.yaml @@ -4,9 +4,13 @@ metadata: name: "deny-use-of-host-fs" annotations: policies.kyverno.io/category: Data Protection - policies.kyverno.io/description: The volume of type 'hostpath' binds pods to a specific host, - and data persisted in the volume is dependent on the life of the node. In a shared cluster, - it is recommeded that applications are independent of hosts. + policies.kyverno.io/description: The volume of type `hostPath` allows pods to use host bind + mounts (i.e. directories and volumes mounted to a host path) in containers. Using host + resources can be used to access shared data or escalate priviliges. Also, this couples pods + 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 recommeded that applications + are designed to be decoupled from the underlying infrstructure (in this case, nodes). + spec: rules: - name: "deny-use-of-host-fs" From 3438e84013ab3c1ed85070673c9007b32fa027fc Mon Sep 17 00:00:00 2001 From: Jim Bugwadia Date: Wed, 30 Oct 2019 13:12:29 -0700 Subject: [PATCH 08/12] fix YAML syntax --- samples/best_practices/disallow_host_filesystem.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/samples/best_practices/disallow_host_filesystem.yaml b/samples/best_practices/disallow_host_filesystem.yaml index 27268b2694..7ad0596f67 100644 --- a/samples/best_practices/disallow_host_filesystem.yaml +++ b/samples/best_practices/disallow_host_filesystem.yaml @@ -5,11 +5,11 @@ metadata: annotations: policies.kyverno.io/category: Data Protection policies.kyverno.io/description: The volume of type `hostPath` allows pods to use host bind - mounts (i.e. directories and volumes mounted to a host path) in containers. Using host - resources can be used to access shared data or escalate priviliges. Also, this couples pods - 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 recommeded that applications - are designed to be decoupled from the underlying infrstructure (in this case, nodes). + mounts (i.e. directories and volumes mounted to a host path) in containers. Using host + resources can be used to access shared data or escalate priviliges. Also, this couples pods + 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 recommeded that applications + are designed to be decoupled from the underlying infrstructure (in this case, nodes). spec: rules: From 55983e222302bd35c9ace21f05b34289bf15b127 Mon Sep 17 00:00:00 2001 From: Shuting Zhao Date: Wed, 30 Oct 2019 13:14:59 -0700 Subject: [PATCH 09/12] add clone instuction --- samples/README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/samples/README.md b/samples/README.md index 88824f8087..55eb58ab16 100644 --- a/samples/README.md +++ b/samples/README.md @@ -11,6 +11,13 @@ kubectl create -f https://github.com/nirmata/kyverno/raw/master/definitions/inst **Apply Kyverno Policies** +To start applying policies to your cluster, first clone the repo: + +````bash +git clone https://github.com/nirmata/kyverno.git +cd kyverno +```` + Import best_practices from [here](best_pratices): ````bash From fb6151c6d0f3f3e964772a3f544a8f2f5e528400 Mon Sep 17 00:00:00 2001 From: Shuting Zhao Date: Wed, 30 Oct 2019 18:05:08 -0700 Subject: [PATCH 10/12] release 0.11.0 --- definitions/install.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/definitions/install.yaml b/definitions/install.yaml index dce2f84e0a..ec3c86eb87 100644 --- a/definitions/install.yaml +++ b/definitions/install.yaml @@ -307,7 +307,7 @@ spec: serviceAccountName: kyverno-service-account containers: - name: kyverno - image: nirmata/kyverno:latest + image: nirmata/kyverno:v0.11.0 args: - "--filterK8Resources=[Event,*,*][*,kube-system,*][*,kube-public,*][*,kube-node-lease,*][Node,*,*][APIService,*,*][TokenReview,*,*][SubjectAccessReview,*,*][*,kyverno,*]" # customize webhook timout From 61c1ea5a49895ca1f45f074d39500794fb2587ad Mon Sep 17 00:00:00 2001 From: shivkumar dudhani Date: Thu, 31 Oct 2019 13:04:56 -0700 Subject: [PATCH 11/12] use validatepattern in generate rule to check for subset existance --- pkg/engine/generation.go | 36 +--- pkg/engine/pattern.go | 25 ++- pkg/utils/json.go | 188 ------------------ pkg/webhooks/validation.go | 2 - .../require_namespace_quota.yaml | 8 +- ...policy_validate_user_group_fsgroup_id.yaml | 6 +- 6 files changed, 35 insertions(+), 230 deletions(-) diff --git a/pkg/engine/generation.go b/pkg/engine/generation.go index 91bfc451db..ff62a72de7 100644 --- a/pkg/engine/generation.go +++ b/pkg/engine/generation.go @@ -1,7 +1,6 @@ package engine import ( - "encoding/json" "time" "fmt" @@ -9,7 +8,6 @@ import ( "github.com/golang/glog" kyverno "github.com/nirmata/kyverno/pkg/api/kyverno/v1alpha1" client "github.com/nirmata/kyverno/pkg/dclient" - "github.com/nirmata/kyverno/pkg/utils" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" @@ -149,38 +147,12 @@ func applyRuleGenerator(client *client.Client, ns unstructured.Unstructured, rul //checkResource checks if the config is present in th eresource func checkResource(config interface{}, resource *unstructured.Unstructured) (bool, error) { - var err error - objByte, err := resource.MarshalJSON() + // we are checking if config is a subset of resource with default pattern + path, err := validateResourceWithPattern(resource.Object, config) if err != nil { - // unable to parse the json + glog.V(4).Infof("config not a subset of resource. failed at path %s: %v", path, err) return false, err } - err = resource.UnmarshalJSON(objByte) - if err != nil { - // unable to parse the json - return false, err - } - // marshall and unmarshall json to verify if its right format - configByte, err := json.Marshal(config) - if err != nil { - // unable to marshall the config - return false, err - } - var configData interface{} - err = json.Unmarshal(configByte, &configData) - if err != nil { - // unable to unmarshall - return false, err - } - - var objData interface{} - err = json.Unmarshal(objByte, &objData) - if err != nil { - // unable to unmarshall - return false, err - } - - // Check if the config is a subset of resource - return utils.JSONsubsetValue(configData, objData), nil + return true, nil } diff --git a/pkg/engine/pattern.go b/pkg/engine/pattern.go index 185f993e2e..421398509f 100644 --- a/pkg/engine/pattern.go +++ b/pkg/engine/pattern.go @@ -3,6 +3,7 @@ package engine import ( "math" "regexp" + "strconv" "strings" "github.com/golang/glog" @@ -91,6 +92,14 @@ func validateValueWithIntPattern(value interface{}, pattern int64) bool { glog.Warningf("Expected int, found float: %f\n", typedValue) return false + case string: + // extract int64 from string + int64Num, err := strconv.ParseInt(typedValue, 10, 64) + if err != nil { + glog.Warningf("Failed to parse int64 from string: %v", err) + return false + } + return int64Num == pattern default: glog.Warningf("Expected int, found: %T\n", value) return false @@ -105,11 +114,25 @@ func validateValueWithFloatPattern(value interface{}, pattern float64) bool { if pattern == math.Trunc(pattern) { return int(pattern) == value } - + glog.Warningf("Expected float, found int: %d\n", typedValue) + return false + case int64: + // check that float has no fraction + if pattern == math.Trunc(pattern) { + return int64(pattern) == value + } glog.Warningf("Expected float, found int: %d\n", typedValue) return false case float64: return typedValue == pattern + case string: + // extract float64 from string + float64Num, err := strconv.ParseFloat(typedValue, 64) + if err != nil { + glog.Warningf("Failed to parse float64 from string: %v", err) + return false + } + return float64Num == pattern default: glog.Warningf("Expected float, found: %T\n", value) return false diff --git a/pkg/utils/json.go b/pkg/utils/json.go index 651e86c049..471714958f 100644 --- a/pkg/utils/json.go +++ b/pkg/utils/json.go @@ -1,193 +1,5 @@ package utils -import ( - "github.com/golang/glog" -) - -//JSONsubsetValue checks if JSON a is contained in JSON b -func JSONsubsetValue(a interface{}, b interface{}) bool { - switch typed := a.(type) { - case bool: - bv, ok := b.(bool) - if !ok { - glog.Errorf("expected bool found %T", b) - return false - } - av, _ := a.(bool) - if av == bv { - return true - } - case int: - bv, ok := b.(int) - if !ok { - glog.Errorf("expected int found %T", b) - return false - } - av, _ := a.(int) - if av == bv { - return true - } - case float64: - bv, ok := b.(float64) - if !ok { - glog.Errorf("expected float64 found %T", b) - return false - } - av, _ := a.(float64) - if av == bv { - return true - } - - case string: - bv, ok := b.(string) - if !ok { - glog.Errorf("expected string found %T", b) - return false - } - av, _ := a.(string) - if av == bv { - return true - } - - case map[string]interface{}: - bv, ok := b.(map[string]interface{}) - if !ok { - glog.Errorf("expected map[string]interface{} found %T", b) - return false - } - av, _ := a.(map[string]interface{}) - return subsetMap(av, bv) - case []interface{}: - // TODO: verify the logic - bv, ok := b.([]interface{}) - if !ok { - glog.Errorf("expected []interface{} found %T", b) - return false - } - av, _ := a.([]interface{}) - return subsetSlice(av, bv) - default: - glog.Errorf("Unspported type %s", typed) - - } - return false -} - -func subsetMap(a, b map[string]interface{}) bool { - // check if keys are present - for k := range a { - if _, ok := b[k]; !ok { - glog.Errorf("key %s, not present in resource", k) - return false - } - } - // check if values for the keys match - for ak, av := range a { - bv := b[ak] - if !JSONsubsetValue(av, bv) { - return false - } - } - return true -} - -func containsInt(a interface{}, b []interface{}) bool { - switch typed := a.(type) { - case bool: - for _, bv := range b { - bv, ok := bv.(bool) - if !ok { - return false - } - av, _ := a.(bool) - - if bv == av { - return true - } - } - case int: - for _, bv := range b { - bv, ok := bv.(int) - if !ok { - return false - } - av, _ := a.(int) - - if bv == av { - return true - } - } - case float64: - for _, bv := range b { - bv, ok := bv.(float64) - if !ok { - return false - } - av, _ := a.(float64) - - if bv == av { - return true - } - } - case string: - for _, bv := range b { - bv, ok := bv.(string) - if !ok { - return false - } - av, _ := a.(string) - - if bv == av { - return true - } - } - case map[string]interface{}: - for _, bv := range b { - bv, ok := bv.(map[string]interface{}) - if !ok { - return false - } - av, _ := a.(map[string]interface{}) - if subsetMap(av, bv) { - return true - } - } - case []interface{}: - for _, bv := range b { - bv, ok := bv.([]interface{}) - if !ok { - return false - } - av, _ := a.([]interface{}) - if JSONsubsetValue(av, bv) { - return true - } - } - default: - glog.Errorf("Unspported type %s", typed) - } - - return false -} - -func subsetSlice(a, b []interface{}) bool { - // if empty - if len(a) == 0 { - return true - } - // check if len is not greater - if len(a) > len(b) { - return false - } - - for _, av := range a { - if !containsInt(av, b) { - return false - } - } - return true -} - // JoinPatches joins array of serialized JSON patches to the single JSONPatch array func JoinPatches(patches [][]byte) []byte { var result []byte diff --git a/pkg/webhooks/validation.go b/pkg/webhooks/validation.go index 7623f647ec..9afee7d96f 100644 --- a/pkg/webhooks/validation.go +++ b/pkg/webhooks/validation.go @@ -90,8 +90,6 @@ func (ws *WebhookServer) HandleValidation(request *v1beta1.AdmissionRequest, pat glog.V(2).Infof("Handling validation for Kind=%s, Namespace=%s Name=%s UID=%s patchOperation=%s", resource.GetKind(), resource.GetNamespace(), resource.GetName(), request.UID, request.Operation) - // glog.V(4).Infof("Validating resource %s/%s/%s with policy %s with %d rules\n", resource.GetKind(), resource.GetNamespace(), resource.GetName(), policy.ObjectMeta.Name, len(policy.Spec.Rules)) - engineResponse := engine.Validate(*policy, *resource) engineResponses = append(engineResponses, engineResponse) // Gather policy application statistics diff --git a/samples/best_practices/require_namespace_quota.yaml b/samples/best_practices/require_namespace_quota.yaml index f00a091b98..134af75a51 100644 --- a/samples/best_practices/require_namespace_quota.yaml +++ b/samples/best_practices/require_namespace_quota.yaml @@ -20,7 +20,7 @@ spec: data: spec: hard: - requests.cpu: '4' - requests.memory: "16Gi" - limits.cpu: '4' - limits.memory: "16Gi" \ No newline at end of file + requests.cpu: 4 + requests.memory: 16Gi + limits.cpu: 4 + limits.memory: 16Gi \ No newline at end of file diff --git a/samples/more/policy_validate_user_group_fsgroup_id.yaml b/samples/more/policy_validate_user_group_fsgroup_id.yaml index ba6bc3bded..853d8fd06e 100644 --- a/samples/more/policy_validate_user_group_fsgroup_id.yaml +++ b/samples/more/policy_validate_user_group_fsgroup_id.yaml @@ -20,7 +20,7 @@ spec: pattern: spec: securityContext: - runAsUser: '1000' + runAsUser: 1000 - name: validate-groupid match: resources: @@ -31,7 +31,7 @@ spec: pattern: spec: securityContext: - runAsGroup: '3000' + runAsGroup: 3000 - name: validate-fsgroup match: resources: @@ -42,7 +42,7 @@ spec: pattern: spec: securityContext: - fsGroup: '2000' + fsGroup: 2000 # Alls processes inside the pod can be made to run with specific user and groupID by setting runAsUser and runAsGroup respectively. # fsGroup can be specified to make sure any file created in the volume with have the specified groupID. # The above parameters can also be used in a validate policy to restrict user & group IDs. \ No newline at end of file From 92c96aaf1f174ad4e2dede5ddfb07dea45b44c77 Mon Sep 17 00:00:00 2001 From: Shivkumar Dudhani Date: Thu, 31 Oct 2019 13:21:38 -0700 Subject: [PATCH 12/12] Revert "use validatepattern in generate rule to check for subset existance" --- pkg/engine/generation.go | 36 +++- pkg/engine/pattern.go | 25 +-- pkg/utils/json.go | 188 ++++++++++++++++++ pkg/webhooks/validation.go | 2 + .../require_namespace_quota.yaml | 8 +- ...policy_validate_user_group_fsgroup_id.yaml | 6 +- 6 files changed, 230 insertions(+), 35 deletions(-) diff --git a/pkg/engine/generation.go b/pkg/engine/generation.go index ff62a72de7..91bfc451db 100644 --- a/pkg/engine/generation.go +++ b/pkg/engine/generation.go @@ -1,6 +1,7 @@ package engine import ( + "encoding/json" "time" "fmt" @@ -8,6 +9,7 @@ import ( "github.com/golang/glog" kyverno "github.com/nirmata/kyverno/pkg/api/kyverno/v1alpha1" client "github.com/nirmata/kyverno/pkg/dclient" + "github.com/nirmata/kyverno/pkg/utils" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" @@ -147,12 +149,38 @@ func applyRuleGenerator(client *client.Client, ns unstructured.Unstructured, rul //checkResource checks if the config is present in th eresource func checkResource(config interface{}, resource *unstructured.Unstructured) (bool, error) { + var err error - // we are checking if config is a subset of resource with default pattern - path, err := validateResourceWithPattern(resource.Object, config) + objByte, err := resource.MarshalJSON() if err != nil { - glog.V(4).Infof("config not a subset of resource. failed at path %s: %v", path, err) + // unable to parse the json return false, err } - return true, nil + err = resource.UnmarshalJSON(objByte) + if err != nil { + // unable to parse the json + return false, err + } + // marshall and unmarshall json to verify if its right format + configByte, err := json.Marshal(config) + if err != nil { + // unable to marshall the config + return false, err + } + var configData interface{} + err = json.Unmarshal(configByte, &configData) + if err != nil { + // unable to unmarshall + return false, err + } + + var objData interface{} + err = json.Unmarshal(objByte, &objData) + if err != nil { + // unable to unmarshall + return false, err + } + + // Check if the config is a subset of resource + return utils.JSONsubsetValue(configData, objData), nil } diff --git a/pkg/engine/pattern.go b/pkg/engine/pattern.go index 421398509f..185f993e2e 100644 --- a/pkg/engine/pattern.go +++ b/pkg/engine/pattern.go @@ -3,7 +3,6 @@ package engine import ( "math" "regexp" - "strconv" "strings" "github.com/golang/glog" @@ -92,14 +91,6 @@ func validateValueWithIntPattern(value interface{}, pattern int64) bool { glog.Warningf("Expected int, found float: %f\n", typedValue) return false - case string: - // extract int64 from string - int64Num, err := strconv.ParseInt(typedValue, 10, 64) - if err != nil { - glog.Warningf("Failed to parse int64 from string: %v", err) - return false - } - return int64Num == pattern default: glog.Warningf("Expected int, found: %T\n", value) return false @@ -114,25 +105,11 @@ func validateValueWithFloatPattern(value interface{}, pattern float64) bool { if pattern == math.Trunc(pattern) { return int(pattern) == value } - glog.Warningf("Expected float, found int: %d\n", typedValue) - return false - case int64: - // check that float has no fraction - if pattern == math.Trunc(pattern) { - return int64(pattern) == value - } + glog.Warningf("Expected float, found int: %d\n", typedValue) return false case float64: return typedValue == pattern - case string: - // extract float64 from string - float64Num, err := strconv.ParseFloat(typedValue, 64) - if err != nil { - glog.Warningf("Failed to parse float64 from string: %v", err) - return false - } - return float64Num == pattern default: glog.Warningf("Expected float, found: %T\n", value) return false diff --git a/pkg/utils/json.go b/pkg/utils/json.go index 471714958f..651e86c049 100644 --- a/pkg/utils/json.go +++ b/pkg/utils/json.go @@ -1,5 +1,193 @@ package utils +import ( + "github.com/golang/glog" +) + +//JSONsubsetValue checks if JSON a is contained in JSON b +func JSONsubsetValue(a interface{}, b interface{}) bool { + switch typed := a.(type) { + case bool: + bv, ok := b.(bool) + if !ok { + glog.Errorf("expected bool found %T", b) + return false + } + av, _ := a.(bool) + if av == bv { + return true + } + case int: + bv, ok := b.(int) + if !ok { + glog.Errorf("expected int found %T", b) + return false + } + av, _ := a.(int) + if av == bv { + return true + } + case float64: + bv, ok := b.(float64) + if !ok { + glog.Errorf("expected float64 found %T", b) + return false + } + av, _ := a.(float64) + if av == bv { + return true + } + + case string: + bv, ok := b.(string) + if !ok { + glog.Errorf("expected string found %T", b) + return false + } + av, _ := a.(string) + if av == bv { + return true + } + + case map[string]interface{}: + bv, ok := b.(map[string]interface{}) + if !ok { + glog.Errorf("expected map[string]interface{} found %T", b) + return false + } + av, _ := a.(map[string]interface{}) + return subsetMap(av, bv) + case []interface{}: + // TODO: verify the logic + bv, ok := b.([]interface{}) + if !ok { + glog.Errorf("expected []interface{} found %T", b) + return false + } + av, _ := a.([]interface{}) + return subsetSlice(av, bv) + default: + glog.Errorf("Unspported type %s", typed) + + } + return false +} + +func subsetMap(a, b map[string]interface{}) bool { + // check if keys are present + for k := range a { + if _, ok := b[k]; !ok { + glog.Errorf("key %s, not present in resource", k) + return false + } + } + // check if values for the keys match + for ak, av := range a { + bv := b[ak] + if !JSONsubsetValue(av, bv) { + return false + } + } + return true +} + +func containsInt(a interface{}, b []interface{}) bool { + switch typed := a.(type) { + case bool: + for _, bv := range b { + bv, ok := bv.(bool) + if !ok { + return false + } + av, _ := a.(bool) + + if bv == av { + return true + } + } + case int: + for _, bv := range b { + bv, ok := bv.(int) + if !ok { + return false + } + av, _ := a.(int) + + if bv == av { + return true + } + } + case float64: + for _, bv := range b { + bv, ok := bv.(float64) + if !ok { + return false + } + av, _ := a.(float64) + + if bv == av { + return true + } + } + case string: + for _, bv := range b { + bv, ok := bv.(string) + if !ok { + return false + } + av, _ := a.(string) + + if bv == av { + return true + } + } + case map[string]interface{}: + for _, bv := range b { + bv, ok := bv.(map[string]interface{}) + if !ok { + return false + } + av, _ := a.(map[string]interface{}) + if subsetMap(av, bv) { + return true + } + } + case []interface{}: + for _, bv := range b { + bv, ok := bv.([]interface{}) + if !ok { + return false + } + av, _ := a.([]interface{}) + if JSONsubsetValue(av, bv) { + return true + } + } + default: + glog.Errorf("Unspported type %s", typed) + } + + return false +} + +func subsetSlice(a, b []interface{}) bool { + // if empty + if len(a) == 0 { + return true + } + // check if len is not greater + if len(a) > len(b) { + return false + } + + for _, av := range a { + if !containsInt(av, b) { + return false + } + } + return true +} + // JoinPatches joins array of serialized JSON patches to the single JSONPatch array func JoinPatches(patches [][]byte) []byte { var result []byte diff --git a/pkg/webhooks/validation.go b/pkg/webhooks/validation.go index 9afee7d96f..7623f647ec 100644 --- a/pkg/webhooks/validation.go +++ b/pkg/webhooks/validation.go @@ -90,6 +90,8 @@ func (ws *WebhookServer) HandleValidation(request *v1beta1.AdmissionRequest, pat glog.V(2).Infof("Handling validation for Kind=%s, Namespace=%s Name=%s UID=%s patchOperation=%s", resource.GetKind(), resource.GetNamespace(), resource.GetName(), request.UID, request.Operation) + // glog.V(4).Infof("Validating resource %s/%s/%s with policy %s with %d rules\n", resource.GetKind(), resource.GetNamespace(), resource.GetName(), policy.ObjectMeta.Name, len(policy.Spec.Rules)) + engineResponse := engine.Validate(*policy, *resource) engineResponses = append(engineResponses, engineResponse) // Gather policy application statistics diff --git a/samples/best_practices/require_namespace_quota.yaml b/samples/best_practices/require_namespace_quota.yaml index 134af75a51..f00a091b98 100644 --- a/samples/best_practices/require_namespace_quota.yaml +++ b/samples/best_practices/require_namespace_quota.yaml @@ -20,7 +20,7 @@ spec: data: spec: hard: - requests.cpu: 4 - requests.memory: 16Gi - limits.cpu: 4 - limits.memory: 16Gi \ No newline at end of file + requests.cpu: '4' + requests.memory: "16Gi" + limits.cpu: '4' + limits.memory: "16Gi" \ No newline at end of file diff --git a/samples/more/policy_validate_user_group_fsgroup_id.yaml b/samples/more/policy_validate_user_group_fsgroup_id.yaml index 853d8fd06e..ba6bc3bded 100644 --- a/samples/more/policy_validate_user_group_fsgroup_id.yaml +++ b/samples/more/policy_validate_user_group_fsgroup_id.yaml @@ -20,7 +20,7 @@ spec: pattern: spec: securityContext: - runAsUser: 1000 + runAsUser: '1000' - name: validate-groupid match: resources: @@ -31,7 +31,7 @@ spec: pattern: spec: securityContext: - runAsGroup: 3000 + runAsGroup: '3000' - name: validate-fsgroup match: resources: @@ -42,7 +42,7 @@ spec: pattern: spec: securityContext: - fsGroup: 2000 + fsGroup: '2000' # Alls processes inside the pod can be made to run with specific user and groupID by setting runAsUser and runAsGroup respectively. # fsGroup can be specified to make sure any file created in the volume with have the specified groupID. # The above parameters can also be used in a validate policy to restrict user & group IDs. \ No newline at end of file