diff --git a/checks.md b/checks.md index b52dd2b..2a9fa22 100644 --- a/checks.md +++ b/checks.md @@ -106,6 +106,37 @@ spec: - NET_ADMIN ``` +###### Run As Non-Root + +Name: `run-as-non-root` + +Group: `security` + +Description: If containers within a pod are allowed to run with the pid `0`, then the host can be subjected to malicious attacks. It occurs when the container image or the base image is not from a trusted source. We recommend that a UID other than 0 be used in your container image for running applications. However, this can also be enforced in the Kubernetes pod configuration shown below. + +Example: + +```yaml +# Don't do this +spec: + containers: + - name: mypod + image: nginx +``` + +How to fix: + +```yaml +# Specify to error out when container is run as root +spec: + securityContext: + runAsNonRoot: true + containers: + - name: mypod + image: nginx + +``` + ###### Fully Qualified Image Name: `fully-qualified-image` diff --git a/checks/security/helper_test.go b/checks/security/helper_test.go new file mode 100644 index 0000000..13bb132 --- /dev/null +++ b/checks/security/helper_test.go @@ -0,0 +1,141 @@ +/* +Copyright 2019 DigitalOcean + +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 security + +import ( + "github.com/digitalocean/clusterlint/kube" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func initPod() *kube.Objects { + objs := &kube.Objects{ + Pods: &corev1.PodList{ + Items: []corev1.Pod{ + { + TypeMeta: metav1.TypeMeta{Kind: "Pod", APIVersion: "v1"}, + ObjectMeta: metav1.ObjectMeta{Name: "pod_foo", Namespace: "k8s"}, + }, + }, + }, + } + return objs +} + +func containerPrivileged(privileged bool) *kube.Objects { + objs := initPod() + objs.Pods.Items[0].Spec = corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "bar", + SecurityContext: &corev1.SecurityContext{Privileged: &privileged}, + }}, + } + return objs +} + +func containerNonRoot(pod, container *bool) *kube.Objects { + objs := initPod() + podSecurityContext := &corev1.PodSecurityContext{} + if pod != nil { + podSecurityContext = &corev1.PodSecurityContext{RunAsNonRoot: pod} + } + objs.Pods.Items[0].Spec = corev1.PodSpec{ + SecurityContext: podSecurityContext, + Containers: []corev1.Container{ + { + Name: "bar", + SecurityContext: &corev1.SecurityContext{RunAsNonRoot: container}, + }}, + } + return objs +} + +func initContainerNonRoot(pod, container *bool) *kube.Objects { + objs := initPod() + podSecurityContext := &corev1.PodSecurityContext{} + if pod != nil { + podSecurityContext = &corev1.PodSecurityContext{RunAsNonRoot: pod} + } + objs.Pods.Items[0].Spec = corev1.PodSpec{ + SecurityContext: podSecurityContext, + InitContainers: []corev1.Container{ + { + Name: "bar", + SecurityContext: &corev1.SecurityContext{RunAsNonRoot: container}, + }}, + } + return objs +} + +func containerSecurityContextNil() *kube.Objects { + objs := initPod() + objs.Pods.Items[0].Spec = corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "bar", + }}, + } + return objs +} + +func containerPrivilegedNil() *kube.Objects { + objs := initPod() + objs.Pods.Items[0].Spec = corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "bar", + SecurityContext: &corev1.SecurityContext{}, + }}, + } + return objs +} + +func initContainerPrivileged(privileged bool) *kube.Objects { + objs := initPod() + objs.Pods.Items[0].Spec = corev1.PodSpec{ + InitContainers: []corev1.Container{ + { + Name: "bar", + SecurityContext: &corev1.SecurityContext{Privileged: &privileged}, + }}, + } + return objs +} + +func initContainerSecurityContextNil() *kube.Objects { + objs := initPod() + objs.Pods.Items[0].Spec = corev1.PodSpec{ + InitContainers: []corev1.Container{ + { + Name: "bar", + }}, + } + return objs +} + +func initContainerPrivilegedNil() *kube.Objects { + objs := initPod() + objs.Pods.Items[0].Spec = corev1.PodSpec{ + InitContainers: []corev1.Container{ + { + Name: "bar", + SecurityContext: &corev1.SecurityContext{}, + }}, + } + return objs +} diff --git a/checks/security/privileged_containers_test.go b/checks/security/privileged_containers_test.go index ae6338c..5136021 100644 --- a/checks/security/privileged_containers_test.go +++ b/checks/security/privileged_containers_test.go @@ -22,8 +22,6 @@ import ( "github.com/digitalocean/clusterlint/checks" "github.com/digitalocean/clusterlint/kube" "github.com/stretchr/testify/assert" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) func TestPrivilegedContainersCheckMeta(t *testing.T) { @@ -55,8 +53,8 @@ func TestPrivilegedContainerWarning(t *testing.T) { }, { name: "pod with container in privileged mode", - objs: container(true), - expected: warnings(container(true), privilegedContainerCheck.Name()), + objs: containerPrivileged(true), + expected: warnings(containerPrivileged(true), privilegedContainerCheck.Name()), }, { name: "pod with container.SecurityContext = nil", @@ -70,13 +68,13 @@ func TestPrivilegedContainerWarning(t *testing.T) { }, { name: "pod with container in regular mode", - objs: container(false), + objs: containerPrivileged(false), expected: nil, }, { name: "pod with init container in privileged mode", - objs: initContainer(true), - expected: warnings(initContainer(true), privilegedContainerCheck.Name()), + objs: initContainerPrivileged(true), + expected: warnings(initContainerPrivileged(true), privilegedContainerCheck.Name()), }, { name: "pod with initContainer.SecurityContext = nil", @@ -90,7 +88,7 @@ func TestPrivilegedContainerWarning(t *testing.T) { }, { name: "pod with init container in regular mode", - objs: initContainer(false), + objs: initContainerPrivileged(false), expected: nil, }, } @@ -104,90 +102,6 @@ func TestPrivilegedContainerWarning(t *testing.T) { } } -func initPod() *kube.Objects { - objs := &kube.Objects{ - Pods: &corev1.PodList{ - Items: []corev1.Pod{ - { - TypeMeta: metav1.TypeMeta{Kind: "Pod", APIVersion: "v1"}, - ObjectMeta: metav1.ObjectMeta{Name: "pod_foo", Namespace: "k8s"}, - }, - }, - }, - } - return objs -} - -func container(privileged bool) *kube.Objects { - objs := initPod() - objs.Pods.Items[0].Spec = corev1.PodSpec{ - Containers: []corev1.Container{ - { - Name: "bar", - SecurityContext: &corev1.SecurityContext{Privileged: &privileged}, - }}, - } - return objs -} - -func containerSecurityContextNil() *kube.Objects { - objs := initPod() - objs.Pods.Items[0].Spec = corev1.PodSpec{ - Containers: []corev1.Container{ - { - Name: "bar", - }}, - } - return objs -} - -func containerPrivilegedNil() *kube.Objects { - objs := initPod() - objs.Pods.Items[0].Spec = corev1.PodSpec{ - Containers: []corev1.Container{ - { - Name: "bar", - SecurityContext: &corev1.SecurityContext{}, - }}, - } - return objs -} - -func initContainer(privileged bool) *kube.Objects { - objs := initPod() - objs.Pods.Items[0].Spec = corev1.PodSpec{ - InitContainers: []corev1.Container{ - { - Name: "bar", - SecurityContext: &corev1.SecurityContext{Privileged: &privileged}, - }}, - } - return objs -} - -func initContainerSecurityContextNil() *kube.Objects { - objs := initPod() - objs.Pods.Items[0].Spec = corev1.PodSpec{ - InitContainers: []corev1.Container{ - { - Name: "bar", - }}, - } - return objs -} - -func initContainerPrivilegedNil() *kube.Objects { - objs := initPod() - objs.Pods.Items[0].Spec = corev1.PodSpec{ - InitContainers: []corev1.Container{ - { - Name: "bar", - SecurityContext: &corev1.SecurityContext{}, - }}, - } - return objs -} - func warnings(objs *kube.Objects, name string) []checks.Diagnostic { pod := objs.Pods.Items[0] d := []checks.Diagnostic{ diff --git a/checks/security/run_as_non_root.go b/checks/security/run_as_non_root.go new file mode 100644 index 0000000..99ff168 --- /dev/null +++ b/checks/security/run_as_non_root.go @@ -0,0 +1,82 @@ +/* +Copyright 2019 DigitalOcean + +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 security + +import ( + "fmt" + + "github.com/digitalocean/clusterlint/checks" + "github.com/digitalocean/clusterlint/kube" + corev1 "k8s.io/api/core/v1" +) + +func init() { + checks.Register(&nonRootUserCheck{}) +} + +type nonRootUserCheck struct{} + +// Name returns a unique name for this check. +func (nr *nonRootUserCheck) Name() string { + return "non-root-user" +} + +// Groups returns a list of group names this check should be part of. +func (nr *nonRootUserCheck) Groups() []string { + return []string{"security"} +} + +// Description returns a detailed human-readable description of what this check +// does. +func (nr *nonRootUserCheck) Description() string { + return "Checks if there are pods which run as root user" +} + +// Run runs this check on a set of Kubernetes objects. It can return warnings +// (low-priority problems) and errors (high-priority problems) as well as an +// error value indicating that the check failed to run. +func (nr *nonRootUserCheck) Run(objects *kube.Objects) ([]checks.Diagnostic, error) { + var diagnostics []checks.Diagnostic + + for _, pod := range objects.Pods.Items { + diagnostics = append(diagnostics, nr.checkRootUser(pod.Spec.Containers, pod)...) + diagnostics = append(diagnostics, nr.checkRootUser(pod.Spec.InitContainers, pod)...) + } + + return diagnostics, nil +} + +func (nr *nonRootUserCheck) checkRootUser(containers []corev1.Container, pod corev1.Pod) []checks.Diagnostic { + var diagnostics []checks.Diagnostic + for _, container := range containers { + podRunAsRoot := pod.Spec.SecurityContext == nil || pod.Spec.SecurityContext.RunAsNonRoot == nil || !*pod.Spec.SecurityContext.RunAsNonRoot + containerRunAsRoot := container.SecurityContext == nil || container.SecurityContext.RunAsNonRoot == nil || !*container.SecurityContext.RunAsNonRoot + + if containerRunAsRoot && podRunAsRoot { + d := checks.Diagnostic{ + Check: nr.Name(), + Severity: checks.Warning, + Message: fmt.Sprintf("Container `%s` can run as root user. Please ensure that the image is from a trusted source.", container.Name), + Kind: checks.Pod, + Object: &pod.ObjectMeta, + Owners: pod.ObjectMeta.GetOwnerReferences(), + } + diagnostics = append(diagnostics, d) + } + } + return diagnostics +} diff --git a/checks/security/run_as_non_root_test.go b/checks/security/run_as_non_root_test.go new file mode 100644 index 0000000..b606a3b --- /dev/null +++ b/checks/security/run_as_non_root_test.go @@ -0,0 +1,173 @@ +/* +Copyright 2019 DigitalOcean + +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 security + +import ( + "testing" + + "github.com/digitalocean/clusterlint/checks" + "github.com/digitalocean/clusterlint/kube" + "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" +) + +func TestNonRootUserCheckMeta(t *testing.T) { + nonRootUserCheck := nonRootUserCheck{} + assert.Equal(t, "non-root-user", nonRootUserCheck.Name()) + assert.Equal(t, []string{"security"}, nonRootUserCheck.Groups()) + assert.NotEmpty(t, nonRootUserCheck.Description()) +} + +func TestNonRootUserCheckRegistration(t *testing.T) { + nonRootUserCheck := &nonRootUserCheck{} + check, err := checks.Get("non-root-user") + assert.NoError(t, err) + assert.Equal(t, check, nonRootUserCheck) +} + +func TestNonRootUserWarning(t *testing.T) { + nonRootUserCheck := nonRootUserCheck{} + trueVar := true + falseVar := false + + tests := []struct { + name string + objs *kube.Objects + expected []checks.Diagnostic + }{ + { + name: "no pods", + objs: &kube.Objects{Pods: &corev1.PodList{}}, + expected: nil, + }, + { + name: "pod security context and container security context unset", + objs: containerSecurityContextNil(), + expected: diagnostic(), + }, + { + name: "pod security context unset, container with run as non root set to true", + objs: containerNonRoot(nil, &trueVar), + expected: nil, + }, + { + name: "pod security context unset, container with run as non root set to false", + objs: containerNonRoot(nil, &falseVar), + expected: diagnostic(), + }, + { + name: "pod run as non root true, container run as non root true", + objs: containerNonRoot(&trueVar, &trueVar), + expected: nil, + }, + { + name: "pod run as non root true, container run as non root false", + objs: containerNonRoot(&trueVar, &falseVar), + expected: nil, + }, + { + name: "pod run as non root false, container run as non root true", + objs: containerNonRoot(&falseVar, &trueVar), + expected: nil, + }, + { + name: "pod run as non root false, container run as non root false", + objs: containerNonRoot(&falseVar, &falseVar), + expected: diagnostic(), + }, + { + name: "pod run as non root true, container security context unset", + objs: containerNonRoot(&trueVar, nil), + expected: nil, + }, + { + name: "pod run as non root false, container security context unset", + objs: containerNonRoot(&falseVar, nil), + expected: diagnostic(), + }, + // init container tests + + { + name: "pod security context and init container security context unset", + objs: initContainerSecurityContextNil(), + expected: diagnostic(), + }, + { + name: "pod security context unset, init container with run as non root set to true", + objs: initContainerNonRoot(nil, &trueVar), + expected: nil, + }, + { + name: "pod security context unset, init container with run as non root set to false", + objs: initContainerNonRoot(nil, &falseVar), + expected: diagnostic(), + }, + { + name: "pod run as non root true, init container run as non root true", + objs: initContainerNonRoot(&trueVar, &trueVar), + expected: nil, + }, + { + name: "pod run as non root true, init container run as non root false", + objs: initContainerNonRoot(&trueVar, &falseVar), + expected: nil, + }, + { + name: "pod run as non root false, init container run as non root true", + objs: initContainerNonRoot(&falseVar, &trueVar), + expected: nil, + }, + { + name: "pod run as non root false, init container run as non root false", + objs: initContainerNonRoot(&falseVar, &falseVar), + expected: diagnostic(), + }, + { + name: "pod run as non root true, init container security context unset", + objs: initContainerNonRoot(&trueVar, nil), + expected: nil, + }, + { + name: "pod run as non root false, init container security context unset", + objs: initContainerNonRoot(&falseVar, nil), + expected: diagnostic(), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + d, err := nonRootUserCheck.Run(test.objs) + assert.NoError(t, err) + assert.ElementsMatch(t, test.expected, d) + }) + } +} + +func diagnostic() []checks.Diagnostic { + pod := initPod().Pods.Items[0] + d := []checks.Diagnostic{ + { + Check: "non-root-user", + Severity: checks.Warning, + Message: "Container `bar` can run as root user. Please ensure that the image is from a trusted source.", + Kind: checks.Pod, + Object: &pod.ObjectMeta, + Owners: pod.ObjectMeta.GetOwnerReferences(), + }, + } + return d +}