mirror of
https://github.com/FairwindsOps/polaris.git
synced 2026-05-10 03:07:12 +00:00
* Fixing Container Security Context Logic Kubernetes rationalizes Container Security Context in conjunction with the Pod Spec Security Context. In this scenario you can 'leave out' certain security context settings and rely on the pod spec definition to still set these settings for you. The RunAsNonRoot setting originally only checked to see if the value was set at the container level, vs also checking if it was enabled at the pod level. I have attached the container's parent pod spec to the container validate struct in case any other things like this arise in the future. I have also refactored the logic for validating bool pointers, since these can be tricky, if you want to avoid dereferences pointer issues. Changes: - Added parent pod spec of container to validate certain settings which affect container spec - Refactored the logic statements for validating bool pointers (used helpers) - Added tests for this pod.container.securityContext condition
944 lines
26 KiB
Go
944 lines
26 KiB
Go
// Copyright 2019 ReactiveOps
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package validator
|
|
|
|
import (
|
|
"testing"
|
|
|
|
conf "github.com/reactiveops/polaris/pkg/config"
|
|
"github.com/stretchr/testify/assert"
|
|
corev1 "k8s.io/api/core/v1"
|
|
"k8s.io/apimachinery/pkg/api/resource"
|
|
)
|
|
|
|
var resourceConf1 = `---
|
|
resources:
|
|
cpuRequestRanges:
|
|
error:
|
|
below: 100m
|
|
above: 1
|
|
warning:
|
|
below: 200m
|
|
above: 800m
|
|
memoryRequestRanges:
|
|
error:
|
|
below: 100M
|
|
above: 3G
|
|
warning:
|
|
below: 200M
|
|
above: 2G
|
|
cpuLimitRanges:
|
|
error:
|
|
below: 100m
|
|
above: 2
|
|
warning:
|
|
below: 300m
|
|
above: 1800m
|
|
memoryLimitRanges:
|
|
error:
|
|
below: 200M
|
|
above: 6G
|
|
warning:
|
|
below: 300M
|
|
above: 4G
|
|
`
|
|
|
|
var resourceConf2 = `---
|
|
resources:
|
|
cpuRequestsMissing: warning
|
|
memoryRequestsMissing: warning
|
|
cpuLimitsMissing: error
|
|
memoryLimitsMissing: error
|
|
`
|
|
|
|
func TestValidateResourcesEmptyConfig(t *testing.T) {
|
|
container := corev1.Container{
|
|
Name: "Empty",
|
|
}
|
|
|
|
cv := ContainerValidation{
|
|
Container: &container,
|
|
ResourceValidation: &ResourceValidation{},
|
|
}
|
|
|
|
expected := conf.Resources{}
|
|
|
|
cv.validateResources(&expected)
|
|
assert.Len(t, cv.Errors, 0)
|
|
}
|
|
|
|
func TestValidateResourcesEmptyContainer(t *testing.T) {
|
|
container := corev1.Container{
|
|
Name: "Empty",
|
|
}
|
|
|
|
expectedWarnings := []*ResultMessage{
|
|
{
|
|
Type: "warning",
|
|
Message: "CPU requests should be set",
|
|
Category: "Resources",
|
|
},
|
|
{
|
|
Type: "warning",
|
|
Message: "Memory requests should be set",
|
|
Category: "Resources",
|
|
},
|
|
}
|
|
|
|
expectedErrors := []*ResultMessage{
|
|
{
|
|
Type: "error",
|
|
Message: "CPU limits should be set",
|
|
Category: "Resources",
|
|
},
|
|
{
|
|
Type: "error",
|
|
Message: "Memory limits should be set",
|
|
Category: "Resources",
|
|
},
|
|
}
|
|
|
|
testValidateResources(t, &container, &resourceConf2, &expectedErrors, &expectedWarnings)
|
|
}
|
|
|
|
func TestValidateResourcesPartiallyValid(t *testing.T) {
|
|
cpuRequest, err := resource.ParseQuantity("100m")
|
|
assert.NoError(t, err, "Error parsing quantity")
|
|
|
|
cpuLimit, err := resource.ParseQuantity("200m")
|
|
assert.NoError(t, err, "Error parsing quantity")
|
|
|
|
container := corev1.Container{
|
|
Name: "Empty",
|
|
Resources: corev1.ResourceRequirements{
|
|
Requests: corev1.ResourceList{
|
|
"cpu": cpuRequest,
|
|
},
|
|
Limits: corev1.ResourceList{
|
|
"cpu": cpuLimit,
|
|
},
|
|
},
|
|
}
|
|
|
|
expectedWarnings := []*ResultMessage{
|
|
{
|
|
Type: "warning",
|
|
Message: "CPU requests should be higher than 200m",
|
|
Category: "Resources",
|
|
},
|
|
{
|
|
Type: "warning",
|
|
Message: "CPU limits should be higher than 300m",
|
|
Category: "Resources",
|
|
},
|
|
}
|
|
|
|
expectedErrors := []*ResultMessage{
|
|
{
|
|
Type: "error",
|
|
Message: "Memory requests should be higher than 100M",
|
|
Category: "Resources",
|
|
},
|
|
{
|
|
Type: "error",
|
|
Message: "Memory limits should be higher than 200M",
|
|
Category: "Resources",
|
|
},
|
|
}
|
|
|
|
testValidateResources(t, &container, &resourceConf1, &expectedErrors, &expectedWarnings)
|
|
}
|
|
|
|
func TestValidateResourcesInit(t *testing.T) {
|
|
cvEmpty := ContainerValidation{
|
|
Container: &corev1.Container{},
|
|
ResourceValidation: &ResourceValidation{},
|
|
}
|
|
cvInit := ContainerValidation{
|
|
Container: &corev1.Container{},
|
|
ResourceValidation: &ResourceValidation{},
|
|
IsInitContainer: true,
|
|
}
|
|
|
|
parsedConf, err := conf.Parse([]byte(resourceConf1))
|
|
assert.NoError(t, err, "Expected no error when parsing config")
|
|
|
|
cvEmpty.validateResources(&parsedConf.Resources)
|
|
assert.Len(t, cvEmpty.Errors, 4)
|
|
|
|
cvInit.validateResources(&parsedConf.Resources)
|
|
assert.Len(t, cvInit.Errors, 0)
|
|
}
|
|
|
|
func TestValidateResourcesFullyValid(t *testing.T) {
|
|
cpuRequest, err := resource.ParseQuantity("300m")
|
|
assert.NoError(t, err, "Error parsing quantity")
|
|
|
|
cpuLimit, err := resource.ParseQuantity("400m")
|
|
assert.NoError(t, err, "Error parsing quantity")
|
|
|
|
memoryRequest, err := resource.ParseQuantity("400Mi")
|
|
assert.NoError(t, err, "Error parsing quantity")
|
|
|
|
memoryLimit, err := resource.ParseQuantity("500Mi")
|
|
assert.NoError(t, err, "Error parsing quantity")
|
|
|
|
container := corev1.Container{
|
|
Name: "Empty",
|
|
Resources: corev1.ResourceRequirements{
|
|
Requests: corev1.ResourceList{
|
|
"cpu": cpuRequest,
|
|
"memory": memoryRequest,
|
|
},
|
|
Limits: corev1.ResourceList{
|
|
"cpu": cpuLimit,
|
|
"memory": memoryLimit,
|
|
},
|
|
},
|
|
}
|
|
|
|
testValidateResources(t, &container, &resourceConf1, &[]*ResultMessage{}, &[]*ResultMessage{})
|
|
}
|
|
|
|
func testValidateResources(t *testing.T, container *corev1.Container, resourceConf *string, expectedErrors *[]*ResultMessage, expectedWarnings *[]*ResultMessage) {
|
|
cv := ContainerValidation{
|
|
Container: container,
|
|
ResourceValidation: &ResourceValidation{},
|
|
}
|
|
|
|
parsedConf, err := conf.Parse([]byte(*resourceConf))
|
|
assert.NoError(t, err, "Expected no error when parsing config")
|
|
|
|
cv.validateResources(&parsedConf.Resources)
|
|
assert.Len(t, cv.Warnings, len(*expectedWarnings))
|
|
assert.ElementsMatch(t, cv.Warnings, *expectedWarnings)
|
|
|
|
assert.Len(t, cv.Errors, len(*expectedErrors))
|
|
assert.ElementsMatch(t, cv.Errors, *expectedErrors)
|
|
}
|
|
|
|
func TestValidateHealthChecks(t *testing.T) {
|
|
|
|
// Test setup.
|
|
p1 := conf.HealthChecks{}
|
|
p2 := conf.HealthChecks{
|
|
ReadinessProbeMissing: conf.SeverityIgnore,
|
|
LivenessProbeMissing: conf.SeverityIgnore,
|
|
}
|
|
p3 := conf.HealthChecks{
|
|
ReadinessProbeMissing: conf.SeverityError,
|
|
LivenessProbeMissing: conf.SeverityWarning,
|
|
}
|
|
|
|
probe := corev1.Probe{}
|
|
emptyCV := ContainerValidation{
|
|
Container: &corev1.Container{Name: ""},
|
|
ResourceValidation: &ResourceValidation{},
|
|
}
|
|
emptyCVInit := ContainerValidation{
|
|
Container: &corev1.Container{Name: ""},
|
|
ResourceValidation: &ResourceValidation{},
|
|
IsInitContainer: true,
|
|
}
|
|
goodCV := ContainerValidation{
|
|
Container: &corev1.Container{
|
|
Name: "",
|
|
LivenessProbe: &probe,
|
|
ReadinessProbe: &probe,
|
|
},
|
|
ResourceValidation: &ResourceValidation{},
|
|
}
|
|
|
|
l := &ResultMessage{Type: "warning", Message: "Liveness probe should be configured", Category: "Health Checks"}
|
|
r := &ResultMessage{Type: "error", Message: "Readiness probe should be configured", Category: "Health Checks"}
|
|
f1 := []*ResultMessage{}
|
|
f2 := []*ResultMessage{r}
|
|
w1 := []*ResultMessage{l}
|
|
|
|
var testCases = []struct {
|
|
name string
|
|
probes conf.HealthChecks
|
|
cv ContainerValidation
|
|
errors *[]*ResultMessage
|
|
warnings *[]*ResultMessage
|
|
}{
|
|
{name: "probes not configured", probes: p1, cv: emptyCV, errors: &f1},
|
|
{name: "probes not required", probes: p2, cv: emptyCV, errors: &f1},
|
|
{name: "probes required & configured", probes: p3, cv: goodCV, errors: &f1},
|
|
{name: "probes required, not configured, but init", probes: p3, cv: emptyCVInit, errors: &f1},
|
|
{name: "probes required & not configured", probes: p3, cv: emptyCV, errors: &f2, warnings: &w1},
|
|
{name: "probes configured, but not required", probes: p2, cv: goodCV, errors: &f1},
|
|
}
|
|
|
|
for _, tt := range testCases {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
tt.cv.validateHealthChecks(&tt.probes)
|
|
|
|
if tt.warnings != nil {
|
|
assert.Len(t, tt.cv.Warnings, len(*tt.warnings))
|
|
assert.ElementsMatch(t, tt.cv.Warnings, *tt.warnings)
|
|
}
|
|
|
|
assert.Len(t, tt.cv.Errors, len(*tt.errors))
|
|
assert.ElementsMatch(t, tt.cv.Errors, *tt.errors)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidateImage(t *testing.T) {
|
|
emptyConf := conf.Images{}
|
|
standardConf := conf.Images{
|
|
TagNotSpecified: conf.SeverityError,
|
|
PullPolicyNotAlways: conf.SeverityIgnore,
|
|
}
|
|
strongConf := conf.Images{
|
|
TagNotSpecified: conf.SeverityError,
|
|
PullPolicyNotAlways: conf.SeverityError,
|
|
}
|
|
|
|
emptyCV := ContainerValidation{
|
|
Container: &corev1.Container{},
|
|
ResourceValidation: &ResourceValidation{},
|
|
}
|
|
badCV := ContainerValidation{
|
|
Container: &corev1.Container{Image: "test"},
|
|
ResourceValidation: &ResourceValidation{},
|
|
}
|
|
lessBadCV := ContainerValidation{
|
|
Container: &corev1.Container{Image: "test:latest", ImagePullPolicy: ""},
|
|
ResourceValidation: &ResourceValidation{},
|
|
}
|
|
goodCV := ContainerValidation{
|
|
Container: &corev1.Container{Image: "test:0.1.0", ImagePullPolicy: "Always"},
|
|
ResourceValidation: &ResourceValidation{},
|
|
}
|
|
|
|
var testCases = []struct {
|
|
name string
|
|
image conf.Images
|
|
cv ContainerValidation
|
|
expected []*ResultMessage
|
|
}{
|
|
{
|
|
name: "emptyConf + emptyCV",
|
|
image: emptyConf,
|
|
cv: emptyCV,
|
|
expected: []*ResultMessage{},
|
|
},
|
|
{
|
|
name: "standardConf + emptyCV",
|
|
image: standardConf,
|
|
cv: emptyCV,
|
|
expected: []*ResultMessage{{
|
|
Message: "Image tag should be specified",
|
|
Type: "error",
|
|
Category: "Images",
|
|
}},
|
|
},
|
|
{
|
|
name: "standardConf + badCV",
|
|
image: standardConf,
|
|
cv: badCV,
|
|
expected: []*ResultMessage{{
|
|
Message: "Image tag should be specified",
|
|
Type: "error",
|
|
Category: "Images",
|
|
}},
|
|
},
|
|
{
|
|
name: "standardConf + lessBadCV",
|
|
image: standardConf,
|
|
cv: lessBadCV,
|
|
expected: []*ResultMessage{{
|
|
Message: "Image tag should be specified",
|
|
Type: "error",
|
|
Category: "Images",
|
|
}},
|
|
},
|
|
{
|
|
name: "strongConf + badCV",
|
|
image: strongConf,
|
|
cv: badCV,
|
|
expected: []*ResultMessage{{
|
|
Message: "Image pull policy should be \"Always\"",
|
|
Type: "error",
|
|
Category: "Images",
|
|
}, {
|
|
Message: "Image tag should be specified",
|
|
Type: "error",
|
|
Category: "Images",
|
|
}},
|
|
},
|
|
{
|
|
name: "strongConf + goodCV",
|
|
image: strongConf,
|
|
cv: goodCV,
|
|
expected: []*ResultMessage{},
|
|
},
|
|
}
|
|
|
|
for _, tt := range testCases {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
tt.cv = resetCV(tt.cv)
|
|
tt.cv.validateImage(&tt.image)
|
|
assert.Len(t, tt.cv.Errors, len(tt.expected))
|
|
assert.ElementsMatch(t, tt.cv.Errors, tt.expected)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidateNetworking(t *testing.T) {
|
|
// Test setup.
|
|
emptyConf := conf.Networking{}
|
|
standardConf := conf.Networking{
|
|
HostPortSet: conf.SeverityWarning,
|
|
}
|
|
strongConf := conf.Networking{
|
|
HostPortSet: conf.SeverityError,
|
|
}
|
|
|
|
emptyCV := ContainerValidation{
|
|
Container: &corev1.Container{Name: ""},
|
|
ResourceValidation: &ResourceValidation{},
|
|
}
|
|
|
|
badCV := ContainerValidation{
|
|
Container: &corev1.Container{
|
|
Ports: []corev1.ContainerPort{{
|
|
ContainerPort: 3000,
|
|
HostPort: 443,
|
|
}},
|
|
},
|
|
ResourceValidation: &ResourceValidation{},
|
|
}
|
|
|
|
goodCV := ContainerValidation{
|
|
Container: &corev1.Container{
|
|
Ports: []corev1.ContainerPort{{
|
|
ContainerPort: 3000,
|
|
}},
|
|
},
|
|
ResourceValidation: &ResourceValidation{},
|
|
}
|
|
|
|
var testCases = []struct {
|
|
name string
|
|
networkConf conf.Networking
|
|
cv ContainerValidation
|
|
expectedMessages []*ResultMessage
|
|
}{
|
|
{
|
|
name: "empty ports + empty validation config",
|
|
networkConf: emptyConf,
|
|
cv: emptyCV,
|
|
expectedMessages: []*ResultMessage{},
|
|
},
|
|
{
|
|
name: "empty ports + standard validation config",
|
|
networkConf: standardConf,
|
|
cv: emptyCV,
|
|
expectedMessages: []*ResultMessage{{
|
|
Message: "Host port is not configured",
|
|
Type: "success",
|
|
Category: "Networking",
|
|
}},
|
|
},
|
|
{
|
|
name: "empty ports + strong validation config",
|
|
networkConf: standardConf,
|
|
cv: emptyCV,
|
|
expectedMessages: []*ResultMessage{{
|
|
Message: "Host port is not configured",
|
|
Type: "success",
|
|
Category: "Networking",
|
|
}},
|
|
},
|
|
{
|
|
name: "host ports + empty validation config",
|
|
networkConf: emptyConf,
|
|
cv: badCV,
|
|
expectedMessages: []*ResultMessage{},
|
|
},
|
|
{
|
|
name: "host ports + standard validation config",
|
|
networkConf: standardConf,
|
|
cv: badCV,
|
|
expectedMessages: []*ResultMessage{{
|
|
Message: "Host port should not be configured",
|
|
Type: "warning",
|
|
Category: "Networking",
|
|
}},
|
|
},
|
|
{
|
|
name: "no host ports + standard validation config",
|
|
networkConf: standardConf,
|
|
cv: goodCV,
|
|
expectedMessages: []*ResultMessage{{
|
|
Message: "Host port is not configured",
|
|
Type: "success",
|
|
Category: "Networking",
|
|
}},
|
|
},
|
|
{
|
|
name: "host ports + strong validation config",
|
|
networkConf: strongConf,
|
|
cv: badCV,
|
|
expectedMessages: []*ResultMessage{{
|
|
Message: "Host port should not be configured",
|
|
Type: "error",
|
|
Category: "Networking",
|
|
}},
|
|
},
|
|
}
|
|
|
|
for _, tt := range testCases {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
tt.cv = resetCV(tt.cv)
|
|
tt.cv.validateNetworking(&tt.networkConf)
|
|
assert.Len(t, tt.cv.messages(), len(tt.expectedMessages))
|
|
assert.ElementsMatch(t, tt.cv.messages(), tt.expectedMessages)
|
|
})
|
|
}
|
|
}
|
|
|
|
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,
|
|
Capabilities: conf.SecurityCapabilities{
|
|
Error: conf.SecurityCapabilityLists{
|
|
IfAnyAdded: []corev1.Capability{"ALL", "SYS_ADMIN", "NET_ADMIN"},
|
|
},
|
|
Warning: conf.SecurityCapabilityLists{
|
|
IfAnyAddedBeyond: []corev1.Capability{"NONE"},
|
|
},
|
|
},
|
|
}
|
|
strongConf := conf.Security{
|
|
RunAsRootAllowed: conf.SeverityError,
|
|
RunAsPrivileged: conf.SeverityError,
|
|
NotReadOnlyRootFileSystem: conf.SeverityError,
|
|
PrivilegeEscalationAllowed: conf.SeverityError,
|
|
Capabilities: conf.SecurityCapabilities{
|
|
Error: conf.SecurityCapabilityLists{
|
|
IfAnyAdded: []corev1.Capability{"ALL", "SYS_ADMIN", "NET_ADMIN"},
|
|
IfAnyNotDropped: []corev1.Capability{"NET_BIND_SERVICE", "DAC_OVERRIDE", "SYS_CHROOT"},
|
|
},
|
|
Warning: conf.SecurityCapabilityLists{
|
|
IfAnyAddedBeyond: []corev1.Capability{"NONE"},
|
|
},
|
|
},
|
|
}
|
|
|
|
emptyCV := ContainerValidation{
|
|
Container: &corev1.Container{Name: ""},
|
|
ResourceValidation: &ResourceValidation{},
|
|
}
|
|
|
|
badCV := ContainerValidation{
|
|
Container: &corev1.Container{Name: "", SecurityContext: &corev1.SecurityContext{
|
|
RunAsNonRoot: &falseVar,
|
|
ReadOnlyRootFilesystem: &falseVar,
|
|
Privileged: &trueVar,
|
|
AllowPrivilegeEscalation: &trueVar,
|
|
Capabilities: &corev1.Capabilities{
|
|
Add: []corev1.Capability{"AUDIT_CONTROL", "SYS_ADMIN", "NET_ADMIN"},
|
|
},
|
|
}},
|
|
ResourceValidation: &ResourceValidation{},
|
|
}
|
|
|
|
badCVWithGoodPodSpec := ContainerValidation{
|
|
Container: &corev1.Container{Name: "", SecurityContext: &corev1.SecurityContext{
|
|
RunAsNonRoot: &falseVar,
|
|
ReadOnlyRootFilesystem: &falseVar,
|
|
Privileged: &trueVar,
|
|
AllowPrivilegeEscalation: &trueVar,
|
|
Capabilities: &corev1.Capabilities{
|
|
Add: []corev1.Capability{"AUDIT_CONTROL", "SYS_ADMIN", "NET_ADMIN"},
|
|
},
|
|
}},
|
|
ResourceValidation: &ResourceValidation{},
|
|
parentPodSpec: corev1.PodSpec{
|
|
SecurityContext: &corev1.PodSecurityContext{
|
|
RunAsNonRoot: &trueVar,
|
|
},
|
|
},
|
|
}
|
|
|
|
badCVWithBadPodSpec := ContainerValidation{
|
|
Container: &corev1.Container{Name: "", SecurityContext: &corev1.SecurityContext{
|
|
RunAsNonRoot: nil, // this will use the default from the podspec
|
|
ReadOnlyRootFilesystem: &falseVar,
|
|
Privileged: &trueVar,
|
|
AllowPrivilegeEscalation: &trueVar,
|
|
Capabilities: &corev1.Capabilities{
|
|
Add: []corev1.Capability{"AUDIT_CONTROL", "SYS_ADMIN", "NET_ADMIN"},
|
|
},
|
|
}},
|
|
ResourceValidation: &ResourceValidation{},
|
|
parentPodSpec: corev1.PodSpec{
|
|
SecurityContext: &corev1.PodSecurityContext{
|
|
RunAsNonRoot: &falseVar,
|
|
},
|
|
},
|
|
}
|
|
|
|
goodCV := ContainerValidation{
|
|
Container: &corev1.Container{Name: "", SecurityContext: &corev1.SecurityContext{
|
|
RunAsNonRoot: &trueVar,
|
|
ReadOnlyRootFilesystem: &trueVar,
|
|
Privileged: &falseVar,
|
|
AllowPrivilegeEscalation: &falseVar,
|
|
Capabilities: &corev1.Capabilities{
|
|
Drop: []corev1.Capability{"NET_BIND_SERVICE", "FOWNER"},
|
|
},
|
|
}},
|
|
ResourceValidation: &ResourceValidation{},
|
|
}
|
|
|
|
strongCV := 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{},
|
|
}
|
|
|
|
strongCVWithPodSpecSecurityContext := ContainerValidation{
|
|
Container: &corev1.Container{Name: "", SecurityContext: &corev1.SecurityContext{
|
|
RunAsNonRoot: nil, // not set but overridden via podSpec
|
|
ReadOnlyRootFilesystem: &trueVar,
|
|
Privileged: &falseVar,
|
|
AllowPrivilegeEscalation: &falseVar,
|
|
Capabilities: &corev1.Capabilities{
|
|
Drop: []corev1.Capability{"ALL"},
|
|
},
|
|
}},
|
|
ResourceValidation: &ResourceValidation{},
|
|
parentPodSpec: corev1.PodSpec{
|
|
SecurityContext: &corev1.PodSecurityContext{
|
|
RunAsNonRoot: &trueVar,
|
|
},
|
|
},
|
|
}
|
|
|
|
strongCVWithBadPodSpecSecurityContext := ContainerValidation{
|
|
Container: &corev1.Container{Name: "", SecurityContext: &corev1.SecurityContext{
|
|
RunAsNonRoot: &trueVar, // will override the bad setting in PodSpec
|
|
ReadOnlyRootFilesystem: &trueVar,
|
|
Privileged: &falseVar,
|
|
AllowPrivilegeEscalation: &falseVar,
|
|
Capabilities: &corev1.Capabilities{
|
|
Drop: []corev1.Capability{"ALL"},
|
|
},
|
|
}},
|
|
ResourceValidation: &ResourceValidation{},
|
|
parentPodSpec: corev1.PodSpec{
|
|
SecurityContext: &corev1.PodSecurityContext{
|
|
RunAsNonRoot: &falseVar, // is overridden at container level with RunAsNonRoot:true
|
|
},
|
|
},
|
|
}
|
|
|
|
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: "empty security context + standard validation config",
|
|
securityConf: standardConf,
|
|
cv: emptyCV,
|
|
expectedMessages: []*ResultMessage{{
|
|
Message: "Should not be allowed to run as root",
|
|
Type: "warning",
|
|
Category: "Security",
|
|
}, {
|
|
Message: "Filesystem should be read only",
|
|
Type: "warning",
|
|
Category: "Security",
|
|
}, {
|
|
Message: "Not running as privileged",
|
|
Type: "success",
|
|
Category: "Security",
|
|
}, {
|
|
Message: "Privilege escalation not allowed",
|
|
Type: "success",
|
|
Category: "Security",
|
|
}, {
|
|
Message: "Security capabilities are within the configured limits",
|
|
Type: "success",
|
|
Category: "Security",
|
|
}},
|
|
},
|
|
{
|
|
name: "bad security context + standard validation config",
|
|
securityConf: standardConf,
|
|
cv: badCV,
|
|
expectedMessages: []*ResultMessage{{
|
|
Message: "The following security capabilities should not be added: SYS_ADMIN, NET_ADMIN",
|
|
Type: "error",
|
|
Category: "Security",
|
|
}, {
|
|
Message: "Privilege escalation should not be allowed",
|
|
Type: "error",
|
|
Category: "Security",
|
|
}, {
|
|
Message: "Should not be running as privileged",
|
|
Type: "error",
|
|
Category: "Security",
|
|
}, {
|
|
Message: "The following security capabilities should not be added: AUDIT_CONTROL, SYS_ADMIN, NET_ADMIN",
|
|
Type: "warning",
|
|
Category: "Security",
|
|
}, {
|
|
Message: "Should not be allowed to run as root",
|
|
Type: "warning",
|
|
Category: "Security",
|
|
}, {
|
|
Message: "Filesystem should be read only",
|
|
Type: "warning",
|
|
Category: "Security",
|
|
}},
|
|
},
|
|
{
|
|
name: "bad security context + standard validation config with good settings in podspec",
|
|
securityConf: standardConf,
|
|
cv: badCVWithGoodPodSpec,
|
|
expectedMessages: []*ResultMessage{{
|
|
Message: "The following security capabilities should not be added: SYS_ADMIN, NET_ADMIN",
|
|
Type: "error",
|
|
Category: "Security",
|
|
}, {
|
|
Message: "Privilege escalation should not be allowed",
|
|
Type: "error",
|
|
Category: "Security",
|
|
}, {
|
|
Message: "Should not be running as privileged",
|
|
Type: "error",
|
|
Category: "Security",
|
|
}, {
|
|
Message: "The following security capabilities should not be added: AUDIT_CONTROL, SYS_ADMIN, NET_ADMIN",
|
|
Type: "warning",
|
|
Category: "Security",
|
|
}, {
|
|
Message: "Should not be allowed to run as root",
|
|
Type: "warning",
|
|
Category: "Security",
|
|
}, {
|
|
Message: "Filesystem should be read only",
|
|
Type: "warning",
|
|
Category: "Security",
|
|
}},
|
|
},
|
|
{
|
|
name: "bad security context + standard validation config from default set in podspec",
|
|
securityConf: standardConf,
|
|
cv: badCVWithBadPodSpec,
|
|
expectedMessages: []*ResultMessage{{
|
|
Message: "The following security capabilities should not be added: SYS_ADMIN, NET_ADMIN",
|
|
Type: "error",
|
|
Category: "Security",
|
|
}, {
|
|
Message: "Privilege escalation should not be allowed",
|
|
Type: "error",
|
|
Category: "Security",
|
|
}, {
|
|
Message: "Should not be running as privileged",
|
|
Type: "error",
|
|
Category: "Security",
|
|
}, {
|
|
Message: "The following security capabilities should not be added: AUDIT_CONTROL, SYS_ADMIN, NET_ADMIN",
|
|
Type: "warning",
|
|
Category: "Security",
|
|
}, {
|
|
Message: "Should not be allowed to run as root",
|
|
Type: "warning",
|
|
Category: "Security",
|
|
}, {
|
|
Message: "Filesystem should be read only",
|
|
Type: "warning",
|
|
Category: "Security",
|
|
}},
|
|
},
|
|
{
|
|
name: "good security context + standard validation config",
|
|
securityConf: standardConf,
|
|
cv: goodCV,
|
|
expectedMessages: []*ResultMessage{{
|
|
Message: "Is not allowed to run as root",
|
|
Type: "success",
|
|
Category: "Security",
|
|
}, {
|
|
Message: "Filesystem is read only",
|
|
Type: "success",
|
|
Category: "Security",
|
|
}, {
|
|
Message: "Not running as privileged",
|
|
Type: "success",
|
|
Category: "Security",
|
|
}, {
|
|
Message: "Privilege escalation not allowed",
|
|
Type: "success",
|
|
Category: "Security",
|
|
}, {
|
|
Message: "Security capabilities are within the configured limits",
|
|
Type: "success",
|
|
Category: "Security",
|
|
}},
|
|
},
|
|
{
|
|
name: "good security context + strong validation config",
|
|
securityConf: strongConf,
|
|
cv: goodCV,
|
|
expectedMessages: []*ResultMessage{{
|
|
Message: "The following security capabilities should be dropped: DAC_OVERRIDE, SYS_CHROOT",
|
|
Type: "error",
|
|
Category: "Security",
|
|
}, {
|
|
Message: "Is not allowed to run as root",
|
|
Type: "success",
|
|
Category: "Security",
|
|
}, {
|
|
Message: "Filesystem is read only",
|
|
Type: "success",
|
|
Category: "Security",
|
|
}, {
|
|
Message: "Not running as privileged",
|
|
Type: "success",
|
|
Category: "Security",
|
|
}, {
|
|
Message: "Privilege escalation not allowed",
|
|
Type: "success",
|
|
Category: "Security",
|
|
}},
|
|
},
|
|
{
|
|
name: "strong security context + strong validation config",
|
|
securityConf: strongConf,
|
|
cv: strongCV,
|
|
expectedMessages: []*ResultMessage{{
|
|
Message: "Is not allowed to run as root",
|
|
Type: "success",
|
|
Category: "Security",
|
|
}, {
|
|
Message: "Filesystem is read only",
|
|
Type: "success",
|
|
Category: "Security",
|
|
}, {
|
|
Message: "Not running as privileged",
|
|
Type: "success",
|
|
Category: "Security",
|
|
}, {
|
|
Message: "Privilege escalation not allowed",
|
|
Type: "success",
|
|
Category: "Security",
|
|
}, {
|
|
Message: "Security capabilities are within the configured limits",
|
|
Type: "success",
|
|
Category: "Security",
|
|
}},
|
|
},
|
|
{
|
|
name: "strong security context + strong validation config via podspec default",
|
|
securityConf: strongConf,
|
|
cv: strongCVWithPodSpecSecurityContext,
|
|
expectedMessages: []*ResultMessage{{
|
|
Message: "Is not allowed to run as root",
|
|
Type: "success",
|
|
Category: "Security",
|
|
}, {
|
|
Message: "Filesystem is read only",
|
|
Type: "success",
|
|
Category: "Security",
|
|
}, {
|
|
Message: "Not running as privileged",
|
|
Type: "success",
|
|
Category: "Security",
|
|
}, {
|
|
Message: "Privilege escalation not allowed",
|
|
Type: "success",
|
|
Category: "Security",
|
|
}, {
|
|
Message: "Security capabilities are within the configured limits",
|
|
Type: "success",
|
|
Category: "Security",
|
|
}},
|
|
},
|
|
{
|
|
name: "strong security context + strong validation config with bad setting in podspec default",
|
|
securityConf: strongConf,
|
|
cv: strongCVWithBadPodSpecSecurityContext,
|
|
expectedMessages: []*ResultMessage{{
|
|
Message: "Is not allowed to run as root",
|
|
Type: "success",
|
|
Category: "Security",
|
|
}, {
|
|
Message: "Filesystem is read only",
|
|
Type: "success",
|
|
Category: "Security",
|
|
}, {
|
|
Message: "Not running as privileged",
|
|
Type: "success",
|
|
Category: "Security",
|
|
}, {
|
|
Message: "Privilege escalation not allowed",
|
|
Type: "success",
|
|
Category: "Security",
|
|
}, {
|
|
Message: "Security capabilities are within the configured limits",
|
|
Type: "success",
|
|
Category: "Security",
|
|
}},
|
|
},
|
|
}
|
|
|
|
for _, tt := range testCases {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
tt.cv = resetCV(tt.cv)
|
|
tt.cv.validateSecurity(&tt.securityConf)
|
|
assert.Len(t, tt.cv.messages(), len(tt.expectedMessages))
|
|
assert.ElementsMatch(t, tt.cv.messages(), tt.expectedMessages)
|
|
})
|
|
}
|
|
}
|
|
|
|
func resetCV(cv ContainerValidation) ContainerValidation {
|
|
cv.Errors = []*ResultMessage{}
|
|
cv.Successes = []*ResultMessage{}
|
|
cv.Warnings = []*ResultMessage{}
|
|
return cv
|
|
}
|