diff --git a/config.yml b/config.yml index f75d5ae7..5f69e7c5 100644 --- a/config.yml +++ b/config.yml @@ -50,29 +50,17 @@ networking: hostPIDSet: error hostPortSet: error security: - RunAsPrivileged: warning + runAsRootAllowed: warning + runAsPrivileged: error notReadOnlyRootFileSystem: warning - runAsNonRoot: warning + privilegeEscalationAllowed: error capabilities: - blacklist: - error: - - CHOWN - - SYS_CHROOT - - AUDIT_WRITE - whitelist: - warning: - - CHOWN - - DAC_OVERRIDE - - FSETID - - FOWNER - - MKNOD - - NET_RAW - - SETGID - - SETUID - - SETFCAP - - SETPCAP - - NET_BIND_SERVICE - - SYS_CHROOT - - KILL - - AUDIT_WRITE - + error: + ifAnyAdded: + - CAP_SYS_ADMIN + - ALL + ifAnyNotDropped: + - ALL + warning: + ifAnyAddedBeyond: + - NONE diff --git a/deploy/all.yaml b/deploy/all.yaml index 142b194f..ccbaa7dd 100644 --- a/deploy/all.yaml +++ b/deploy/all.yaml @@ -127,31 +127,20 @@ data: hostPIDSet: error hostPortSet: error security: - RunAsPrivileged: warning + runAsRootAllowed: warning + runAsPrivileged: error notReadOnlyRootFileSystem: warning - runAsNonRoot: warning + privilegeEscalationAllowed: error capabilities: - blacklist: - error: - - CHOWN - - SYS_CHROOT - - AUDIT_WRITE - whitelist: - warning: - - CHOWN - - DAC_OVERRIDE - - FSETID - - FOWNER - - MKNOD - - NET_RAW - - SETGID - - SETUID - - SETFCAP - - SETPCAP - - NET_BIND_SERVICE - - SYS_CHROOT - - KILL - - AUDIT_WRITE + error: + ifAnyAdded: + - CAP_SYS_ADMIN + - ALL + ifAnyNotDropped: + - ALL + warning: + ifAnyAddedBeyond: + - NONE --- apiVersion: extensions/v1beta1 kind: Deployment diff --git a/pkg/config/config.go b/pkg/config/config.go index 4faf4b31..809d869a 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -98,14 +98,15 @@ type Security struct { // SecurityCapabilities contains the config for security capabilities validations. type SecurityCapabilities struct { - Added ErrorWarningCapLists `json:"added"` - Dropped ErrorWarningCapLists `json:"dropped"` + Error SecurityCapabilityLists `json:"error"` + Warning SecurityCapabilityLists `json:"warning"` } -// 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"` +// SecurityCapabilityLists contains the config for security capabilitie list validations. +type SecurityCapabilityLists struct { + IfAnyAdded []corev1.Capability `json:"ifAnyAdded"` + IfAnyAddedBeyond []corev1.Capability `json:"ifAnyAddedBeyond"` + IfAnyNotDropped []corev1.Capability `json:"ifAnyNotDropped"` } // ParseFile parses config from a file. diff --git a/pkg/validator/container.go b/pkg/validator/container.go index e4be5c78..4d126202 100644 --- a/pkg/validator/container.go +++ b/pkg/validator/container.go @@ -159,20 +159,16 @@ func (cv *ContainerValidation) validateSecurity(securityConf *conf.Security) { 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 { + if securityContext.RunAsNonRoot == (*bool)(nil) || !*securityContext.RunAsNonRoot { cv.addFailure("Container is allowed to run as root", securityConf.RunAsRootAllowed) + } else { + cv.addSuccess("Container is not allowed to run as root") } } if securityConf.RunAsPrivileged.IsActionable() { - if *securityContext.Privileged { + if securityContext.Privileged == (*bool)(nil) || !*securityContext.Privileged { cv.addSuccess("Container is not running as privileged") } else { cv.addFailure("Container is running as privileged", securityConf.RunAsPrivileged) @@ -180,84 +176,75 @@ func (cv *ContainerValidation) validateSecurity(securityConf *conf.Security) { } if securityConf.NotReadOnlyRootFileSystem.IsActionable() { - if *securityContext.ReadOnlyRootFilesystem { - cv.addSuccess("Container is running with a read only filesystem") - } else { + if securityContext.ReadOnlyRootFilesystem == (*bool)(nil) || !*securityContext.ReadOnlyRootFilesystem { cv.addFailure("Container is not running with a read only filesystem", securityConf.NotReadOnlyRootFileSystem) + } else { + cv.addSuccess("Container is running with a read only filesystem") } } if securityConf.PrivilegeEscalationAllowed.IsActionable() { - if *cv.Container.SecurityContext.AllowPrivilegeEscalation { + if securityContext.AllowPrivilegeEscalation == (*bool)(nil) || !*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") - } - } - + cv.validateCapabilities(securityConf.Capabilities.Error, conf.SeverityError) + cv.validateCapabilities(securityConf.Capabilities.Warning, conf.SeverityWarning) } -func contains(list []corev1.Capability, val corev1.Capability) bool { - for _, s := range list { - if s == val { - return true +func (cv *ContainerValidation) validateCapabilities(confLists conf.SecurityCapabilityLists, severity conf.Severity) { + capabilities := &corev1.Capabilities{} + if cv.Container.SecurityContext != nil && cv.Container.SecurityContext.Capabilities != nil { + capabilities = cv.Container.SecurityContext.Capabilities + } + + if len(confLists.IfAnyAdded) > 0 { + intersectAdds := capIntersection(capabilities.Add, confLists.IfAnyAdded) + if len(intersectAdds) > 0 { + capsString := commaSeparatedCapabilities(intersectAdds) + cv.addFailure(fmt.Sprintf("Security capabilities added from %v list: %v", severity, capsString), severity) + } else if capContains(capabilities.Add, "ALL") { + cv.addFailure(fmt.Sprintf("All security capabilities added, violating %v list", severity), severity) + } else { + cv.addSuccess(fmt.Sprintf("No security capabilities added from %v list", severity)) } } - return false + if len(confLists.IfAnyAddedBeyond) > 0 { + differentAdds := capDifference(capabilities.Add, confLists.IfAnyAddedBeyond) + if len(differentAdds) > 0 { + capsString := commaSeparatedCapabilities(differentAdds) + cv.addFailure(fmt.Sprintf("Security capabilities added beyond %v list: %v", severity, capsString), severity) + } else if capContains(capabilities.Add, "ALL") { + cv.addFailure(fmt.Sprintf("All security capabilities added, going beyond %v list", severity), severity) + } else { + cv.addSuccess(fmt.Sprintf("No security capabilities added beyond %v list", severity)) + } + } + + if len(confLists.IfAnyNotDropped) > 0 { + intersectDrops := capIntersection(capabilities.Drop, confLists.IfAnyNotDropped) + if len(intersectDrops) > 0 && !capContains(capabilities.Drop, "ALL") { + capsString := commaSeparatedCapabilities(intersectDrops) + cv.addFailure(fmt.Sprintf("Security capabilities not dropped from %v list: %v", severity, capsString), severity) + } else { + cv.addSuccess(fmt.Sprintf("All security capabilities dropped from %v list", severity)) + } + } } -func intersection(a, b []corev1.Capability) []corev1.Capability { +func commaSeparatedCapabilities(caps []corev1.Capability) string { + capsString := "" + for _, cap := range caps { + capsString = fmt.Sprintf("%s, %s", capsString, cap) + } + return capsString[2:] +} + +func capIntersection(a, b []corev1.Capability) []corev1.Capability { result := []corev1.Capability{} hash := map[corev1.Capability]bool{} @@ -273,3 +260,30 @@ func intersection(a, b []corev1.Capability) []corev1.Capability { return result } + +func capDifference(b, a []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 +} + +func capContains(list []corev1.Capability, val corev1.Capability) bool { + for _, s := range list { + if s == val { + return true + } + } + + return false +} diff --git a/pkg/validator/container_test.go b/pkg/validator/container_test.go index 98a5a3d7..26609b7b 100644 --- a/pkg/validator/container_test.go +++ b/pkg/validator/container_test.go @@ -343,6 +343,14 @@ func TestValidateSecurity(t *testing.T) { RunAsPrivileged: conf.SeverityError, NotReadOnlyRootFileSystem: conf.SeverityWarning, PrivilegeEscalationAllowed: conf.SeverityError, + Capabilities: conf.SecurityCapabilities{ + Error: conf.SecurityCapabilityLists{ + IfAnyAdded: []corev1.Capability{"ALL", "SYS_ADMIN", "NET_ADMIN"}, + }, + Warning: conf.SecurityCapabilityLists{ + IfAnyAddedBeyond: []corev1.Capability{"NONE"}, + }, + }, } emptyCV := ContainerValidation{ @@ -358,6 +366,24 @@ func TestValidateSecurity(t *testing.T) { ReadOnlyRootFilesystem: &falseVar, Privileged: &trueVar, AllowPrivilegeEscalation: &trueVar, + Capabilities: &corev1.Capabilities{ + Add: []corev1.Capability{"AUDIT_CONTROL", "SYS_ADMIN", "NET_ADMIN"}, + }, + }}, + ResourceValidation: &ResourceValidation{ + Summary: &ResultSummary{}, + }, + } + + goodCV := ContainerValidation{ + Container: &corev1.Container{Name: "", SecurityContext: &corev1.SecurityContext{ + RunAsNonRoot: &trueVar, + ReadOnlyRootFilesystem: &trueVar, + Privileged: &falseVar, + AllowPrivilegeEscalation: &falseVar, + Capabilities: &corev1.Capabilities{ + Drop: []corev1.Capability{"ALL"}, + }, }}, ResourceValidation: &ResourceValidation{ Summary: &ResultSummary{}, @@ -377,9 +403,9 @@ func TestValidateSecurity(t *testing.T) { expectedMessages: []*ResultMessage{}, }, { - name: "bad security context + standard validation config", + name: "empty security context + standard validation config", securityConf: standardConf, - cv: badCV, + cv: emptyCV, expectedMessages: []*ResultMessage{{ Message: "Container is allowed to run as root", Type: "warning", @@ -392,6 +418,60 @@ func TestValidateSecurity(t *testing.T) { }, { Message: "Container does not allow privilege escalation", Type: "success", + }, { + Message: "No security capabilities added from error list", + Type: "success", + }, { + Message: "No security capabilities added beyond warning list", + Type: "success", + }}, + }, + { + name: "bad security context + standard validation config", + securityConf: standardConf, + cv: badCV, + expectedMessages: []*ResultMessage{{ + Message: "Security capabilities added from error list: SYS_ADMIN, NET_ADMIN", + Type: "error", + }, { + Message: "Container allows privilege escalation", + Type: "error", + }, { + Message: "Container is running as privileged", + Type: "error", + }, { + Message: "Security capabilities added beyond warning list: AUDIT_CONTROL, SYS_ADMIN, NET_ADMIN", + Type: "warning", + }, { + Message: "Container is allowed to run as root", + Type: "warning", + }, { + Message: "Container is not running with a read only filesystem", + Type: "warning", + }}, + }, + { + name: "good security context + standard validation config", + securityConf: standardConf, + cv: goodCV, + expectedMessages: []*ResultMessage{{ + Message: "Container is not allowed to run as root", + Type: "success", + }, { + Message: "Container is running with a read only filesystem", + Type: "success", + }, { + Message: "Container is not running as privileged", + Type: "success", + }, { + Message: "Container does not allow privilege escalation", + Type: "success", + }, { + Message: "No security capabilities added from error list", + Type: "success", + }, { + Message: "No security capabilities added beyond warning list", + Type: "success", }}, }, }