initial work on security validations

This commit is contained in:
Rob Scott
2019-04-03 13:12:52 -04:00
parent 5c69b99f4e
commit 82164105d7
5 changed files with 211 additions and 8 deletions

View File

@@ -50,7 +50,7 @@ networking:
hostPIDSet: error
hostPortSet: error
security:
runAsPriviliged: warning
RunAsPrivileged: warning
notReadOnlyRootFileSystem: warning
runAsNonRoot: warning
capabilities:

View File

@@ -127,7 +127,7 @@ data:
hostPIDSet: error
hostPortSet: error
security:
runAsPriviliged: warning
RunAsPrivileged: warning
notReadOnlyRootFileSystem: warning
runAsNonRoot: warning
capabilities:

View File

@@ -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.

View File

@@ -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
}

View File

@@ -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)
})
}
}