diff --git a/docs/generated/checks.md b/docs/generated/checks.md index 690cb8f25..8b26682f6 100644 --- a/docs/generated/checks.md +++ b/docs/generated/checks.md @@ -513,6 +513,21 @@ key: owner **Remediation**: Set runAsUser to a non-zero number and runAsNonRoot to true in your pod or container securityContext. Refer to https://kubernetes.io/docs/tasks/configure-pod-container/security-context/ for details. **Template**: [run-as-non-root](templates.md#run-as-non-root-user) +## scc-deny-privileged-container + +**Enabled by default**: No + +**Description**: Indicates when allowPrivilegedContainer SecurityContextConstraints set to true + +**Remediation**: SecurityContextConstraints has AllowPrivilegedContainer set to "true". Using this option is dangerous, please consider using allowedCapabilities instead. Refer to https://docs.openshift.com/container-platform/4.12/authentication/managing-security-context-constraints.html#scc-settings_configuring-internal-oauth for details. + +**Template**: [scc-deny-privileged-container](templates.md#securitycontextconstraints-allowprivilegedcontainer) + +**Parameters**: + +```yaml +AllowPrivilegedContainer: true +``` ## sensitive-host-mounts **Enabled by default**: Yes diff --git a/docs/generated/templates.md b/docs/generated/templates.md index 57c67be8b..569e0c059 100644 --- a/docs/generated/templates.md +++ b/docs/generated/templates.md @@ -694,6 +694,24 @@ KubeLinter supports the following templates: **Supported Objects**: DeploymentLike +## SecurityContextConstraints allowPrivilegedContainer + +**Key**: `scc-deny-privileged-container` + +**Description**: Flag SCC with allowPrivilegedContainer set to true + +**Supported Objects**: SecurityContextConstraints + + +**Parameters**: + +```yaml +- description: allowPrivilegedContainer value + name: allowPrivilegedContainer + required: false + type: boolean +``` + ## Service Account **Key**: `service-account` diff --git a/e2etests/bats-tests.sh b/e2etests/bats-tests.sh index 8371d6ab1..c9b03eba3 100755 --- a/e2etests/bats-tests.sh +++ b/e2etests/bats-tests.sh @@ -739,6 +739,21 @@ get_value_from() { [[ "${count}" == "2" ]] } +@test "scc-deny-privileged-container" { + tmp="tests/checks/scc-deny-privileged-container.yml" + cmd="${KUBE_LINTER_BIN} lint --include scc-deny-privileged-container --do-not-auto-add-defaults --format json ${tmp}" + run ${cmd} + + print_info "${status}" "${output}" "${cmd}" "${tmp}" + [ "$status" -eq 1 ] + + message1=$(get_value_from "${lines[0]}" '.Reports[0].Object.K8sObject.GroupVersionKind.Kind + ": " + .Reports[0].Diagnostic.Message') + count=$(get_value_from "${lines[0]}" '.Reports | length') + + [[ "${message1}" == "SecurityContextConstraints: SCC has allowPrivilegedContainer set to true" ]] + [[ "${count}" == "1" ]] +} + @test "sensitive-host-mounts" { tmp="tests/checks/sensitive-host-mounts.yml" cmd="${KUBE_LINTER_BIN} lint --include sensitive-host-mounts --do-not-auto-add-defaults --format json ${tmp}" diff --git a/pkg/builtinchecks/yamls/scc-deny-privileged-container.yaml b/pkg/builtinchecks/yamls/scc-deny-privileged-container.yaml new file mode 100644 index 000000000..bb6474ac9 --- /dev/null +++ b/pkg/builtinchecks/yamls/scc-deny-privileged-container.yaml @@ -0,0 +1,10 @@ +name: "scc-deny-privileged-container" +description: "Indicates when allowPrivilegedContainer SecurityContextConstraints set to true" +remediation: >- + SecurityContextConstraints has AllowPrivilegedContainer set to "true". Using this option is dangerous, please consider using allowedCapabilities instead. Refer to https://docs.openshift.com/container-platform/4.12/authentication/managing-security-context-constraints.html#scc-settings_configuring-internal-oauth for details. +scope: + objectKinds: + - SecurityContextConstraints +template: "scc-deny-privileged-container" +params: + AllowPrivilegedContainer: true \ No newline at end of file diff --git a/pkg/extract/scc_spec.go b/pkg/extract/scc_spec.go new file mode 100644 index 000000000..f84182596 --- /dev/null +++ b/pkg/extract/scc_spec.go @@ -0,0 +1,14 @@ +package extract + +import ( + ocpSecV1 "github.com/openshift/api/security/v1" + "golang.stackrox.io/kube-linter/pkg/k8sutil" +) + +// SCCallowPrivilegedContainer extracts allowPrivilegedContainer from the given object, if available. +func SCCallowPrivilegedContainer(obj k8sutil.Object) (bool, bool) { + if scc, ok := obj.(*ocpSecV1.SecurityContextConstraints); ok { + return scc.AllowPrivilegedContainer, true + } + return false, false +} diff --git a/pkg/lintcontext/mocks/scc.go b/pkg/lintcontext/mocks/scc.go new file mode 100644 index 000000000..4039c81ee --- /dev/null +++ b/pkg/lintcontext/mocks/scc.go @@ -0,0 +1,23 @@ +package mocks + +import ( + "testing" + + ocpSecV1 "github.com/openshift/api/security/v1" + "github.com/stretchr/testify/require" + "golang.stackrox.io/kube-linter/pkg/objectkinds" + metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// AddMockSecurityContextConstraints adds a mock SecurityContextConstraints to LintContext +func (l *MockLintContext) AddMockSecurityContextConstraints(t *testing.T, name string, allowFlag bool) { + require.NotEmpty(t, name) + l.objects[name] = &ocpSecV1.SecurityContextConstraints{ + TypeMeta: metaV1.TypeMeta{ + Kind: objectkinds.SecurityContextConstraints, + APIVersion: objectkinds.GetSCCAPIVersion(), + }, + ObjectMeta: metaV1.ObjectMeta{Name: name}, + AllowPrivilegedContainer: allowFlag, + } +} diff --git a/pkg/lintcontext/parse_yaml.go b/pkg/lintcontext/parse_yaml.go index 6d3d139e6..2cf8a78df 100644 --- a/pkg/lintcontext/parse_yaml.go +++ b/pkg/lintcontext/parse_yaml.go @@ -12,6 +12,7 @@ import ( y "github.com/ghodss/yaml" ocsAppsV1 "github.com/openshift/api/apps/v1" + ocpSecV1 "github.com/openshift/api/security/v1" "github.com/pkg/errors" k8sMonitoring "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1" "golang.stackrox.io/kube-linter/pkg/k8sutil" @@ -42,7 +43,7 @@ func init() { clientScheme := scheme.Scheme // Add OpenShift and Autoscaling schema - schemeBuilder := runtime.NewSchemeBuilder(ocsAppsV1.AddToScheme, autoscalingV2Beta1.AddToScheme, k8sMonitoring.AddToScheme) + schemeBuilder := runtime.NewSchemeBuilder(ocsAppsV1.AddToScheme, autoscalingV2Beta1.AddToScheme, k8sMonitoring.AddToScheme, ocpSecV1.AddToScheme) if err := schemeBuilder.AddToScheme(clientScheme); err != nil { panic(fmt.Sprintf("Can not add OpenShift schema %v", err)) } diff --git a/pkg/objectkinds/securitycontext.go b/pkg/objectkinds/securitycontext.go new file mode 100644 index 000000000..4e64a31e3 --- /dev/null +++ b/pkg/objectkinds/securitycontext.go @@ -0,0 +1,26 @@ +package objectkinds + +import ( + ocpSecV1 "github.com/openshift/api/security/v1" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +const ( + // Service represents Kubernetes Service objects. + SecurityContextConstraints = "SecurityContextConstraints" +) + +var ( + sccGVK = ocpSecV1.SchemeGroupVersion.WithKind("SecurityContextConstraints") +) + +func init() { + RegisterObjectKind(SecurityContextConstraints, MatcherFunc(func(gvk schema.GroupVersionKind) bool { + return gvk == sccGVK + })) +} + +// GetSCCAPIVersion returns SCC's apiversion +func GetSCCAPIVersion() string { + return sccGVK.GroupVersion().String() +} diff --git a/pkg/templates/all/all.go b/pkg/templates/all/all.go index 1834dc4e5..e29ed9639 100644 --- a/pkg/templates/all/all.go +++ b/pkg/templates/all/all.go @@ -46,6 +46,7 @@ import ( _ "golang.stackrox.io/kube-linter/pkg/templates/requiredannotation" _ "golang.stackrox.io/kube-linter/pkg/templates/requiredlabel" _ "golang.stackrox.io/kube-linter/pkg/templates/runasnonroot" + _ "golang.stackrox.io/kube-linter/pkg/templates/sccdenypriv" _ "golang.stackrox.io/kube-linter/pkg/templates/serviceaccount" _ "golang.stackrox.io/kube-linter/pkg/templates/servicetype" _ "golang.stackrox.io/kube-linter/pkg/templates/sysctl" diff --git a/pkg/templates/sccdenypriv/internal/params/gen-params.go b/pkg/templates/sccdenypriv/internal/params/gen-params.go new file mode 100644 index 000000000..e5151293a --- /dev/null +++ b/pkg/templates/sccdenypriv/internal/params/gen-params.go @@ -0,0 +1,69 @@ +// Code generated by kube-linter template codegen. DO NOT EDIT. +// +build !templatecodegen + +package params + +import ( + "fmt" + "strings" + + "github.com/pkg/errors" + "golang.stackrox.io/kube-linter/pkg/check" + "golang.stackrox.io/kube-linter/pkg/templates/util" +) + +var ( + // Use some imports in case they don't get used otherwise. + _ = util.MustParseParameterDesc + _ = fmt.Sprintf + + allowPrivilegedContainerParamDesc = util.MustParseParameterDesc(`{ + "Name": "allowPrivilegedContainer", + "Type": "boolean", + "Description": "allowPrivilegedContainer value", + "Examples": null, + "Enum": null, + "SubParameters": null, + "ArrayElemType": "", + "Required": false, + "NoRegex": false, + "NotNegatable": false, + "XXXStructFieldName": "AllowPrivilegedContainer", + "XXXIsPointer": false +} +`) + + ParamDescs = []check.ParameterDesc{ + allowPrivilegedContainerParamDesc, + } +) + +func (p *Params) Validate() error { + var validationErrors []string + if len(validationErrors) > 0 { + return errors.Errorf("invalid parameters: %s", strings.Join(validationErrors, ", ")) + } + return nil +} + +// ParseAndValidate instantiates a Params object out of the passed map[string]interface{}, +// validates it, and returns it. +// The return type is interface{} to satisfy the type in the Template struct. +func ParseAndValidate(m map[string]interface{}) (interface{}, error) { + var p Params + if err := util.DecodeMapStructure(m, &p); err != nil { + return nil, err + } + if err := p.Validate(); err != nil { + return nil, err + } + return p, nil +} + +// WrapInstantiateFunc is a convenience wrapper that wraps an untyped instantiate function +// into a typed one. +func WrapInstantiateFunc(f func(p Params) (check.Func, error)) func (interface{}) (check.Func, error) { + return func(paramsInt interface{}) (check.Func, error) { + return f(paramsInt.(Params)) + } +} diff --git a/pkg/templates/sccdenypriv/internal/params/params.go b/pkg/templates/sccdenypriv/internal/params/params.go new file mode 100644 index 000000000..3abd8f01b --- /dev/null +++ b/pkg/templates/sccdenypriv/internal/params/params.go @@ -0,0 +1,8 @@ +package params + +// Params represents the params accepted by this template. +type Params struct { + + // allowPrivilegedContainer value + AllowPrivilegedContainer bool +} diff --git a/pkg/templates/sccdenypriv/template.go b/pkg/templates/sccdenypriv/template.go new file mode 100644 index 000000000..dc120faf6 --- /dev/null +++ b/pkg/templates/sccdenypriv/template.go @@ -0,0 +1,42 @@ +package sccdenypriv + +import ( + "fmt" + + "golang.stackrox.io/kube-linter/pkg/check" + "golang.stackrox.io/kube-linter/pkg/config" + "golang.stackrox.io/kube-linter/pkg/diagnostic" + "golang.stackrox.io/kube-linter/pkg/extract" + "golang.stackrox.io/kube-linter/pkg/lintcontext" + "golang.stackrox.io/kube-linter/pkg/objectkinds" + "golang.stackrox.io/kube-linter/pkg/templates" + "golang.stackrox.io/kube-linter/pkg/templates/sccdenypriv/internal/params" +) + +const ( + templateKey = "scc-deny-privileged-container" +) + +func init() { + templates.Register(check.Template{ + HumanName: "SecurityContextConstraints allowPrivilegedContainer", + Key: templateKey, + Description: "Flag SCC with allowPrivilegedContainer set to true", + SupportedObjectKinds: config.ObjectKindsDesc{ + ObjectKinds: []string{objectkinds.SecurityContextConstraints}, + }, + Parameters: params.ParamDescs, + ParseAndValidateParams: params.ParseAndValidate, + Instantiate: params.WrapInstantiateFunc(func(p params.Params) (check.Func, error) { + return func(_ lintcontext.LintContext, object lintcontext.Object) []diagnostic.Diagnostic { + state, found := extract.SCCallowPrivilegedContainer(object.K8sObject) + if found && state == p.AllowPrivilegedContainer { + return []diagnostic.Diagnostic{ + {Message: fmt.Sprintf("SCC has allowPrivilegedContainer set to %v", state)}, + } + } + return nil + }, nil + }), + }) +} diff --git a/pkg/templates/sccdenypriv/template_test.go b/pkg/templates/sccdenypriv/template_test.go new file mode 100644 index 000000000..e12d1a3f4 --- /dev/null +++ b/pkg/templates/sccdenypriv/template_test.go @@ -0,0 +1,70 @@ +package sccdenypriv + +import ( + "testing" + + "github.com/stretchr/testify/suite" + "golang.stackrox.io/kube-linter/pkg/diagnostic" + "golang.stackrox.io/kube-linter/pkg/lintcontext/mocks" + "golang.stackrox.io/kube-linter/pkg/templates" + "golang.stackrox.io/kube-linter/pkg/templates/sccdenypriv/internal/params" +) + +func TestSCCPriv(t *testing.T) { + suite.Run(t, new(SCCPrivTestSuite)) +} + +type SCCPrivTestSuite struct { + templates.TemplateTestSuite + + ctx *mocks.MockLintContext +} + +func (s *SCCPrivTestSuite) SetupTest() { + s.Init(templateKey) + s.ctx = mocks.NewMockContext() +} + +func (s *SCCPrivTestSuite) addSCCWithPriv(name string, allowFlag bool) { + s.ctx.AddMockSecurityContextConstraints(s.T(), name, allowFlag) +} + +func (s *SCCPrivTestSuite) TestPrivFalse() { + const acceptableScc = "scc-priv-false" + + s.addSCCWithPriv(acceptableScc, false) + + s.Validate(s.ctx, []templates.TestCase{ + { + Param: params.Params{ + AllowPrivilegedContainer: true, + }, + Diagnostics: map[string][]diagnostic.Diagnostic{ + acceptableScc: nil, + }, + ExpectInstantiationError: false, + }, + }) +} + +func (s *SCCPrivTestSuite) TestPrivTrue() { + const ( + unacceptableScc = "scc-priv-true" + ) + + s.addSCCWithPriv(unacceptableScc, true) + + s.Validate(s.ctx, []templates.TestCase{ + { + Param: params.Params{ + AllowPrivilegedContainer: true, + }, + Diagnostics: map[string][]diagnostic.Diagnostic{ + unacceptableScc: { + {Message: "SCC has allowPrivilegedContainer set to true"}, + }, + }, + ExpectInstantiationError: false, + }, + }) +} diff --git a/tests/checks/scc-deny-privileged-container.yml b/tests/checks/scc-deny-privileged-container.yml new file mode 100644 index 000000000..b5069b2d5 --- /dev/null +++ b/tests/checks/scc-deny-privileged-container.yml @@ -0,0 +1,87 @@ +allowHostDirVolumePlugin: true +allowHostIPC: false +allowHostNetwork: false +allowHostPID: false +allowHostPorts: false +allowPrivilegeEscalation: false +allowPrivilegedContainer: true +allowedCapabilities: null +apiVersion: security.openshift.io/v1 +defaultAddCapabilities: null +fsGroup: + type: RunAsAny +groups: [] +kind: SecurityContextConstraints +metadata: + labels: + owner: owner + app: app1 + name: app1 +priority: null +readOnlyRootFilesystem: false +requiredDropCapabilities: +- KILL +- MKNOD +- SETUID +- SETGID +runAsUser: + type: RunAsAny + uid: 1000 +seLinuxContext: + type: MustRunAs +supplementalGroups: + type: RunAsAny +users: +- system:serviceaccount:ns:chart-jenkins-agent +volumes: +- configMap +- downwardAPI +- emptyDir +- hostPath +- persistentVolumeClaim +- projected +- secret +--- +allowHostDirVolumePlugin: true +allowHostIPC: false +allowHostNetwork: false +allowHostPID: false +allowHostPorts: false +allowPrivilegeEscalation: false +allowPrivilegedContainer: false +allowedCapabilities: null +apiVersion: security.openshift.io/v1 +defaultAddCapabilities: null +fsGroup: + type: RunAsAny +groups: [] +kind: SecurityContextConstraints +metadata: + labels: + owner: owner + app: app1 + name: dont-fire +priority: null +readOnlyRootFilesystem: false +requiredDropCapabilities: +- KILL +- MKNOD +- SETUID +- SETGID +runAsUser: + type: RunAsAny + uid: 1000 +seLinuxContext: + type: MustRunAs +supplementalGroups: + type: RunAsAny +users: +- system:serviceaccount:ns:chart-jenkins-agent +volumes: +- configMap +- downwardAPI +- emptyDir +- hostPath +- persistentVolumeClaim +- projected +- secret