diff --git a/README.md b/README.md index f852eac2..757d0080 100644 --- a/README.md +++ b/README.md @@ -54,16 +54,6 @@ Fairwinds includes experimental support for an optional validating webhook. This Unfortunately we have not found a way to disply warnings as part of `kubectl` output unless we are rejecting a deployment altogether. That means that any checks with a severity of `warning` will still pass webhook validation, and the only evidence of that warning will either be in the Fairwinds dashboard or the Fairwinds webhook logs. -## CLI Options - -* `config`: Specify a location for the Fairwinds config -* `dashboard`: Runs the webserver for Fairwinds dashboard. -* `dashboard-port`: Port for the dashboard webserver (default 8080) -* `webhook`: Runs the webhook webserver. -* `webhook-port`: Port for the webhook webserver (default 9876) -* `disable-webhook-config-installer`: disable the installer in the webhook server, so it won't install webhook configuration resources during bootstrapping -* `kubeconfig`: Paths to a kubeconfig. Only required if out-of-cluster. - ## Configuration Fairwinds supports a wide range of validations covering a number of Kubernetes best practices. Here's a sample configuration file that includes all currently supported checks. The [default configuration](https://github.com/reactiveops/fairwinds/blob/master/config.yaml) contains a number of those checks. This repository also includes a sample [full configuration file](https://github.com/reactiveops/fairwinds/blob/master/config-full.yaml) that enables all available checks. @@ -78,5 +68,15 @@ Fairwinds validation checks fall into several different categories: - [Resources](docs/resources.md) - [Security](docs/security.md) +## CLI Options + +* `config`: Specify a location for the Fairwinds config +* `dashboard`: Runs the webserver for Fairwinds dashboard. +* `dashboard-port`: Port for the dashboard webserver (default 8080) +* `webhook`: Runs the webhook webserver. +* `webhook-port`: Port for the webhook webserver (default 9876) +* `disable-webhook-config-installer`: disable the installer in the webhook server, so it won't install webhook configuration resources during bootstrapping +* `kubeconfig`: Paths to a kubeconfig. Only required if out-of-cluster. + ## License Apache License 2.0 diff --git a/config-full.yaml b/config-full.yaml index c68038e5..29da8764 100644 --- a/config-full.yaml +++ b/config-full.yaml @@ -44,12 +44,11 @@ healthChecks: readinessProbeMissing: warning livenessProbeMissing: warning networking: - hostAliasSet: error - hostIPCSet: error hostNetworkSet: error - hostPIDSet: error hostPortSet: error security: + hostIPCSet: error + hostPIDSet: error runAsRootAllowed: warning runAsPrivileged: error notReadOnlyRootFileSystem: warning diff --git a/deploy/all.yaml b/deploy/all.yaml index 0fddbed8..a2e70570 100644 --- a/deploy/all.yaml +++ b/deploy/all.yaml @@ -37,16 +37,15 @@ data: readinessProbeMissing: warning livenessProbeMissing: warning networking: - hostAliasSet: error - hostIPCSet: error - hostNetworkSet: error - hostPIDSet: error - hostPortSet: error + hostNetworkSet: warning + hostPortSet: warning security: - runAsRootAllowed: warning - runAsPrivileged: error + hostIPCSet: error + hostPIDSet: error notReadOnlyRootFileSystem: warning privilegeEscalationAllowed: error + runAsRootAllowed: warning + runAsPrivileged: error capabilities: error: ifAnyAdded: @@ -138,7 +137,7 @@ apiVersion: extensions/v1beta1 kind: Deployment metadata: annotations: - checksum/config: 'af539094a198a41b47151133fbf9c53309b0b621234185a651af15723f2c92ab' + checksum/config: '5702aca235561630172c22b6b900f5cebd4e82fae60389df18a3537ff82e2f09' name: fairwinds-dashboard namespace: fairwinds labels: @@ -200,7 +199,7 @@ apiVersion: extensions/v1beta1 kind: Deployment metadata: annotations: - checksum/config: 'af539094a198a41b47151133fbf9c53309b0b621234185a651af15723f2c92ab' + checksum/config: '5702aca235561630172c22b6b900f5cebd4e82fae60389df18a3537ff82e2f09' name: fairwinds-webhook namespace: fairwinds labels: diff --git a/deploy/helm/fairwinds/values.yaml b/deploy/helm/fairwinds/values.yaml index dba0dadc..260cf843 100644 --- a/deploy/helm/fairwinds/values.yaml +++ b/deploy/helm/fairwinds/values.yaml @@ -11,16 +11,15 @@ config: | readinessProbeMissing: warning livenessProbeMissing: warning networking: - hostAliasSet: error - hostIPCSet: error - hostNetworkSet: error - hostPIDSet: error - hostPortSet: error + hostNetworkSet: warning + hostPortSet: warning security: - runAsRootAllowed: warning - runAsPrivileged: error + hostIPCSet: error + hostPIDSet: error notReadOnlyRootFileSystem: warning privilegeEscalationAllowed: error + runAsRootAllowed: warning + runAsPrivileged: error capabilities: error: ifAnyAdded: diff --git a/pkg/config/config.go b/pkg/config/config.go index 809d869a..dc68eee5 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -80,15 +80,14 @@ type ErrorWarningLists struct { // Networking contains the config for networking validations. type Networking struct { - HostAliasSet Severity `json:"hostAliasSet"` - HostIPCSet Severity `json:"hostIPCSet"` HostNetworkSet Severity `json:"hostNetworkSet"` - HostPIDSet Severity `json:"hostPIDSet"` HostPortSet Severity `json:"hostPortSet"` } // Security contains the config for security validations. type Security struct { + HostIPCSet Severity `json:"hostIPCSet"` + HostPIDSet Severity `json:"hostPIDSet"` RunAsRootAllowed Severity `json:"runAsRootAllowed"` RunAsPrivileged Severity `json:"RunAsPrivileged"` NotReadOnlyRootFileSystem Severity `json:"notReadOnlyRootFileSystem"` diff --git a/pkg/validator/container.go b/pkg/validator/container.go index 42621216..da81e661 100644 --- a/pkg/validator/container.go +++ b/pkg/validator/container.go @@ -152,7 +152,7 @@ func (cv *ContainerValidation) validateNetworking(networkConf *conf.Networking) } if hostPortSet { - cv.addFailure(messages.HostPortFailure, networkConf.HostAliasSet, category) + cv.addFailure(messages.HostPortFailure, networkConf.HostPortSet, category) } else { cv.addSuccess(messages.HostPortSuccess, category) } diff --git a/pkg/validator/container_test.go b/pkg/validator/container_test.go index 29db9c0b..55ea4be2 100644 --- a/pkg/validator/container_test.go +++ b/pkg/validator/container_test.go @@ -340,6 +340,126 @@ func TestValidateImage(t *testing.T) { } } +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{ + Summary: &ResultSummary{}, + }, + } + + badCV := ContainerValidation{ + Container: &corev1.Container{ + Ports: []corev1.ContainerPort{{ + ContainerPort: 3000, + HostPort: 443, + }}, + }, + ResourceValidation: &ResourceValidation{ + Summary: &ResultSummary{}, + }, + } + + goodCV := ContainerValidation{ + Container: &corev1.Container{ + Ports: []corev1.ContainerPort{{ + ContainerPort: 3000, + }}, + }, + ResourceValidation: &ResourceValidation{ + Summary: &ResultSummary{}, + }, + } + + 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 diff --git a/pkg/validator/pod.go b/pkg/validator/pod.go index fdf35919..1f8ae56f 100644 --- a/pkg/validator/pod.go +++ b/pkg/validator/pod.go @@ -38,6 +38,7 @@ func ValidatePod(podConf conf.Configuration, pod *corev1.PodSpec) ResourceResult }, } + pv.validateSecurity(&podConf.Security) pv.validateNetworking(&podConf.Networking) pRes := PodResult{ @@ -70,56 +71,29 @@ func (pv *PodValidation) validateContainers(containers []corev1.Container, pRes } } -func (pv *PodValidation) validateNetworking(networkConf *conf.Networking) { - pv.validateHostAlias(networkConf) - pv.validateHostIPC(networkConf) - pv.validateHostPID(networkConf) - pv.validateHostNetwork(networkConf) -} +func (pv *PodValidation) validateSecurity(securityConf *conf.Security) { + category := messages.CategorySecurity -func (pv *PodValidation) validateHostAlias(networkConf *conf.Networking) { - category := messages.CategoryNetworking - if networkConf.HostAliasSet.IsActionable() { - hostAliasSet := false - for _, alias := range pv.Pod.HostAliases { - if alias.IP != "" && len(alias.Hostnames) == 0 { - hostAliasSet = true - break - } - } - - if hostAliasSet { - pv.addFailure(messages.HostAliasFailure, networkConf.HostAliasSet, category) - } else { - pv.addSuccess(messages.HostAliasSuccess, category) - } - } -} - -func (pv *PodValidation) validateHostIPC(networkConf *conf.Networking) { - category := messages.CategoryNetworking - if networkConf.HostIPCSet.IsActionable() { + if securityConf.HostIPCSet.IsActionable() { if pv.Pod.HostIPC { - pv.addFailure(messages.HostIPCFailure, networkConf.HostIPCSet, category) + pv.addFailure(messages.HostIPCFailure, securityConf.HostIPCSet, category) } else { pv.addSuccess(messages.HostIPCSuccess, category) } } -} -func (pv *PodValidation) validateHostPID(networkConf *conf.Networking) { - category := messages.CategoryNetworking - if networkConf.HostPIDSet.IsActionable() { + if securityConf.HostPIDSet.IsActionable() { if pv.Pod.HostPID { - pv.addFailure(messages.HostPIDFailure, networkConf.HostPIDSet, category) + pv.addFailure(messages.HostPIDFailure, securityConf.HostPIDSet, category) } else { pv.addSuccess(messages.HostPIDSuccess, category) } } } -func (pv *PodValidation) validateHostNetwork(networkConf *conf.Networking) { +func (pv *PodValidation) validateNetworking(networkConf *conf.Networking) { category := messages.CategoryNetworking + if networkConf.HostNetworkSet.IsActionable() { if pv.Pod.HostNetwork { pv.addFailure(messages.HostNetworkFailure, networkConf.HostNetworkSet, category) diff --git a/pkg/validator/pod_test.go b/pkg/validator/pod_test.go index d2b7ac17..2c853d27 100644 --- a/pkg/validator/pod_test.go +++ b/pkg/validator/pod_test.go @@ -24,11 +24,12 @@ import ( func TestValidatePod(t *testing.T) { c := conf.Configuration{ + Security: conf.Security{ + HostIPCSet: conf.SeverityError, + HostPIDSet: conf.SeverityError, + }, Networking: conf.Networking{ - HostAliasSet: conf.SeverityError, - HostIPCSet: conf.SeverityError, HostNetworkSet: conf.SeverityWarning, - HostPIDSet: conf.SeverityError, HostPortSet: conf.SeverityError, }, } @@ -38,15 +39,14 @@ func TestValidatePod(t *testing.T) { pod := test.MockPod() expectedSum := ResultSummary{ - Successes: uint(9), + Successes: uint(8), Warnings: uint(0), Errors: uint(0), } expectedMessages := []*ResultMessage{ - {Message: "Host alias is not configured", Type: "success", Category: "Networking"}, - {Message: "Host IPC is not configured", Type: "success", Category: "Networking"}, - {Message: "Host PID is not configured", Type: "success", Category: "Networking"}, + {Message: "Host IPC is not configured", Type: "success", Category: "Security"}, + {Message: "Host PID is not configured", Type: "success", Category: "Security"}, {Message: "Host network is not configured", Type: "success", Category: "Networking"}, }