mirror of
https://github.com/FairwindsOps/polaris.git
synced 2026-05-10 03:07:12 +00:00
security validations fully working
This commit is contained in:
36
config.yml
36
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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
}},
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user