1
0
Fork 0
mirror of https://github.com/kyverno/kyverno.git synced 2025-03-31 03:45:17 +00:00

Merge commit '5b8ab3842b43a72cc675b93b8b72e290adfca1d2' into 518_pod_controller

# Conflicts:
#	pkg/api/kyverno/v1/types.go
#	pkg/engine/mutation.go
#	pkg/engine/mutation_test.go
#	pkg/engine/validation.go
#	pkg/policy/existing.go
This commit is contained in:
Shuting Zhao 2020-01-02 10:32:17 -08:00
commit d36934fe11
43 changed files with 639 additions and 285 deletions

View file

@ -19,7 +19,11 @@ before_install:
install: true
script:
- make build || travis_terminate 1;
# build initContainer
- make initContainer || travis_terminate 1;
# build kyverno container
- make kyverno || travis_terminate 1;
# tests
- make test-all || travis_terminate 1;
after_script:

View file

@ -19,8 +19,7 @@ TIMESTAMP := $(shell date '+%Y-%m-%d_%I:%M:%S%p')
##################################
KYVERNO_PATH:= cmd/kyverno
build:
GOOS=$(GOOS) go build -o $(PWD)/$(KYVERNO_PATH)/kyverno -ldflags=$(LD_FLAGS) $(PWD)/$(KYVERNO_PATH)/main.go
build: kyverno
##################################
# INIT CONTAINER
@ -52,10 +51,13 @@ docker-push-initContainer:
.PHONY: docker-build-kyverno docker-tag-repo-kyverno docker-push-kyverno
KYVERNO_PATH := cmd/kyverno
KYVERNO_IMAGE := kyverno
kyverno:
GOOS=$(GOOS) go build -o $(PWD)/$(KYVERNO_PATH)/kyverno -ldflags=$(LD_FLAGS) $(PWD)/$(KYVERNO_PATH)/main.go
docker-publish-kyverno: docker-build-kyverno docker-tag-repo-kyverno docker-push-kyverno
docker-build-kyverno:
GO_ENABLED=0 GOOS=linux go build -o $(PWD)/$(KYVERNO_PATH)/kyverno -ldflags=$(LD_FLAGS) $(PWD)/$(KYVERNO_PATH)/main.go
CGO_ENABLED=0 GOOS=linux go build -o $(PWD)/$(KYVERNO_PATH)/kyverno -ldflags=$(LD_FLAGS) $(PWD)/$(KYVERNO_PATH)/main.go
@docker build -f $(PWD)/$(KYVERNO_PATH)/Dockerfile -t $(REGISTRY)/nirmata/$(KYVERNO_IMAGE):$(IMAGE_TAG) $(PWD)/$(KYVERNO_PATH)
docker-tag-repo-kyverno:

View file

@ -31,9 +31,6 @@ func main() {
defer glog.Flush()
// os signal handler
stopCh := signal.SetupSignalHandler()
// arguments
flag.StringVar(&kubeconfig, "kubeconfig", "", "Path to a kubeconfig. Only required if out-of-cluster.")
// create client config
clientConfig, err := createClientConfig(kubeconfig)
if err != nil {
@ -82,6 +79,15 @@ func main() {
}
}
func init() {
// arguments
flag.StringVar(&kubeconfig, "kubeconfig", "", "Path to a kubeconfig. Only required if out-of-cluster.")
flag.Set("logtostderr", "true")
flag.Set("stderrthreshold", "WARNING")
flag.Set("v", "2")
flag.Parse()
}
func removeWebhookIfExists(client *client.Client, kind string, name string) error {
var err error
// Get resource

View file

@ -64,6 +64,7 @@ type Policy struct {
type Spec struct {
Rules []Rule `json:"rules,omitempty"`
ValidationFailureAction string `json:"validationFailureAction"`
Background bool `json:"background,omitempty"`
}
// Rule is set of mutation, validation and generation actions
@ -79,18 +80,21 @@ type Rule struct {
//MatchResources contains resource description of the resources that the rule is to apply on
type MatchResources struct {
Roles []string `json:"roles,omitempty"`
ClusterRoles []string `json:"clusterRoles,omitempty"`
Subjects []rbacv1.Subject `json:"subjects,omitempty"`
UserInfo
ResourceDescription `json:"resources"`
}
//ExcludeResources container resource description of the resources that are to be excluded from the applying the policy rule
type ExcludeResources struct {
Roles []string `json:"roles,omitempty"`
ClusterRoles []string `json:"clusterRoles,omitempty"`
Subjects []rbacv1.Subject `json:"subjects,omitempty"`
ResourceDescription `json:"resources,omitempty"`
UserInfo
ResourceDescription `json:"resources"`
}
// UserInfo filter based on users
type UserInfo struct {
Roles []string `json:"roles,omitempty"`
ClusterRoles []string `json:"clusterRoles,omitempty"`
Subjects []rbacv1.Subject `json:"subjects,omitempty"`
}
// ResourceDescription describes the resource to which the PolicyRule will be applied.

View file

@ -27,8 +27,7 @@ type EvalInterface interface {
//Context stores the data resources as JSON
type Context struct {
mu sync.RWMutex
// data map[string]interface{}
mu sync.RWMutex
jsonRaw []byte
}
@ -55,28 +54,7 @@ func (ctx *Context) AddJSON(dataRaw []byte) error {
return nil
}
// //Add adds resource with the key
// // we always overwrite the resoruce if already present
// func (ctx *Context) Add(key string, resource []byte) error {
// ctx.mu.Lock()
// defer ctx.mu.Unlock()
// // insert/update
// // umarshall before adding
// var data interface{}
// if err := json.Unmarshal(resource, &data); err != nil {
// glog.V(4).Infof("failed to unmarshall resource in context: %v", err)
// fmt.Println(err)
// return err
// }
// ctx.data[key] = data
// return nil
// }
// func (ctx *Context) getData() interface{} {
// return ctx.data
// }
//Add data at path: request.object
//AddResource adds data at path: request.object
func (ctx *Context) AddResource(dataRaw []byte) error {
// unmarshall the resource struct
@ -104,6 +82,7 @@ func (ctx *Context) AddResource(dataRaw []byte) error {
return ctx.AddJSON(objRaw)
}
//AddUserInfo adds data at path: request.userInfo
func (ctx *Context) AddUserInfo(userInfo authenticationv1.UserInfo) error {
modifiedResource := struct {
Request interface{} `json:"request"`

View file

@ -7,25 +7,7 @@ import (
"github.com/jmespath/go-jmespath"
)
// //Query searches for query in the context
// func (ctx *Context) Query(query string) (interface{}, error) {
// var emptyResult interface{}
// // compile the query
// queryPath, err := jmespath.Compile(query)
// if err != nil {
// glog.V(4).Infof("incorrect query %s: %v", query, err)
// return emptyResult, err
// }
// // search
// result, err := queryPath.Search(ctx.getData())
// if err != nil {
// glog.V(4).Infof("failed to search query %s: %v", query, err)
// return emptyResult, err
// }
// return result, nil
// }
//Query ...
//Query the JSON context with JMESPATH search path
func (ctx *Context) Query(query string) (interface{}, error) {
var emptyResult interface{}
// compile the query

View file

@ -63,7 +63,7 @@ func Test_VariableSubstitutionOverlay(t *testing.T) {
}
}
`)
expectedPatch := []byte(`{ "op": "add", "path": "/metadata/labels", "value": {"appname":"check-root-user"} }`)
expectedPatch := []byte(`{ "op": "add", "path": "/metadata/labels", "value":{"appname":"check-root-user"} }`)
var policy kyverno.ClusterPolicy
json.Unmarshal(rawPolicy, &policy)

View file

@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"reflect"
"regexp"
"strconv"
"strings"
"time"
@ -17,7 +18,6 @@ import (
"github.com/nirmata/kyverno/pkg/engine/anchor"
"github.com/nirmata/kyverno/pkg/engine/context"
"github.com/nirmata/kyverno/pkg/engine/response"
"github.com/nirmata/kyverno/pkg/engine/validate"
"github.com/nirmata/kyverno/pkg/engine/variables"
)
@ -352,13 +352,19 @@ func processSubtree(overlay interface{}, path string, op string) ([]byte, error)
path = preparePath(path)
value := prepareJSONValue(overlay)
patchStr := fmt.Sprintf(`{ "op": "%s", "path": "%s", "value": %s }`, op, path, value)
patchStr := fmt.Sprintf(`{ "op": "%s", "path": "%s", "value":%s }`, op, path, value)
// explicitly handle boolean type in annotation
// keep the type boolean as it is in any other fields
if strings.Contains(path, "/metadata/annotations") {
patchStr = wrapBoolean(patchStr)
}
// check the patch
_, err := jsonpatch.DecodePatch([]byte("[" + patchStr + "]"))
if err != nil {
glog.V(3).Info(err)
return nil, fmt.Errorf("Failed to make '%s' patch from an overlay '%s' for path %s", op, value, path)
return nil, fmt.Errorf("Failed to make '%s' patch from an overlay '%s' for path %s, err: %v", op, value, path, err)
}
return []byte(patchStr), nil
@ -382,13 +388,14 @@ func preparePath(path string) string {
// converts overlay to JSON string to be inserted into the JSON Patch
func prepareJSONValue(overlay interface{}) string {
var err error
// Need to remove anchors from the overlay struct
overlayWithoutAnchors := removeAnchorFromSubTree(overlay)
jsonOverlay, err := json.Marshal(overlayWithoutAnchors)
if err != nil || hasOnlyAnchors(overlay) {
glog.V(3).Info(err)
return ""
}
// Need to remove anchors from the overlay struct
overlayWithoutAnchors := removeAnchorFromSubTree(overlay)
jsonOverlay, err := json.Marshal(overlayWithoutAnchors)
return string(jsonOverlay)
}
@ -471,20 +478,15 @@ func hasNestedAnchors(overlay interface{}) bool {
}
}
// Checks if array object matches anchors. If not - skip - return true
func skipArrayObject(object, anchors map[string]interface{}) bool {
for key, pattern := range anchors {
key = key[1 : len(key)-1]
value, ok := object[key]
if !ok {
return true
}
if !validate.ValidateValueWithPattern(value, pattern) {
return true
}
func wrapBoolean(patchStr string) string {
reTrue := regexp.MustCompile(`:\s*true\s*`)
if idx := reTrue.FindStringIndex(patchStr); len(idx) != 0 {
return fmt.Sprintf("%s:\"true\"%s", patchStr[:idx[0]], patchStr[idx[1]:])
}
return false
reFalse := regexp.MustCompile(`:\s*false\s*`)
if idx := reFalse.FindStringIndex(patchStr); len(idx) != 0 {
return fmt.Sprintf("%s:\"false\"%s", patchStr[:idx[0]], patchStr[idx[1]:])
}
return patchStr
}

View file

@ -885,7 +885,7 @@ func TestProcessOverlayPatches_insertWithCondition(t *testing.T) {
var resource, overlay interface{}
json.Unmarshal(resourceRawAnchorOnPeers, &resource)
json.Unmarshal(resourceRaw, &resource)
json.Unmarshal(overlayRaw, &overlay)
patches, overlayerr := processOverlayPatches(resource, overlay)
@ -1013,7 +1013,7 @@ func TestProcessOverlayPatches_InsertIfNotPresentWithConditions(t *testing.T) {
"metadata": {
"name": "pod-with-emptydir",
"annotations": {
"cluster-autoscaler.kubernetes.io/safe-to-evict": true
"cluster-autoscaler.kubernetes.io/safe-to-evict": "true"
}
},
"spec": {
@ -1038,9 +1038,40 @@ func TestProcessOverlayPatches_InsertIfNotPresentWithConditions(t *testing.T) {
}
}`)
t.Log(string(doc))
compareJSONAsMap(t, expectedResult, doc)
}
func Test_wrapBoolean(t *testing.T) {
tests := []struct {
test string
expected string
}{
{
test: `{ "op": "add", "path": "/metadata/annotations", "value":{"cluster-autoscaler.kubernetes.io/safe-to-evict":true} }`,
expected: `{ "op": "add", "path": "/metadata/annotations", "value":{"cluster-autoscaler.kubernetes.io/safe-to-evict":"true"} }`,
},
{
test: `{ "op": "add", "path": "/metadata/annotations", "value":{"cluster-autoscaler.kubernetes.io/safe-to-evict": true} }`,
expected: `{ "op": "add", "path": "/metadata/annotations", "value":{"cluster-autoscaler.kubernetes.io/safe-to-evict":"true"} }`,
},
{
test: `{ "op": "add", "path": "/metadata/annotations", "value":{"cluster-autoscaler.kubernetes.io/safe-to-evict": false } }`,
expected: `{ "op": "add", "path": "/metadata/annotations", "value":{"cluster-autoscaler.kubernetes.io/safe-to-evict":"false"} }`,
},
{
test: `{ "op": "add", "path": "/metadata/annotations/cluster-autoscaler.kubernetes.io~1safe-to-evict", "value": false }`,
expected: `{ "op": "add", "path": "/metadata/annotations/cluster-autoscaler.kubernetes.io~1safe-to-evict", "value":"false"}`,
},
}
for _, testcase := range tests {
out := wrapBoolean(testcase.test)
t.Log(out)
assert.Assert(t, testcase.expected == out)
}
}
func TestApplyOverlay_ConditionOnArray(t *testing.T) {
resourceRaw := []byte(`
{
@ -1119,7 +1150,7 @@ func TestApplyOverlay_ConditionOnArray(t *testing.T) {
assert.NilError(t, json.Unmarshal(overlayRaw, &overlay))
expectedPatches := []byte(`[
{ "op": "replace", "path": "/spec/affinity/nodeAffinity/a/b/0/matchExpressions/0/operator", "value": "In" }
{ "op": "replace", "path": "/spec/affinity/nodeAffinity/a/b/0/matchExpressions/0/operator", "value":"In" }
]`)
p, err := applyOverlay(resource, overlay, "/")
assert.NilError(t, err)

View file

@ -0,0 +1,55 @@
package policy
import (
"fmt"
kyverno "github.com/nirmata/kyverno/pkg/api/kyverno/v1"
"github.com/nirmata/kyverno/pkg/engine/variables"
)
//ContainsUserInfo returns error is userInfo is defined
func ContainsUserInfo(policy kyverno.ClusterPolicy) error {
// iterate of the policy rules to identify if userInfo is used
for idx, rule := range policy.Spec.Rules {
if err := userInfoDefined(rule.MatchResources.UserInfo); err != nil {
return fmt.Errorf("path: spec/rules[%d]/match/%s", idx, err)
}
if err := userInfoDefined(rule.ExcludeResources.UserInfo); err != nil {
return fmt.Errorf("path: spec/rules[%d]/exclude/%s", idx, err)
}
// variable defined with user information
// - mutate.overlay
// - validate.pattern
// - validate.anyPattern[*]
// variables to filter
// - request.userInfo
filterVars := []string{"request.userInfo*"}
if err := variables.CheckVariables(rule.Mutation.Overlay, filterVars, "/"); err != nil {
return fmt.Errorf("path: spec/rules[%d]/mutate/overlay%s", idx, err)
}
if err := variables.CheckVariables(rule.Validation.Pattern, filterVars, "/"); err != nil {
return fmt.Errorf("path: spec/rules[%d]/validate/pattern%s", idx, err)
}
for idx2, pattern := range rule.Validation.AnyPattern {
if err := variables.CheckVariables(pattern, filterVars, "/"); err != nil {
return fmt.Errorf("path: spec/rules[%d]/validate/anyPattern[%d]%s", idx, idx2, err)
}
}
}
return nil
}
func userInfoDefined(ui kyverno.UserInfo) error {
if len(ui.Roles) > 0 {
return fmt.Errorf("roles")
}
if len(ui.ClusterRoles) > 0 {
return fmt.Errorf("clusterRoles")
}
if len(ui.Subjects) > 0 {
return fmt.Errorf("subjects")
}
return nil
}

View file

@ -21,6 +21,14 @@ func Validate(p kyverno.ClusterPolicy) error {
if path, err := validateUniqueRuleName(p); err != nil {
return fmt.Errorf("path: spec.%s: %v", path, err)
}
if p.Spec.Background {
if err := ContainsUserInfo(p); err != nil {
// policy.spec.background -> "true"
// - cannot use variables with request.userInfo
// - cannot define userInfo(roles, cluserRoles, subjects) for filtering (match & exclude)
return fmt.Errorf("userInfo not allowed in background policy mode. Failure path %s", err)
}
}
for i, rule := range p.Spec.Rules {
// only one type of rule is allowed per rule

View file

@ -1180,3 +1180,257 @@ func Test_Validate_ServiceAccount(t *testing.T) {
assert.Assert(t, err != nil)
assert.Assert(t, path == "exclude.subjects")
}
func Test_BackGroundUserInfo_match_roles(t *testing.T) {
var err error
rawPolicy := []byte(`
{
"apiVersion": "kyverno.io/v1",
"kind": "ClusterPolicy",
"metadata": {
"name": "disallow-root-user"
},
"spec": {
"rules": [
{
"name": "match.roles",
"match": {
"roles": [
"a",
"b"
]
}
}
]
}
}
`)
var policy *kyverno.ClusterPolicy
err = json.Unmarshal(rawPolicy, &policy)
assert.NilError(t, err)
err = ContainsUserInfo(*policy)
if err.Error() != "path: spec/rules[0]/match/roles" {
t.Error("Incorrect Path")
}
}
func Test_BackGroundUserInfo_match_clusterRoles(t *testing.T) {
var err error
rawPolicy := []byte(`
{
"apiVersion": "kyverno.io/v1",
"kind": "ClusterPolicy",
"metadata": {
"name": "disallow-root-user"
},
"spec": {
"rules": [
{
"name": "match.clusterRoles",
"match": {
"clusterRoles": [
"a",
"b"
]
}
}
]
}
}
`)
var policy *kyverno.ClusterPolicy
err = json.Unmarshal(rawPolicy, &policy)
assert.NilError(t, err)
err = ContainsUserInfo(*policy)
if err.Error() != "path: spec/rules[0]/match/clusterRoles" {
t.Log(err)
t.Error("Incorrect Path")
}
}
func Test_BackGroundUserInfo_match_subjects(t *testing.T) {
var err error
rawPolicy := []byte(`
{
"apiVersion": "kyverno.io/v1",
"kind": "ClusterPolicy",
"metadata": {
"name": "disallow-root-user"
},
"spec": {
"rules": [
{
"name": "match.subjects",
"match": {
"subjects": [
{
"Name": "a"
},
{
"Name": "b"
}
]
}
}
]
}
} `)
var policy *kyverno.ClusterPolicy
err = json.Unmarshal(rawPolicy, &policy)
assert.NilError(t, err)
err = ContainsUserInfo(*policy)
if err.Error() != "path: spec/rules[0]/match/subjects" {
t.Log(err)
t.Error("Incorrect Path")
}
}
func Test_BackGroundUserInfo_mutate_overlay1(t *testing.T) {
var err error
rawPolicy := []byte(`
{
"apiVersion": "kyverno.io/v1",
"kind": "ClusterPolicy",
"metadata": {
"name": "disallow-root-user"
},
"spec": {
"rules": [
{
"name": "mutate.overlay1",
"mutate": {
"overlay": {
"var1": "{{request.userInfo}}"
}
}
}
]
}
}
`)
var policy *kyverno.ClusterPolicy
err = json.Unmarshal(rawPolicy, &policy)
assert.NilError(t, err)
err = ContainsUserInfo(*policy)
if err.Error() != "path: spec/rules[0]/mutate/overlay/var1/{{request.userInfo}}" {
t.Log(err)
t.Error("Incorrect Path")
}
}
func Test_BackGroundUserInfo_mutate_overlay2(t *testing.T) {
var err error
rawPolicy := []byte(`
{
"apiVersion": "kyverno.io/v1",
"kind": "ClusterPolicy",
"metadata": {
"name": "disallow-root-user"
},
"spec": {
"rules": [
{
"name": "mutate.overlay2",
"mutate": {
"overlay": {
"var1": "{{request.userInfo.userName}}"
}
}
}
]
}
}
`)
var policy *kyverno.ClusterPolicy
err = json.Unmarshal(rawPolicy, &policy)
assert.NilError(t, err)
err = ContainsUserInfo(*policy)
if err.Error() != "path: spec/rules[0]/mutate/overlay/var1/{{request.userInfo.userName}}" {
t.Log(err)
t.Error("Incorrect Path")
}
}
func Test_BackGroundUserInfo_validate_pattern(t *testing.T) {
var err error
rawPolicy := []byte(`
{
"apiVersion": "kyverno.io/v1",
"kind": "ClusterPolicy",
"metadata": {
"name": "disallow-root-user"
},
"spec": {
"rules": [
{
"name": "validate.overlay",
"validate": {
"pattern": {
"var1": "{{request.userInfo}}"
}
}
}
]
}
}
`)
var policy *kyverno.ClusterPolicy
err = json.Unmarshal(rawPolicy, &policy)
assert.NilError(t, err)
err = ContainsUserInfo(*policy)
if err.Error() != "path: spec/rules[0]/validate/pattern/var1/{{request.userInfo}}" {
t.Log(err)
t.Error("Incorrect Path")
}
}
func Test_BackGroundUserInfo_validate_anyPattern(t *testing.T) {
var err error
rawPolicy := []byte(`
{
"apiVersion": "kyverno.io/v1",
"kind": "ClusterPolicy",
"metadata": {
"name": "disallow-root-user"
},
"spec": {
"rules": [
{
"name": "validate.anyPattern",
"validate": {
"anyPattern": [
{
"var1": "temp"
},
{
"var1": "{{request.userInfo}}"
}
]
}
}
]
}
} `)
var policy *kyverno.ClusterPolicy
err = json.Unmarshal(rawPolicy, &policy)
assert.NilError(t, err)
err = ContainsUserInfo(*policy)
if err.Error() != "path: spec/rules[0]/validate/anyPattern[1]/var1/{{request.userInfo}}" {
t.Log(err)
t.Error("Incorrect Path")
}
}

View file

@ -29,7 +29,9 @@ func Test_matchAdmissionInfo(t *testing.T) {
{
rule: kyverno.Rule{
MatchResources: kyverno.MatchResources{
Roles: []string{"ns-a:role-a"},
UserInfo: kyverno.UserInfo{
Roles: []string{"ns-a:role-a"},
},
},
},
info: RequestInfo{
@ -40,7 +42,9 @@ func Test_matchAdmissionInfo(t *testing.T) {
{
rule: kyverno.Rule{
MatchResources: kyverno.MatchResources{
Roles: []string{"ns-a:role-a"},
UserInfo: kyverno.UserInfo{
Roles: []string{"ns-a:role-a"},
},
},
},
info: RequestInfo{
@ -51,7 +55,9 @@ func Test_matchAdmissionInfo(t *testing.T) {
{
rule: kyverno.Rule{
MatchResources: kyverno.MatchResources{
Subjects: testSubjects(),
UserInfo: kyverno.UserInfo{
Subjects: testSubjects(),
},
},
},
info: RequestInfo{
@ -64,7 +70,9 @@ func Test_matchAdmissionInfo(t *testing.T) {
{
rule: kyverno.Rule{
ExcludeResources: kyverno.ExcludeResources{
Subjects: testSubjects(),
UserInfo: kyverno.UserInfo{
Subjects: testSubjects(),
},
},
},
info: RequestInfo{
@ -77,7 +85,9 @@ func Test_matchAdmissionInfo(t *testing.T) {
{
rule: kyverno.Rule{
ExcludeResources: kyverno.ExcludeResources{
Subjects: testSubjects(),
UserInfo: kyverno.UserInfo{
Subjects: testSubjects(),
},
},
},
info: RequestInfo{
@ -121,7 +131,9 @@ func Test_validateMatch(t *testing.T) {
}
matchRoles := kyverno.MatchResources{
Roles: []string{"ns-a:role-a", "ns-b:role-b"},
UserInfo: kyverno.UserInfo{
Roles: []string{"ns-a:role-a", "ns-b:role-b"},
},
}
for _, info := range requestInfo {
@ -165,7 +177,9 @@ func Test_validateMatch(t *testing.T) {
}
matchClusterRoles := kyverno.MatchResources{
ClusterRoles: []string{"clusterrole-a", "clusterrole-b"},
UserInfo: kyverno.UserInfo{
ClusterRoles: []string{"clusterrole-a", "clusterrole-b"},
},
}
for _, info := range requestInfo {
@ -199,7 +213,9 @@ func Test_validateExclude(t *testing.T) {
}
excludeRoles := kyverno.ExcludeResources{
Roles: []string{"ns-a:role-a", "ns-b:role-b"},
UserInfo: kyverno.UserInfo{
Roles: []string{"ns-a:role-a", "ns-b:role-b"},
},
}
for _, info := range requestInfo {
@ -237,7 +253,9 @@ func Test_validateExclude(t *testing.T) {
}
excludeClusterRoles := kyverno.ExcludeResources{
ClusterRoles: []string{"clusterrole-a", "clusterrole-b"},
UserInfo: kyverno.UserInfo{
ClusterRoles: []string{"clusterrole-a", "clusterrole-b"},
},
}
for _, info := range requestInfo {

View file

@ -285,24 +285,6 @@ func isStringIsReference(str string) bool {
return str[0] == '$' && str[1] == '(' && str[len(str)-1] == ')'
}
// // Checks if array object matches anchors. If not - skip - return true
// func skipArrayObject(object, anchors map[string]interface{}) bool {
// for key, pattern := range anchors {
// key = key[1 : len(key)-1]
// value, ok := object[key]
// if !ok {
// return true
// }
// if !ValidateValueWithPattern(value, pattern) {
// return true
// }
// }
// return false
// }
// removeAnchor remove special characters around anchored key
func removeAnchor(key string) string {
if anchor.IsConditionAnchor(key) {

View file

@ -245,9 +245,6 @@ func getValueFromPattern(patternMap map[string]interface{}, keys []string, curre
path := ""
/*for i := len(keys) - 1; i >= 0; i-- {
path = keys[i] + path + "/"
}*/
for _, elem := range keys {
path = "/" + elem + path
}

View file

@ -172,7 +172,7 @@ func validatePatterns(ctx context.EvalInterface, resource unstructured.Unstructu
// 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",
resp.Message = fmt.Sprintf("Validation error: %s; Validation rule '%s' failed at path '%s'",
rule.Validation.Message, rule.Name, path)
return resp
}

View file

@ -8,111 +8,6 @@ import (
"gotest.tools/assert"
)
func TestSkipArrayObject_OneAnchor(t *testing.T) {
rawAnchors := []byte(`{
"(name)":"nirmata-*"
}`)
rawResource := []byte(`{
"name":"nirmata-resource",
"namespace":"kyverno",
"object":{
"label":"app",
"array":[
1,
2,
3
]
}
}`)
var resource, anchor map[string]interface{}
json.Unmarshal(rawAnchors, &anchor)
json.Unmarshal(rawResource, &resource)
assert.Assert(t, !skipArrayObject(resource, anchor))
}
func TestSkipArrayObject_OneNumberAnchorPass(t *testing.T) {
rawAnchors := []byte(`{
"(count)":1
}`)
rawResource := []byte(`{
"name":"nirmata-resource",
"count":1,
"namespace":"kyverno",
"object":{
"label":"app",
"array":[
1,
2,
3
]
}
}`)
var resource, anchor map[string]interface{}
json.Unmarshal(rawAnchors, &anchor)
json.Unmarshal(rawResource, &resource)
assert.Assert(t, !skipArrayObject(resource, anchor))
}
func TestSkipArrayObject_TwoAnchorsPass(t *testing.T) {
rawAnchors := []byte(`{
"(name)":"nirmata-*",
"(namespace)":"kyv?rno"
}`)
rawResource := []byte(`{
"name":"nirmata-resource",
"namespace":"kyverno",
"object":{
"label":"app",
"array":[
1,
2,
3
]
}
}`)
var resource, anchor map[string]interface{}
json.Unmarshal(rawAnchors, &anchor)
json.Unmarshal(rawResource, &resource)
assert.Assert(t, !skipArrayObject(resource, anchor))
}
func TestSkipArrayObject_TwoAnchorsSkip(t *testing.T) {
rawAnchors := []byte(`{
"(name)":"nirmata-*",
"(namespace)":"some-?olicy"
}`)
rawResource := []byte(`{
"name":"nirmata-resource",
"namespace":"kyverno",
"object":{
"label":"app",
"array":[
1,
2,
3
]
}
}`)
var resource, anchor map[string]interface{}
json.Unmarshal(rawAnchors, &anchor)
json.Unmarshal(rawResource, &resource)
assert.Assert(t, skipArrayObject(resource, anchor))
}
func TestGetAnchorsFromMap_ThereAreAnchors(t *testing.T) {
rawMap := []byte(`{
"(name)":"nirmata-*",
@ -413,7 +308,7 @@ func TestValidate_image_tag_fail(t *testing.T) {
assert.NilError(t, err)
msgs := []string{
"Validation rule 'validate-tag' succeeded.",
"Validation error: imagePullPolicy 'Always' required with tag 'latest'; Validation rule validate-latest failed at path /spec/containers/0/imagePullPolicy/",
"Validation error: imagePullPolicy 'Always' required with tag 'latest'; Validation rule 'validate-latest' failed at path '/spec/containers/0/imagePullPolicy/'",
}
er := Validate(PolicyContext{Policy: policy, NewResource: *resourceUnstructured})
for index, r := range er.PolicyResponse.Rules {
@ -668,7 +563,7 @@ func TestValidate_host_network_port(t *testing.T) {
resourceUnstructured, err := ConvertToUnstructured(rawResource)
assert.NilError(t, err)
er := Validate(PolicyContext{Policy: policy, NewResource: *resourceUnstructured})
msgs := []string{"Validation error: Host network and port are not allowed; Validation rule validate-host-network-port failed at path /spec/containers/0/ports/0/hostPort/"}
msgs := []string{"Validation error: Host network and port are not allowed; Validation rule 'validate-host-network-port' failed at path '/spec/containers/0/ports/0/hostPort/'"}
for index, r := range er.PolicyResponse.Rules {
assert.Equal(t, r.Message, msgs[index])
@ -845,7 +740,7 @@ func TestValidate_anchor_arraymap_fail(t *testing.T) {
resourceUnstructured, err := ConvertToUnstructured(rawResource)
assert.NilError(t, err)
er := Validate(PolicyContext{Policy: policy, NewResource: *resourceUnstructured})
msgs := []string{"Validation error: Host path '/var/lib/' is not allowed; Validation rule validate-host-path failed at path /spec/volumes/0/hostPath/path/"}
msgs := []string{"Validation error: Host path '/var/lib/' is not allowed; Validation rule 'validate-host-path' failed at path '/spec/volumes/0/hostPath/path/'"}
for index, r := range er.PolicyResponse.Rules {
assert.Equal(t, r.Message, msgs[index])
@ -1058,7 +953,7 @@ func TestValidate_anchor_map_found_invalid(t *testing.T) {
resourceUnstructured, err := ConvertToUnstructured(rawResource)
assert.NilError(t, err)
er := Validate(PolicyContext{Policy: policy, NewResource: *resourceUnstructured})
msgs := []string{"Validation error: pod: validate run as non root user; Validation rule pod rule 2 failed at path /spec/securityContext/runAsNonRoot/"}
msgs := []string{"Validation error: pod: validate run as non root user; Validation rule 'pod rule 2' failed at path '/spec/securityContext/runAsNonRoot/'"}
for index, r := range er.PolicyResponse.Rules {
assert.Equal(t, r.Message, msgs[index])
@ -1442,7 +1337,7 @@ func TestValidate_negationAnchor_deny(t *testing.T) {
resourceUnstructured, err := ConvertToUnstructured(rawResource)
assert.NilError(t, err)
er := Validate(PolicyContext{Policy: policy, NewResource: *resourceUnstructured})
msgs := []string{"Validation error: Host path is not allowed; Validation rule validate-host-path failed at path /spec/volumes/0/hostPath/"}
msgs := []string{"Validation error: Host path is not allowed; Validation rule 'validate-host-path' failed at path '/spec/volumes/0/hostPath/'"}
for index, r := range er.PolicyResponse.Rules {
assert.Equal(t, r.Message, msgs[index])

View file

@ -81,7 +81,7 @@ func getValueQuery(ctx context.EvalInterface, valuePattern string) interface{} {
// search for the path in ctx
variable, err := ctx.Query(searchPath)
if err != nil {
glog.V(4).Infof("variable substituion failed for query %s: %v", searchPath, err)
glog.V(4).Infof("variable substitution failed for query %s: %v", searchPath, err)
return emptyInterface
}
return variable

View file

@ -0,0 +1,69 @@
package variables
import (
"fmt"
"regexp"
"strconv"
)
//CheckVariables checks if the variable regex has been used
func CheckVariables(pattern interface{}, variables []string, path string) error {
switch typedPattern := pattern.(type) {
case map[string]interface{}:
return checkMap(typedPattern, variables, path)
case []interface{}:
return checkArray(typedPattern, variables, path)
case string:
return checkValue(typedPattern, variables, path)
default:
return nil
}
}
func checkMap(patternMap map[string]interface{}, variables []string, path string) error {
for patternKey, patternElement := range patternMap {
if err := CheckVariables(patternElement, variables, path+patternKey+"/"); err != nil {
return err
}
}
return nil
}
func checkArray(patternList []interface{}, variables []string, path string) error {
for idx, patternElement := range patternList {
if err := CheckVariables(patternElement, variables, path+strconv.Itoa(idx)+"/"); err != nil {
return err
}
}
return nil
}
func checkValue(valuePattern string, variables []string, path string) error {
operatorVariable := getOperator(valuePattern)
variable := valuePattern[len(operatorVariable):]
if checkValueVariable(variable, variables) {
return fmt.Errorf(path + valuePattern)
}
return nil
}
func checkValueVariable(valuePattern string, variables []string) bool {
variableRegex := regexp.MustCompile("^{{(.*)}}$")
groups := variableRegex.FindStringSubmatch(valuePattern)
if len(groups) < 2 {
return false
}
return variablePatternSearch(groups[1], variables)
}
func variablePatternSearch(pattern string, regexs []string) bool {
for _, regex := range regexs {
varRegex := regexp.MustCompile(regex)
found := varRegex.FindString(pattern)
if found != "" {
return true
}
}
return true
}

View file

@ -155,20 +155,31 @@ func NewPolicyController(kyvernoClient *kyvernoclient.Clientset,
func (pc *PolicyController) addPolicy(obj interface{}) {
p := obj.(*kyverno.ClusterPolicy)
glog.V(4).Infof("Adding Policy %s", p.Name)
// Only process policies that are enabled for "background" execution
// policy.spec.background -> "True"
// register with policy meta-store
pc.pMetaStore.Register(*p)
if !p.Spec.Background {
return
}
glog.V(4).Infof("Adding Policy %s", p.Name)
pc.enqueuePolicy(p)
}
func (pc *PolicyController) updatePolicy(old, cur interface{}) {
oldP := old.(*kyverno.ClusterPolicy)
curP := cur.(*kyverno.ClusterPolicy)
glog.V(4).Infof("Updating Policy %s", oldP.Name)
// TODO: optimize this : policy meta-store
// Update policy-> (remove,add)
pc.pMetaStore.UnRegister(*oldP)
pc.pMetaStore.Register(*curP)
// Only process policies that are enabled for "background" execution
// policy.spec.background -> "True"
if !curP.Spec.Background {
return
}
glog.V(4).Infof("Updating Policy %s", oldP.Name)
pc.enqueuePolicy(curP)
}
@ -189,6 +200,8 @@ func (pc *PolicyController) deletePolicy(obj interface{}) {
glog.V(4).Infof("Deleting Policy %s", p.Name)
// Unregister from policy meta-store
pc.pMetaStore.UnRegister(*p)
// we process policies that are not set of background processing as we need to perform policy violation
// cleanup when a policy is deleted.
pc.enqueuePolicy(p)
}

View file

@ -71,6 +71,7 @@ func (ws *WebhookServer) HandleMutation(request *v1beta1.AdmissionRequest, polic
policyContext := engine.PolicyContext{
NewResource: *resource,
Context: ctx,
AdmissionInfo: engine.RequestInfo{
Roles: roles,
ClusterRoles: clusterRoles,

View file

@ -240,11 +240,11 @@ func TestGeneratePodControllerRule(t *testing.T) {
"apiVersion": "kyverno.io/v1",
"kind": "ClusterPolicy",
"metadata": {
"name": "add-safe-to-evict",
"annotations": {
"a": "b",
"pod-policies.kyverno.io/autogen-controllers": "all"
}
},
"name": "add-safe-to-evict"
},
"spec": {
"rules": [
@ -441,6 +441,7 @@ func TestGeneratePodControllerRule(t *testing.T) {
}
},
"validate": {
"message": "Running as root user is not allowed. Set runAsNonRoot to true",
"anyPattern": [
{
"spec": {
@ -485,6 +486,7 @@ func TestGeneratePodControllerRule(t *testing.T) {
}
},
"validate": {
"message": "Use of the Docker Unix socket is not allowed",
"pattern": {
"spec": {
"template": {
@ -505,5 +507,7 @@ func TestGeneratePodControllerRule(t *testing.T) {
]
}
}`)
t.Log(string(expectPolicy))
t.Log(string(p))
compareJSONAsMap(t, expectPolicy, p)
}

View file

@ -15,7 +15,7 @@ metadata:
name: add-networkpolicy
spec:
rules:
- name: "default-deny-ingress"
- name: default-deny-ingress
match:
resources:
kinds:

View file

@ -24,7 +24,7 @@ spec:
- Namespace
generate:
kind: ResourceQuota
name: "default-resourcequota"
name: default-resourcequota
data:
spec:
hard:
@ -39,7 +39,7 @@ spec:
- Namespace
generate:
kind: LimitRange
name: "default-limitrange"
name: default-limitrange
data:
spec:
limits:

View file

@ -13,17 +13,17 @@ This policy matches and mutates pods with `emptyDir` and `hostPath` volumes, to
[add_safe_to_evict_annotation.yaml](best_practices/add_safe_to_evict.yaml)
````yaml
apiVersion: "kyverno.io/v1"
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: "add-safe-to-evict"
name: add-safe-to-evict
spec:
rules:
- name: "annotate-empty-dir"
match:
resources:
kinds:
- "Pod"
- Pod
mutate:
overlay:
metadata:
@ -32,11 +32,11 @@ spec:
spec:
volumes:
- (emptyDir): {}
- name: "annotate-host-path"
- name: annotate-host-path
match:
resources:
kinds:
- "Pod"
- Pod
mutate:
overlay:
metadata:
@ -47,5 +47,4 @@ spec:
- (hostPath):
path: "*"
````
````

View file

@ -4,7 +4,7 @@ All processes inside the pod can be made to run with specific user and groupID b
## Policy YAML
[policy_validate_user_group_fsgroup_id.yaml](more/policy_validate_user_group_fsgroup_id.yaml)
[policy_validate_user_group_fsgroup_id.yaml](more/restrict_usergroup_fsgroup_id.yaml)
````yaml
apiVersion: kyverno.io/v1
@ -46,8 +46,4 @@ spec:
spec:
securityContext:
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.
````
````

View file

@ -7,17 +7,17 @@ The volume of type `hostPath` allows pods to use host bind mounts (i.e. director
[disallow_bind_mounts.yaml](best_practices/disallow_bind_mounts.yaml)
````yaml
apiVersion: "kyverno.io/v1"
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: "disallow-bind-mounts"
name: disallow-bind-mounts
spec:
rules:
- name: "validate-hostPath"
- name: validate-hostPath
match:
resources:
kinds:
- "Pod"
- Pod
validate:
message: "Host path volumes are not allowed"
pattern:

View file

@ -25,6 +25,6 @@ spec:
pattern:
spec:
=(volumes):
=(hostPath):
path: "!/var/run/docker.sock"
- =(hostPath):
path: "!/var/run/docker.sock"
````

View file

@ -15,18 +15,28 @@ metadata:
name: disallow-host-network-port
spec:
rules:
- name: validate-host-network-port
- name: validate-host-network
match:
resources:
kinds:
- Pod
validate:
message: "Using host networking is not allowed"
message: "Use of hostNetwork is not allowed"
pattern:
spec:
=(hostNetwork): false
- name: validate-host-port
match:
resources:
kinds:
- Pod
validate:
message: "Use of hostPort is not allowed"
pattern:
spec:
(hostNetwork): false
containers:
- name: "*"
ports:
- hostPort: null
=(ports):
- X(hostPort): null
````

View file

@ -14,7 +14,7 @@ metadata:
name: disallow-latest-tag
spec:
rules:
- name: require-tag
- name: require-image-tag
match:
resources:
kinds:
@ -25,7 +25,7 @@ spec:
spec:
containers:
- image: "*:*"
- name: validate-tag
- name: validate-image-tag
match:
resources:
kinds:

View file

@ -15,7 +15,6 @@ apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: disallow-root-user
annotations:
spec:
rules:
- name: validate-runAsNonRoot
@ -24,7 +23,7 @@ spec:
kinds:
- Pod
validate:
message: "Root user is not allowed. Set runAsNonRoot to true"
message: "Running as root user is not allowed. Set runAsNonRoot to true"
anyPattern:
- spec:
securityContext:

View file

@ -34,6 +34,7 @@ These policies provide additional best practices and are worthy of close conside
18. [Restrict `NodePort` services](RestrictNodePort.md)
19. [Restrict auto-mount of service account credentials](RestrictAutomountSAToken.md)
20. [Restrict ingress classes](RestrictIngressClasses.md)
21. [Restrict User Group](CheckUserGroup.md)
## Applying the sample policies

View file

@ -14,7 +14,6 @@ kind: ClusterPolicy
metadata:
name: require-pod-requests-limits
spec:
validationFailureAction: "audit"
rules:
- name: validate-resources
match:

View file

@ -19,7 +19,7 @@ spec:
kinds:
- Pod
validate:
message: "Deny automounting API credentials"
message: "Auto-mounting of Service Account tokens is not allowed"
pattern:
spec:
automountServiceAccountToken: false

View file

@ -15,19 +15,18 @@ Although NodePort services can be useful, their use should be limited to service
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: restrict-node-port
name: restrict-nodeport
spec:
rules:
- name: validate-node-port
- name: validate-nodeport
match:
resources:
kinds:
- Service
validate:
message: "Service of type NodePort is not allowed"
message: "Services of type NodePort are not allowed"
pattern:
spec:
type: "!NodePort"
````
````

View file

@ -12,7 +12,7 @@ metadata:
desired traffic to application pods from select sources.
spec:
rules:
- name: "default-deny-ingress"
- name: default-deny-ingress
match:
resources:
kinds:

View file

@ -16,7 +16,7 @@ spec:
- Namespace
generate:
kind: ResourceQuota
name: "default-resourcequota"
name: default-resourcequota
data:
spec:
hard:
@ -31,7 +31,7 @@ spec:
- Namespace
generate:
kind: LimitRange
name: "default-limitrange"
name: default-limitrange
data:
spec:
limits:

View file

@ -1,7 +1,7 @@
apiVersion: "kyverno.io/v1"
kind: "ClusterPolicy"
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: "add-safe-to-evict"
name: add-safe-to-evict
annotations:
policies.kyverno.io/category: Workload Management
policies.kyverno.io/description: The Kubernetes cluster autoscaler does not evict pods that
@ -9,29 +9,29 @@ metadata:
cluster-autoscaler.kubernetes.io/safe-to-evict=true must be added to the pods.
spec:
rules:
- name: "annotate-empty-dir"
- name: annotate-empty-dir
match:
resources:
kinds:
- "Pod"
- Pod
mutate:
overlay:
metadata:
annotations:
+(cluster-autoscaler.kubernetes.io/safe-to-evict): "true"
+(cluster-autoscaler.kubernetes.io/safe-to-evict): true
spec:
volumes:
- (emptyDir): {}
- name: "annotate-host-path"
- name: annotate-host-path
match:
resources:
kinds:
- "Pod"
- Pod
mutate:
overlay:
metadata:
annotations:
+(cluster-autoscaler.kubernetes.io/safe-to-evict): "true"
+(cluster-autoscaler.kubernetes.io/safe-to-evict): true
spec:
volumes:
- (hostPath):

View file

@ -1,7 +1,7 @@
apiVersion: "kyverno.io/v1"
kind: "ClusterPolicy"
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: "disallow-bind-mounts"
name: disallow-bind-mounts
annotations:
policies.kyverno.io/category: Workload Isolation
policies.kyverno.io/description: The volume of type `hostPath` allows pods to use host bind
@ -13,11 +13,11 @@ metadata:
spec:
rules:
- name: "validate-hostPath"
- name: validate-hostPath
match:
resources:
kinds:
- "Pod"
- Pod
validate:
message: "Host path volumes are not allowed"
pattern:

View file

@ -4,7 +4,7 @@ metadata:
name: disallow-helm-tiller
annotations:
policies.kyverno.io/category: Security
policies.kyverno.io/description:
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

View file

@ -0,0 +1,45 @@
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: validate-userid-groupid-fsgroup
annotations:
policies.kyverno.io/category: Security Context
policies.kyverno.io/description: All 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. These options can be
used to validate the IDs used for user and group.
spec:
rules:
- name: validate-userid
match:
resources:
kinds:
- Pod
validate:
message: "User ID should be 1000"
pattern:
spec:
securityContext:
runAsUser: '1000'
- name: validate-groupid
match:
resources:
kinds:
- Pod
validate:
message: "Group ID should be 3000"
pattern:
spec:
securityContext:
runAsGroup: '3000'
- name: validate-fsgroup
match:
resources:
kinds:
- Pod
validate:
message: "fsgroup should be 2000"
pattern:
spec:
securityContext:
fsGroup: '2000'

View file

@ -3,7 +3,7 @@ kind: Pod
metadata:
name: pod-with-emptydir
annotations:
cluster-autoscaler.kubernetes.io/safe-to-evict: "true"
cluster-autoscaler.kubernetes.io/safe-to-evict: true
spec:
containers:
- image: k8s.gcr.io/test-webserver

View file

@ -3,7 +3,7 @@ kind: Pod
metadata:
name: pod-with-hostpath
annotations:
cluster-autoscaler.kubernetes.io/safe-to-evict: "true"
cluster-autoscaler.kubernetes.io/safe-to-evict: true
spec:
containers:
- image: k8s.gcr.io/test-webserver