mirror of
https://github.com/FairwindsOps/polaris.git
synced 2026-05-14 21:27:11 +00:00
initial work on security validations
This commit is contained in:
@@ -50,7 +50,7 @@ networking:
|
||||
hostPIDSet: error
|
||||
hostPortSet: error
|
||||
security:
|
||||
runAsPriviliged: warning
|
||||
RunAsPrivileged: warning
|
||||
notReadOnlyRootFileSystem: warning
|
||||
runAsNonRoot: warning
|
||||
capabilities:
|
||||
|
||||
@@ -127,7 +127,7 @@ data:
|
||||
hostPIDSet: error
|
||||
hostPortSet: error
|
||||
security:
|
||||
runAsPriviliged: warning
|
||||
RunAsPrivileged: warning
|
||||
notReadOnlyRootFileSystem: warning
|
||||
runAsNonRoot: warning
|
||||
capabilities:
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user