From 82164105d7da1272edaa29dbc523ff7133fb77b4 Mon Sep 17 00:00:00 2001 From: Rob Scott Date: Wed, 3 Apr 2019 13:12:52 -0400 Subject: [PATCH] initial work on security validations --- config.yml | 2 +- deploy/all.yaml | 2 +- pkg/config/config.go | 20 ++++-- pkg/validator/container.go | 122 ++++++++++++++++++++++++++++++++ pkg/validator/container_test.go | 73 +++++++++++++++++++ 5 files changed, 211 insertions(+), 8 deletions(-) diff --git a/config.yml b/config.yml index 823c0088..f75d5ae7 100644 --- a/config.yml +++ b/config.yml @@ -50,7 +50,7 @@ networking: hostPIDSet: error hostPortSet: error security: - runAsPriviliged: warning + RunAsPrivileged: warning notReadOnlyRootFileSystem: warning runAsNonRoot: warning capabilities: diff --git a/deploy/all.yaml b/deploy/all.yaml index 3e02ad3e..142b194f 100644 --- a/deploy/all.yaml +++ b/deploy/all.yaml @@ -127,7 +127,7 @@ data: hostPIDSet: error hostPortSet: error security: - runAsPriviliged: warning + RunAsPrivileged: warning notReadOnlyRootFileSystem: warning runAsNonRoot: warning capabilities: diff --git a/pkg/config/config.go b/pkg/config/config.go index 8103cceb..4faf4b31 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -20,6 +20,7 @@ import ( "io" "io/ioutil" + corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" "k8s.io/apimachinery/pkg/util/yaml" ) @@ -88,16 +89,23 @@ type Networking struct { // Security contains the config for security validations. type Security struct { - RunAsNonRoot Severity `json:"runAsNonRoot"` - RunAsPriviliged Severity `json:"runAsPriviliged"` - NotReadOnlyRootFileSystem Severity `json:"notReadOnlyRootFileSystem"` - Capabilities SecurityCapabilities `json:"capabilities"` + RunAsRootAllowed Severity `json:"runAsRootAllowed"` + RunAsPrivileged Severity `json:"RunAsPrivileged"` + NotReadOnlyRootFileSystem Severity `json:"notReadOnlyRootFileSystem"` + PrivilegeEscalationAllowed Severity `json:"privilegeEscalationAllowed"` + Capabilities SecurityCapabilities `json:"capabilities"` } // SecurityCapabilities contains the config for security capabilities validations. type SecurityCapabilities struct { - Whitelist ErrorWarningLists `json:"whitelist"` - Blacklist ErrorWarningLists `json:"blacklist"` + Added ErrorWarningCapLists `json:"added"` + Dropped ErrorWarningCapLists `json:"dropped"` +} + +// ErrorWarningCapLists provides lists of capabilities that should trigger an error or warning. +type ErrorWarningCapLists struct { + Error []corev1.Capability `json:"error"` + Warning []corev1.Capability `json:"warning"` } // ParseFile parses config from a file. diff --git a/pkg/validator/container.go b/pkg/validator/container.go index b76442d6..e4be5c78 100644 --- a/pkg/validator/container.go +++ b/pkg/validator/container.go @@ -42,6 +42,7 @@ func ValidateContainer(cnConf *conf.Configuration, container *corev1.Container) cv.validateHealthChecks(&cnConf.HealthChecks) cv.validateImage(&cnConf.Images) cv.validateNetworking(&cnConf.Networking) + cv.validateSecurity(&cnConf.Security) cRes := ContainerResult{ Name: container.Name, @@ -151,3 +152,124 @@ func (cv *ContainerValidation) validateNetworking(networkConf *conf.Networking) } } } + +func (cv *ContainerValidation) validateSecurity(securityConf *conf.Security) { + securityContext := cv.Container.SecurityContext + if securityContext == nil { + securityContext = &corev1.SecurityContext{} + } + + if securityContext.Capabilities == nil { + securityContext.Capabilities = &corev1.Capabilities{} + } + + if securityConf.RunAsRootAllowed.IsActionable() { + if *securityContext.RunAsNonRoot { + cv.addSuccess("Container is not allowed to run as root") + } else { + cv.addFailure("Container is allowed to run as root", securityConf.RunAsRootAllowed) + } + } + + if securityConf.RunAsPrivileged.IsActionable() { + if *securityContext.Privileged { + cv.addSuccess("Container is not running as privileged") + } else { + cv.addFailure("Container is running as privileged", securityConf.RunAsPrivileged) + } + } + + if securityConf.NotReadOnlyRootFileSystem.IsActionable() { + if *securityContext.ReadOnlyRootFilesystem { + cv.addSuccess("Container is running with a read only filesystem") + } else { + cv.addFailure("Container is not running with a read only filesystem", securityConf.NotReadOnlyRootFileSystem) + } + } + + if securityConf.PrivilegeEscalationAllowed.IsActionable() { + if *cv.Container.SecurityContext.AllowPrivilegeEscalation { + cv.addSuccess("Container does not allow privilege escalation") + } else { + cv.addFailure("Container allows privilege escalation", securityConf.PrivilegeEscalationAllowed) + } + } + + capAdds := securityContext.Capabilities.Add + if len(securityConf.Capabilities.Added.Error) > 0 { + intersectCaps := intersection(capAdds, securityConf.Capabilities.Added.Error) + if len(intersectCaps) > 0 { + failMsg := fmt.Sprintf("Security capabilities added from error list: %v", intersectCaps) + cv.addFailure(failMsg, conf.SeverityError) + } else if contains(capAdds, "ALL") { + cv.addFailure("Container has all security capabilities added", conf.SeverityError) + } else { + cv.addSuccess("No security capabilities added from error list") + } + } + + if len(securityConf.Capabilities.Added.Warning) > 0 { + intersectCaps := intersection(capAdds, securityConf.Capabilities.Added.Warning) + if len(intersectCaps) > 0 { + failMsg := fmt.Sprintf("Security capabilities added from warning list: %v", intersectCaps) + cv.addFailure(failMsg, conf.SeverityWarning) + } else if contains(capAdds, "ALL") { + cv.addFailure("Container has all security capabilities added", conf.SeverityWarning) + } else { + cv.addSuccess("No security capabilities added from warning list") + } + } + + capDrops := securityContext.Capabilities.Drop + if len(securityConf.Capabilities.Dropped.Error) > 0 { + intersectCaps := intersection(capDrops, securityConf.Capabilities.Dropped.Error) + if len(intersectCaps) > 0 { + failMsg := fmt.Sprintf("Security capabilities dropped from error list: %v", intersectCaps) + cv.addFailure(failMsg, conf.SeverityError) + } else if contains(capDrops, "ALL") { + cv.addFailure("Container has all security capabilities dropped", conf.SeverityError) + } else { + cv.addSuccess("No security capabilities dropped from error list") + } + } + + if len(securityConf.Capabilities.Dropped.Warning) > 0 { + intersectCaps := intersection(capDrops, securityConf.Capabilities.Dropped.Warning) + if len(intersectCaps) > 0 { + failMsg := fmt.Sprintf("Security capabilities dropped from warning list: %v", intersectCaps) + cv.addFailure(failMsg, conf.SeverityWarning) + } else if contains(capDrops, "ALL") { + cv.addFailure("Container has all security capabilities dropped", conf.SeverityWarning) + } else { + cv.addSuccess("No security capabilities dropped from warning list") + } + } + +} + +func contains(list []corev1.Capability, val corev1.Capability) bool { + for _, s := range list { + if s == val { + return true + } + } + + return false +} + +func intersection(a, b []corev1.Capability) []corev1.Capability { + result := []corev1.Capability{} + hash := map[corev1.Capability]bool{} + + for _, s := range a { + hash[s] = true + } + + for _, s := range b { + if hash[s] { + result = append(result, s) + } + } + + return result +} diff --git a/pkg/validator/container_test.go b/pkg/validator/container_test.go index c643a8af..98a5a3d7 100644 --- a/pkg/validator/container_test.go +++ b/pkg/validator/container_test.go @@ -331,3 +331,76 @@ func TestValidateImage(t *testing.T) { }) } } + +func TestValidateSecurity(t *testing.T) { + trueVar := true + falseVar := false + + // Test setup. + emptyConf := conf.Security{} + standardConf := conf.Security{ + RunAsRootAllowed: conf.SeverityWarning, + RunAsPrivileged: conf.SeverityError, + NotReadOnlyRootFileSystem: conf.SeverityWarning, + PrivilegeEscalationAllowed: conf.SeverityError, + } + + emptyCV := ContainerValidation{ + Container: &corev1.Container{Name: ""}, + ResourceValidation: &ResourceValidation{ + Summary: &ResultSummary{}, + }, + } + + badCV := ContainerValidation{ + Container: &corev1.Container{Name: "", SecurityContext: &corev1.SecurityContext{ + RunAsNonRoot: &falseVar, + ReadOnlyRootFilesystem: &falseVar, + Privileged: &trueVar, + AllowPrivilegeEscalation: &trueVar, + }}, + ResourceValidation: &ResourceValidation{ + Summary: &ResultSummary{}, + }, + } + + var testCases = []struct { + name string + securityConf conf.Security + cv ContainerValidation + expectedMessages []*ResultMessage + }{ + { + name: "empty security context + empty validation config", + securityConf: emptyConf, + cv: emptyCV, + expectedMessages: []*ResultMessage{}, + }, + { + name: "bad security context + standard validation config", + securityConf: standardConf, + cv: badCV, + expectedMessages: []*ResultMessage{{ + Message: "Container is allowed to run as root", + Type: "warning", + }, { + Message: "Container is not running with a read only filesystem", + Type: "warning", + }, { + Message: "Container is not running as privileged", + Type: "success", + }, { + Message: "Container does not allow privilege escalation", + Type: "success", + }}, + }, + } + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + tt.cv.validateSecurity(&tt.securityConf) + assert.Len(t, tt.cv.messages(), len(tt.expectedMessages)) + assert.ElementsMatch(t, tt.cv.messages(), tt.expectedMessages) + }) + } +}