diff --git a/.golangci.yml b/.golangci.yml index eb8c96ece..de30f05e3 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -55,6 +55,7 @@ linters-settings: - importShadow - emptyStringTest - hugeParam + - rangeValCopy nolintlint: allow-leading-space: false # require machine-readable nolint directives (i.e. with no leading space) allow-unused: false # report any unused nolint directives diff --git a/docs/generated/checks.md b/docs/generated/checks.md index 47cc1057f..39d949f86 100644 --- a/docs/generated/checks.md +++ b/docs/generated/checks.md @@ -2,6 +2,8 @@ The following table enumerates built-in checks: | Name | Enabled by default | Description | Template | Parameters | | ---- | ------------------ | ----------- | -------- | ---------- | - | env-var-secret | No | Alert on objects using a secret in an environment variable | env-var |- `name`: `.*secret.*`
| + | env-var-secret | Yes | Alert on objects using a secret in an environment variable | env-var |- `name`: `.*secret.*`
| + | no-read-only-root-fs | Yes | Alert on containers not running with a read-only root filesystem | read-only-root-fs | none | | privileged-container | Yes | Alert on deployments with containers running in privileged mode | privileged | none | | required-label-owner | No | Alert on objects without the 'owner' label | required-label |- `key`: `owner`
| + | run-as-non-root | Yes | Alert on containers not set to runAsNonRoot | run-as-non-root | none | diff --git a/docs/generated/templates.md b/docs/generated/templates.md index 8d281f6db..627a4d7c8 100644 --- a/docs/generated/templates.md +++ b/docs/generated/templates.md @@ -4,4 +4,6 @@ The following table enumerates supported check templates: | ---- | ----------- | ----------------- | ---------- | | env-var | Flag environment variables that match the provided patterns | DeploymentLike |- `name` (required): A regex for the env var name
- `value`: A regex for the env var value
| | privileged | Flag privileged containers | DeploymentLike | none | + | read-only-root-fs | Flag containers without read-only root file systems | DeploymentLike | none | | required-label | Flag objects not carrying at least one label matching the provided patterns | Any |- `key` (required): A regex for the key of the required label
- `value`: A regex for the value of the required label
| + | run-as-non-root | Flag containers set to run as a root user | DeploymentLike | none | diff --git a/internal/builtinchecks/yamls/read-only-root-fs.yaml b/internal/builtinchecks/yamls/read-only-root-fs.yaml new file mode 100644 index 000000000..ec74a7a8d --- /dev/null +++ b/internal/builtinchecks/yamls/read-only-root-fs.yaml @@ -0,0 +1,6 @@ +name: "no-read-only-root-fs" +description: "Alert on containers not running with a read-only root filesystem" +scope: + objectKinds: + - DeploymentLike +template: "read-only-root-fs" diff --git a/internal/builtinchecks/yamls/run-as-non-root.yaml b/internal/builtinchecks/yamls/run-as-non-root.yaml new file mode 100644 index 000000000..2802f04dc --- /dev/null +++ b/internal/builtinchecks/yamls/run-as-non-root.yaml @@ -0,0 +1,6 @@ +name: "run-as-non-root" +description: "Alert on containers not set to runAsNonRoot" +scope: + objectKinds: + - DeploymentLike +template: "run-as-non-root" diff --git a/internal/defaultchecks/default_checks.go b/internal/defaultchecks/default_checks.go index 716f3fcb0..53ec130e9 100644 --- a/internal/defaultchecks/default_checks.go +++ b/internal/defaultchecks/default_checks.go @@ -8,5 +8,8 @@ var ( // List is the list of built-in checks that are enabled by default. List = set.NewFrozenStringSet( "privileged-container", + "env-var-secret", + "no-read-only-root-fs", + "run-as-non-root", ) ) diff --git a/internal/defaultchecks/default_test.go b/internal/defaultchecks/default_test.go new file mode 100644 index 000000000..cf388eb54 --- /dev/null +++ b/internal/defaultchecks/default_test.go @@ -0,0 +1,22 @@ +package defaultchecks + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.stackrox.io/kube-linter/internal/builtinchecks" + "golang.stackrox.io/kube-linter/internal/set" +) + +func TestListReferencesOnlyValidChecks(t *testing.T) { + allChecks, err := builtinchecks.List() + require.NoError(t, err) + allCheckNames := set.NewStringSet() + for _, check := range allChecks { + allCheckNames.Add(check.Name) + } + for _, defaultCheck := range List.AsSlice() { + assert.True(t, allCheckNames.Contains(defaultCheck), "default check %s invalid", defaultCheck) + } +} diff --git a/internal/templates/all/all.go b/internal/templates/all/all.go index 5ff875bf3..bb2b978bc 100644 --- a/internal/templates/all/all.go +++ b/internal/templates/all/all.go @@ -4,5 +4,7 @@ import ( // Import all check templates. _ "golang.stackrox.io/kube-linter/internal/templates/envvar" _ "golang.stackrox.io/kube-linter/internal/templates/privileged" + _ "golang.stackrox.io/kube-linter/internal/templates/readonlyrootfs" _ "golang.stackrox.io/kube-linter/internal/templates/requiredlabel" + _ "golang.stackrox.io/kube-linter/internal/templates/runasnonroot" ) diff --git a/internal/templates/readonlyrootfs/template.go b/internal/templates/readonlyrootfs/template.go new file mode 100644 index 000000000..370cbc03a --- /dev/null +++ b/internal/templates/readonlyrootfs/template.go @@ -0,0 +1,39 @@ +package readonlyrootfs + +import ( + "fmt" + + "golang.stackrox.io/kube-linter/internal/check" + "golang.stackrox.io/kube-linter/internal/diagnostic" + "golang.stackrox.io/kube-linter/internal/extract" + "golang.stackrox.io/kube-linter/internal/lintcontext" + "golang.stackrox.io/kube-linter/internal/objectkinds" + "golang.stackrox.io/kube-linter/internal/templates" +) + +func init() { + templates.Register(check.Template{ + Name: "read-only-root-fs", + Description: "Flag containers without read-only root file systems", + SupportedObjectKinds: check.ObjectKindsDesc{ + ObjectKinds: []string{objectkinds.DeploymentLike}, + }, + Parameters: nil, + Instantiate: func(_ map[string]string) (check.Func, error) { + return func(_ *lintcontext.LintContext, object lintcontext.Object) []diagnostic.Diagnostic { + podSpec, found := extract.PodSpec(object.K8sObject) + if !found { + return nil + } + var results []diagnostic.Diagnostic + for _, container := range podSpec.Containers { + sc := container.SecurityContext + if sc == nil || sc.ReadOnlyRootFilesystem == nil || !*sc.ReadOnlyRootFilesystem { + results = append(results, diagnostic.Diagnostic{Message: fmt.Sprintf("container %q does not have a read-only root file system", container.Name)}) + } + } + return results + }, nil + }, + }) +} diff --git a/internal/templates/runasnonroot/template.go b/internal/templates/runasnonroot/template.go new file mode 100644 index 000000000..0c13e8ddb --- /dev/null +++ b/internal/templates/runasnonroot/template.go @@ -0,0 +1,72 @@ +package runasnonroot + +import ( + "fmt" + + "golang.stackrox.io/kube-linter/internal/check" + "golang.stackrox.io/kube-linter/internal/diagnostic" + "golang.stackrox.io/kube-linter/internal/extract" + "golang.stackrox.io/kube-linter/internal/lintcontext" + "golang.stackrox.io/kube-linter/internal/objectkinds" + "golang.stackrox.io/kube-linter/internal/templates" + v1 "k8s.io/api/core/v1" +) + +func effectiveRunAsNonRoot(podSC *v1.PodSecurityContext, containerSC *v1.SecurityContext) bool { + if containerSC != nil && containerSC.RunAsNonRoot != nil { + return *containerSC.RunAsNonRoot + } + if podSC != nil && podSC.RunAsNonRoot != nil { + return *podSC.RunAsNonRoot + } + return false +} + +func effectiveRunAsUser(podSC *v1.PodSecurityContext, containerSC *v1.SecurityContext) *int64 { + if containerSC != nil && containerSC.RunAsUser != nil { + return containerSC.RunAsUser + } + if podSC != nil { + return podSC.RunAsUser + } + return nil +} + +func init() { + templates.Register(check.Template{ + Name: "run-as-non-root", + Description: "Flag containers set to run as a root user", + SupportedObjectKinds: check.ObjectKindsDesc{ + ObjectKinds: []string{objectkinds.DeploymentLike}, + }, + Parameters: nil, + Instantiate: func(_ map[string]string) (check.Func, error) { + return func(_ *lintcontext.LintContext, object lintcontext.Object) []diagnostic.Diagnostic { + podSpec, found := extract.PodSpec(object.K8sObject) + if !found { + return nil + } + var results []diagnostic.Diagnostic + for _, container := range podSpec.Containers { + runAsUser := effectiveRunAsUser(podSpec.SecurityContext, container.SecurityContext) + // runAsUser explicitly set to non-root. All good. + if runAsUser != nil && *runAsUser > 0 { + continue + } + runAsNonRoot := effectiveRunAsNonRoot(podSpec.SecurityContext, container.SecurityContext) + if runAsNonRoot { + // runAsNonRoot set, but runAsUser set to 0. This will result in a runtime failure. + if runAsUser != nil && *runAsUser == 0 { + results = append(results, diagnostic.Diagnostic{ + Message: fmt.Sprintf("container %q is set to runAsNonRoot, but runAsUser set to %d", container.Name, *runAsUser), + }) + } + continue + } + results = append(results, diagnostic.Diagnostic{Message: fmt.Sprintf("container %q is not set to runAsNonRoot", container.Name)}) + } + return results + }, nil + }, + }) +}