Compare commits

...
33 Commits
Author SHA1 Message Date
Robert Brennan 91378c3d92 Bump version to 0.6 (#231)
* bump version to 0.6.0

* update changelog
2019-12-09 12:20:15 -05:00
Robert Brennan 207297c11d move to using fairwinds quay repo (#230) 2019-12-09 08:51:17 -05:00
Robert Brennan 0d86096f09 remove extensionsv1beta1 reference to support 1.16 (#229) 2019-12-06 11:03:38 -05:00
Robert Brennan 67ab987f7e Add support for annotation-based exemptions (#227)
* add controllers_to_scan to example config-full

* add support for annotation-based exemptions

* fix lint errors

* add docs
2019-12-06 08:29:30 -05:00
Robert Brennan 97457d71c0 Full support for validating webhook, now that tests are in place (#226) 2019-12-04 14:12:13 -05:00
Andrew Suderman 1159a380ba Adding an exception for flannel being privileged to example config (#225) 2019-11-22 12:02:35 -07:00
Robert Brennan ca6aa76729 Add default exemptions (#220)
* Update config.yaml

* Update config.yaml

* add a couple more exemptions
2019-11-15 14:45:58 -05:00
Robert Brennan 7ce71f1632 bump to version 0.5.2 (#224) 2019-11-13 15:27:24 -05:00
Robert Brennan 98b47e0aeb Fix resource success messages (#223)
* add success messages when resources are set

* add tests
2019-11-13 14:07:32 -05:00
Robert Brennan 159c165468 bump version (#221) 2019-11-12 11:40:54 -05:00
Robert Brennan 4eeabb2c7f pass RunAsNonRoot if RunAsUser > 0 (#219) 2019-11-11 13:21:32 -05:00
Barnabas Makonda 7e35b03452 Merge pull request #218 from FairwindsOps/kind-exceptions
Checks exemption for kubehunter, polaris and goldilocks
2019-11-08 23:35:42 +02:00
MAKOSCAFEE c8e4f766a2 revert version upgrade for dashboard and webhook 2019-11-07 23:03:29 +02:00
MAKOSCAFEE 0ee39a4470 revert version upgrade 2019-11-07 23:01:47 +02:00
MAKOSCAFEE fc7c913122 update exemption rules and check controller name prefix 2019-11-07 19:16:06 +02:00
MAKOSCAFEE 3e15586597 add kind exception configurations 2019-11-07 15:37:13 +02:00
Robert Brennan 3d07ce8004 update to 0.5.0 (#205)
* update to 0.5.0

* update changelog

* incorporate changes to polaris chart
2019-11-06 13:57:18 -05:00
Robert Brennan 22ab851681 skip health checks for jobs, cronjobs, and initContainers (#216) 2019-11-06 13:31:17 -05:00
Robert BrennanandAndrew Suderman 152c876067 Update CODEOWNERS
Co-Authored-By: Andrew Suderman <andrew@sudermanjr.com>
2019-11-05 09:42:23 -07:00
Robert Brennan c10b2e0e55 Update CODEOWNERS 2019-11-05 09:42:23 -07:00
Robert Brennan 2b15f11d57 Add exemptions to config (#204)
* first pass at adding exemptions

* Update config.yaml

* make config_test more reliable

* add flag to disallow exemptions in dashboard

* add disallow-exemptions flag to CLI

* add comments

* fix exemptions flag

* fix alert on dashboard

* minor style changes
2019-10-23 17:14:03 -04:00
Robert Brennan b172f61c5f Remove Kim as codeowner (#214) 2019-10-16 08:39:45 -04:00
Adam Shannon 150b812371 pkg/dashboard: setup basePath as a path prefix in routing (#201) 2019-10-02 12:17:59 -04:00
Robert Brennan 434b1f604f Create capabilitiesAdded and capabilitiesDropped IDs (#207)
* ensure check IDs are unique

* create capabilitiesAdded and capabilitiesDropped check IDs
2019-10-02 08:51:47 -04:00
Robert Brennan 6e3ab067d7 skip kubernetes tests for forked PRs (#202) 2019-09-18 10:12:36 -04:00
Robert Brennan 2e44d151fe fix empty dashboard when load-audit-file not specified (#203) 2019-09-18 09:39:43 -04:00
Robert Brennan c91a85a08a add IDs to each check (#197) 2019-09-11 14:07:08 -04:00
Robert Brennan f8bb171209 audit deploy configs in CI/CD (#198) 2019-09-09 13:10:19 -04:00
Bobby Brennan f860c34d62 upgrade alpine and golang images (#193) 2019-08-22 16:08:13 -04:00
Will Ledingham b8422a93b7 Wl/stored audits (#188)
* added ability to run dashboard from a stored audit result file.

* added ability to run dashboard from a stored audit result file.

* more changes

* debugging

* de

* all working as anticipated locally

* .

* updated way of decoding YAML/JSON files.

* remmoved unneded code

* renamed fn names, moved logic around and cleaned up main.go

* deleted output files from weird places.

* deleted test file
2019-08-14 17:20:19 -04:00
Bobby Brennan 15b14555a6 sleep to ensure dashboard/webhook are ready (#189) 2019-08-14 09:17:42 -04:00
Will Ledingham 467ab945cd Wl/webhook test (#182)
* Added testing for webhook using KIND

* fixed branching error

* added tests to CircleCI

* added tests to CircleCI

* attempt to stop failing circleci test

* moved location of testing for webhook in CircleCI

* debugging

* more debugging

* .

* .

* added download of webhook to test

* ..

* ...

* corrected sleep comand

* .

* .

* code working now, clean up

* testing smaller sleep times

* increased sleep time

* responded to comments on github

* .

* debug

* more edits

* debugging second test failing.

* debugging

* tests doing opposite of what they should be debugging.

* debugging

* .

* .

* fixing error in installation of webhook.

* .

* timeout increase

* trying to install webhook

* .

* .

* webhook still not i installing properly

* ..

* ..

* add log message

* ..

* changed order of test_k8s, removed set -e

* ..

* namespace polaris

* .

* .......

* intial testing for new strategy.

* intial testing for new strategy.

* .

* ...

* final edits, working now

* fixed files, cleaned up logs, added more detail to webhook starting documentation.

* ?

* added test files for other controller types, adding testing for them in webhook_test.sh

* increased sleep time

* testing

* finally added tests for jobs

* changed while loop condition to include webhook.

* .

* lskdfsjkl

* sd

* lskfjlskj

* .

* final

* added timeout test for dashboard to try to aleviate error

* .

* .

* install the dashboard

* ...

* initial test for new kube_dashboard_test

* initial test for new kube_dashboard_test

* ?

* deleting unused code

* final change for dashboard test

* final
2019-08-09 16:00:55 -04:00
Bobby Brennan dddeeb3332 Update docs (#186) 2019-08-05 08:51:23 -04:00
56 changed files with 2109 additions and 467 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
DOCKERFILE='Dockerfile'
EXTERNAL_REGISTRY_BASE_DOMAIN=quay.io
REPOSITORY_NAME=reactiveops/polaris
REPOSITORY_NAME=fairwinds/polaris
DOCKERTAG=${EXTERNAL_REGISTRY_BASE_DOMAIN}/${REPOSITORY_NAME}
if [[ -n $CI_TAG ]]; then
ADDITIONAL_DOCKER_TAG_VERSIONS=()
+14 -18
View File
@@ -19,6 +19,7 @@ references:
run:
name: Install K8s
command: |
sudo apt-get update
echo "Installing git and jq"
sudo apt-get install -yqq jq git
@@ -49,7 +50,6 @@ references:
name: Update Coverage
command: |
if [[ -z $CIRCLE_PR_NUMBER ]]; then
go test ./pkg/... -coverprofile=coverage.txt -covermode=count
bash <(curl -s https://codecov.io/bash)
else
echo "Skipping coverage for forked PR"
@@ -59,29 +59,23 @@ references:
name: Test Dashboard
command: |
go run main.go --dashboard --dashboard-port 3000 --audit-path ./examples &
sleep 5
sleep 30
curl -f http://localhost:3000 > /dev/null
curl -f http://localhost:3000/health > /dev/null
curl -f http://localhost:3000/favicon.ico > /dev/null
curl -f http://localhost:3000/static/css/main.css > /dev/null
curl -f http://localhost:3000/results.json > /dev/null
curl -f http://localhost:3000/details/security > /dev/null
test_kube_dashboard: &test_kube_dashboard
test_k8s: &test_k8s
run:
name: Test Dashboard
name: Test Kubernetes Deployments
command: |
sed -ri "s|'(quay.io/reactiveops/polaris:).+'|'\1${CIRCLE_SHA1}'|" ./deploy/dashboard.yaml
kubectl apply -f ./deploy/dashboard.yaml
sleep 10
kubectl get pods --namespace polaris
kubectl port-forward --namespace polaris svc/polaris-dashboard 3000:80 &
sleep 5
curl -f http://localhost:3000 > /dev/null
curl -f http://localhost:3000/health > /dev/null
curl -f http://localhost:3000/favicon.ico > /dev/null
curl -f http://localhost:3000/static/css/main.css > /dev/null
curl -f http://localhost:3000/results.json > /dev/null
curl -f http://localhost:3000/details/security > /dev/null
if [[ -z $CIRCLE_PR_NUMBER ]]; then
./test/webhook_test.sh
./test/kube_dashboard_test.sh
else
echo "Skipping Kubernetes tests for forked PR"
fi
# Release scripts
install_goreleaser: &install_goreleaser
@@ -99,7 +93,7 @@ references:
docker-pull -f .circleci/build.config
docker-build -f .circleci/build.config
if [[ -z $CIRCLE_PR_NUMBER ]]; then
docker login quay.io -u="reactiveops+circleci" -p="${quay_token}"
docker login quay.io -u="${fairwinds_quay_user}" -p="${fairwinds_quay_token}"
docker-push -f .circleci/build.config
else
echo "Skipping docker push for forked PR"
@@ -130,7 +124,7 @@ jobs:
steps:
- checkout
- *install_k8s
- *test_kube_dashboard
- *test_k8s
test:
working_directory: /go/src/github.com/fairwindsops/polaris/
@@ -141,6 +135,8 @@ jobs:
- run: go get -u golang.org/x/lint/golint
- run: go list ./... | grep -v vendor | xargs golint -set_exit_status
- run: go list ./... | grep -v vendor | xargs go vet
- run: go test ./pkg/... -coverprofile=coverage.txt -covermode=count
- run: go run main.go --audit-path ./deploy --set-exit-code-below-score 100 --set-exit-code-on-error
- *update_coverage
- *test_binary_dashboard
+20
View File
@@ -1,5 +1,25 @@
# x.x.x (next release)
# 0.6.0
* Fixed webhook support in Kubernetes 1.16
* this also removes support for 1.8
* Added support for exemptions via controller annotations
# 0.5.2
* Fixed missing success messages for resource requests/limits
# 0.5.1
* Added a few more exemptions
* Started checking exemptions based on controller name prefix
* `runAsUser != 0` now passes the `runAsNonRoot` check
# 0.5.0
* Added `--load-audit-file` flag to run the dashboard from an existing audit
* Added an `ID` field to each check in the output
* Skip health checks for jobs, cronjobs, initcontainers
* Added support for exemptions
* Fixed dashboard base path option
# 0.4.0
* Added additional Pod Controllers to scan PodSpec (`jobs`, `cronjobs`, `daemonsets`, `replicationcontrollers`)
+1 -1
View File
@@ -1 +1 @@
* @bobby-brennan @endzyme @kimschles @robscott
* @rbren @makoscafee @jordandoig
+2 -2
View File
@@ -1,11 +1,11 @@
FROM golang:1.12.4 AS build-env
FROM golang:1.12.9 AS build-env
WORKDIR /go/src/github.com/fairwindsops/polaris/
COPY . .
RUN go get -u github.com/gobuffalo/packr/v2/packr2
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 packr2 build -a -o polaris *.go
FROM alpine:3.9
FROM alpine:3.10
WORKDIR /usr/local/bin
RUN apk --no-cache add ca-certificates
+25 -13
View File
@@ -5,7 +5,7 @@
[![Version][version-image]][version-link] [![CircleCI][circleci-image]][circleci-link] [![Go Report Card][goreport-image]][goreport-link]
</div>
[version-image]: https://img.shields.io/static/v1.svg?label=Version&message=0.4.0&color=239922
[version-image]: https://img.shields.io/static/v1.svg?label=Version&message=0.6.0&color=239922
[version-link]: https://github.com/FairwindsOps/polaris
[goreport-image]: https://goreportcard.com/badge/github.com/FairwindsOps/polaris
@@ -14,19 +14,20 @@
[circleci-image]: https://circleci.com/gh/FairwindsOps/polaris.svg?style=svg
[circleci-link]: https://circleci.com/gh/FairwindsOps/polaris.svg
Polaris helps keep your cluster healthy. It runs a variety of checks to ensure that
Kubernetes deployments are configured using best practices, helping you avoid
Fairwinds' Polaris keeps your clusters sailing smoothly. It runs a variety of checks to ensure that
Kubernetes pods and controllers are configured using best practices, helping you avoid
problems in the future. Polaris can be run in a few different modes:
- A dashboard that provides an overview of how well current deployments are configured within a cluster.
- An experimental validating webhook that can prevent any future deployments that do not live up to a configured standard.
- A command-line audit that can be incorporated into your CI/CD pipeline
Polaris can be run in three different modes:
* As a [dashboard](#dashboard), so you can audit what's running inside your cluster.
* As a [validating webhook](#webhook), so you can automatically reject workloads that don't adhere to your organization's policies.
* As a [command-line tool](#cli), so you can test local YAML files, e.g. as part of a CI/CD process.
**Want to learn more?** Fairwinds holds [office hours on Zoom](https://zoom.us/j/242508205) the first Friday of every month, at 12pm Eastern. You can also reach out via email at `opensource@fairwinds.com`
# Dashboard Quickstart
```
```bash
kubectl apply -f https://github.com/FairwindsOps/polaris/releases/latest/download/dashboard.yaml
kubectl port-forward --namespace polaris svc/polaris-dashboard 8080:80
```
@@ -35,9 +36,11 @@ With the port forwarding in place, you can open http://localhost:8080 in your br
* * *
# Components
## Dashboard
The Polaris dashboard is a way to get a simple visual overview of the current state of your Kubernetes deployments as well as a roadmap for what can be improved. The dashboard provides a cluster wide overview as well as breaking out results by category, namespace, and deployment.
## Dashboard
> [View installation instructions](docs/usage.md#dashboard)
The Polaris dashboard is a way to get a simple visual overview of the current state of your Kubernetes workloads as well as a roadmap for what can be improved. The dashboard provides a cluster wide overview as well as breaking out results by category, namespace, and workload.
<p align="center">
<img src="/dashboard-screenshot.png" alt="Polaris Dashboard" />
@@ -47,13 +50,22 @@ Our default standards in Polaris are rather high, so dont be surprised if you
## Webhook
> [View installation instructions](docs/usage.md#webhook)
Polaris includes experimental support for an optional validating webhook. This accepts the same configuration as the dashboard, and can run the same validations. This webhook will reject any deployments that trigger a validation error. This is indicative of the greater goal of Polaris, not just to encourage better configuration through dashboard visibility, but to actually enforce it with this webhook. *Although we are working towards greater stability and better test coverage, we do not currently consider this webhook component production ready.*
Polaris includes an optional validating webhook. This accepts the same configuration as the dashboard, and can run the same validations. This webhook will reject any workloads that trigger a validation error. This is indicative of the greater goal of Polaris, not just to encourage better configuration through dashboard visibility, but to actually enforce it with this webhook.
Unfortunately we have not found a way to display 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 Polaris dashboard or the Polaris webhook logs.
Unfortunately we have not found a way to display warnings as part of `kubectl` output unless we are rejecting a workload 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 Polaris dashboard or the Polaris webhook logs.
# Usage Documentation
See the [Usage Guide](/docs/usage.md) in the docs folder.
## CLI
> [View installation instructions](docs/usage.md#cli)
Polaris can also be used on the command line, either to audit local files or a running cluster.
This is particularly helpful for running Polaris against your infrastructure-as-code as part of a
CI/CD pipeline. Use the available [command line flags](docs/usage.md#running-with-ci-cd)
to cause CI/CD to fail if your Polaris score drops below a certain threshold, or if any errors arise.
# Installation and Usage
See the [Usage Guide](/docs/usage.md) for details on different methods for installing and using Polaris.
# Contributing
PRs welcome! Check out the [Contributing Guidelines](CONTRIBUTING.md),
+6 -5
View File
@@ -134,6 +134,7 @@ metadata:
namespace: polaris
labels:
app: polaris
annotations:
spec:
ports:
- name: dashboard
@@ -149,8 +150,6 @@ spec:
apiVersion: apps/v1
kind: Deployment
metadata:
annotations:
checksum/config: '8aa5a565fba7a2db98d46752087de8c1dcc83b70cd762c5829d5ba01270d54a2'
name: polaris-dashboard
namespace: polaris
labels:
@@ -164,6 +163,8 @@ spec:
component: dashboard
template:
metadata:
annotations:
checksum/config: '8aa5a565fba7a2db98d46752087de8c1dcc83b70cd762c5829d5ba01270d54a2'
labels:
app: polaris
component: dashboard
@@ -178,7 +179,7 @@ spec:
- --dashboard
- --config
- /opt/app/config.yaml
image: 'quay.io/reactiveops/polaris:0.4'
image: 'quay.io/fairwinds/polaris:0.6'
imagePullPolicy: 'Always'
name: dashboard
ports:
@@ -216,8 +217,8 @@ spec:
subPath: config.yaml
readOnly: true
serviceAccountName: polaris-dashboard
nodeSelector: {}
tolerations: []
nodeSelector:
tolerations:
---
# Source: polaris/templates/audit.job.yaml
+6 -5
View File
@@ -6,6 +6,7 @@ metadata:
name: polaris
---
# Source: polaris/templates/webhook.secret.yaml
# The name of this secret is static as it is populated by the webhook pod.
apiVersion: v1
kind: Secret
metadata:
@@ -212,8 +213,6 @@ spec:
apiVersion: apps/v1
kind: Deployment
metadata:
annotations:
checksum/config: '8aa5a565fba7a2db98d46752087de8c1dcc83b70cd762c5829d5ba01270d54a2'
name: polaris-webhook
namespace: polaris
labels:
@@ -227,6 +226,8 @@ spec:
component: webhook
template:
metadata:
annotations:
checksum/config: '8aa5a565fba7a2db98d46752087de8c1dcc83b70cd762c5829d5ba01270d54a2'
labels:
app: polaris
component: webhook
@@ -238,7 +239,7 @@ spec:
- --webhook
- --config
- /opt/app/config.yaml
image: 'quay.io/reactiveops/polaris:0.4'
image: 'quay.io/fairwinds/polaris:0.6'
imagePullPolicy: 'Always'
ports:
- containerPort: 9876
@@ -288,8 +289,8 @@ spec:
mountPath: /tmp/
readOnly: false
serviceAccountName: polaris-webhook
nodeSelector: {}
tolerations: []
nodeSelector:
tolerations:
volumes:
- name: config
configMap:
+99 -78
View File
@@ -4,9 +4,12 @@ be run as a local binary, which will use your kubeconfig to connect to the clust
or run against local YAML files.
## Configuration
Polaris supports a wide range of validations covering a number of Kubernetes best practices.
Here's a [sample configuration file](/examples/config-full.yaml) that includes all currently supported checks.
The [default configuration](/examples/config.yaml) contains a number of those checks.
Polaris 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/fairwindsops/polaris/blob/master/examples/config.yaml) contains a number of those checks. This repository also includes a sample [full configuration file](https://github.com/fairwindsops/polaris/blob/master/examples/config-full.yaml) that enables all available checks.
### Checks
Each check can be assigned a `severity`. Only checks with a severity of `error` or `warning` will be validated. The results of these validations are visible on the dashboard. In the case of the validating webhook, only failures with a severity of `error` will result in a change being rejected.
Polaris validation checks fall into several different categories:
@@ -17,7 +20,101 @@ Polaris validation checks fall into several different categories:
- [Resources](check-documentation/resources.md)
- [Security](check-documentation/security.md)
## CLI Options
### Exemptions
Exemptions can be added two ways: by annotating a controller, or editing the Polaris config.
To exempt a controller via annotations, use the annotation `polaris.fairwinds.com/exempt=true`, e.g.
```
kubectl annotate deployment my-deployment polaris.fairwinds.com/exempt=true
```
To exempt a controller via the config, you have to specify a list of controller names and a list of rules, e.g.
```yaml
exemptions:
- controllerNames:
- dns-controller
rules:
- hostNetworkSet
```
# Installing
There are several ways to install and use Polaris. Below outline ways to install using `kubectl`, `helm` and `local binary`.
## Dashboard
The dashboard can be installed on a cluster using kubectl or Helm. It can also be run locally,
connecting to your cluster using the credentials stored in your `KUBECONFIG`.
### kubectl
```bash
kubectl apply -f https://github.com/fairwindsops/polaris/releases/latest/download/dashboard.yaml
kubectl port-forward --namespace polaris svc/polaris-dashboard 8080:80
```
### Helm
```bash
helm repo add reactiveops-stable https://charts.reactiveops.com/stable
helm upgrade --install polaris reactiveops-stable/polaris --namespace polaris
kubectl port-forward --namespace polaris svc/polaris-dashboard 8080:80
```
### Local Binary
You'll need a valid `KUBECONFIG` set up for the dashboard to connect to your cluster.
Binary releases can be dowloaded from the [releases page](https://github.com/fairwindsops/polaris/releases)
or can be installed with [Homebrew](https://brew.sh/):
```bash
brew tap reactiveops/tap
brew install reactiveops/tap/polaris
polaris --dashboard --dashboard-port 8080
```
## Webhook
### kubectl
```bash
kubectl apply -f https://github.com/fairwindsops/polaris/releases/latest/download/webhook.yaml
```
### Helm
```bash
helm repo add reactiveops-stable https://charts.reactiveops.com/stable
helm upgrade --install polaris reactiveops-stable/polaris --namespace polaris \
--set webhook.enable=true --set dashboard.enable=false
```
## CLI
### Installation
Binary releases can be downloaded from the [releases page](https://github.com/fairwindsops/polaris/releases)
or can be installed with [Homebrew](https://brew.sh/):
```bash
brew tap reactiveops/tap
brew install reactiveops/tap/polaris
polaris --version
```
You can run audits on the command line and see the output as JSON, YAML, or a raw score:
```bash
polaris --audit --output-format yaml > report.yaml
polaris --audit --output-format score
# 92
```
Both the dashboard and audits can run against a local directory or YAML file
rather than a cluster:
```bash
polaris --audit --audit-path ./deploy/
```
#### Running with CI/CD
You can integrate Polaris into CI/CD for repositories containing infrastructure-as-code.
For example, to fail if polaris detects *any* error-level issues, or if the score drops below 90%:
```bash
polaris --audit --audit-path ./deploy/ \
--set-exit-code-on-error \
--set-exit-code-below-score 90
```
For more on exit code meanings, see [exit-code docs](exit-codes.md).
#### CLI Options
```
# high-level flags
@@ -67,79 +164,3 @@ Polaris validation checks fall into several different categories:
disable the installer in the webhook server, so it won't install webhook configuration resources during bootstrapping
```
# Installing
There are several ways to install and use Polaris. Below outline ways to install using `kubectl`, `helm` and `local binary`.
## kubectl
### Dashboard
```
kubectl apply -f https://github.com/fairwindsops/polaris/releases/latest/download/dashboard.yaml
kubectl port-forward --namespace polaris svc/polaris-dashboard 8080:80
```
### Webhook
```
kubectl apply -f https://github.com/fairwindsops/polaris/releases/latest/download/webhook.yaml
```
## Helm
Start by adding the ReactiveOps Helm repo:
```
helm repo add reactiveops-stable https://charts.reactiveops.com/stable
```
### Dashboard
```
helm upgrade --install polaris reactiveops-stable/polaris --namespace polaris
kubectl port-forward --namespace polaris svc/polaris-dashboard 8080:80
```
### Webhook
```
helm upgrade --install polaris reactiveops-stable/polaris --namespace polaris \
--set webhook.enable=true --set dashboard.enable=false
```
## Local Binary
### Installation
Binary releases are available on the [releases page](https://github.com/fairwindsops/polaris/releases) or can be installed with [Homebrew](https://brew.sh/):
```
brew tap reactiveops/tap
brew install reactiveops/tap/polaris
polaris --version
```
You can run `polaris --help` to see a full list of options.
### Dashboard
The dashboard can be run on your local machine, without installing anything on the cluster.
Polaris will use your local kubeconfig to connect to the cluster.
```
polaris --dashboard --dashboard-port 8080
```
### Audits
You can also run audits on the command line and see the output as JSON, YAML, or a raw score:
```
polaris --audit --output-format yaml > report.yaml
polaris --audit --output-format score
# 92
```
Both the dashboard and audits can run against a local directory or YAML file
rather than a cluster:
```
polaris --audit --audit-path ./deploy/
```
### Running with CI/CD
You can integrate Polaris into CI/CD for repositories containing infrastructure-as-code.
For example, to fail if polaris detects *any* error-level issues, or if the score drops below 90%:
```bash
polaris --audit --audit-path ./deploy/ \
--set-exit-code-on-error \
--set-exit-code-below-score 90
```
For more on exit code meanings, see [exit-code docs](exit-codes.md).
+77
View File
@@ -63,3 +63,80 @@ security:
warning:
ifAnyAddedBeyond:
- NONE
controllers_to_scan:
- Deployments
- StatefulSets
- DaemonSets
- CronJobs
- Jobs
- ReplicationControllers
exemptions:
- controllerNames:
- dns-controller
- datadog-datadog
- kube-flannel-ds
- kube2iam
- aws-iam-authenticator
- datadog
- kube2iam
rules:
- hostNetworkSet
- controllerNames:
- aws-iam-authenticator
- aws-cluster-autoscaler
- kube-state-metrics
- dns-controller
- external-dns
- dnsmasq
- autoscaler
- kubernetes-dashboard
- install-cni
- kube2iam
rules:
- readinessProbeMissing
- livenessProbeMissing
- controllerNames:
- aws-iam-authenticator
- nginx-ingress-controller
- nginx-ingress-default-backend
- aws-cluster-autoscaler
- kube-state-metrics
- dns-controller
- external-dns
- kubedns
- dnsmasq
- autoscaler
- tiller
- kube2iam
rules:
- runAsRootAllowed
- controllerNames:
- aws-iam-authenticator
- nginx-ingress-controller
- nginx-ingress-default-backend
- aws-cluster-autoscaler
- kube-state-metrics
- dns-controller
- external-dns
- kubedns
- dnsmasq
- autoscaler
- tiller
- kube2iam
rules:
- notReadOnlyRootFileSystem
- controllerNames:
- cert-manager
- dns-controller
- kubedns
- dnsmasq
- autoscaler
rules:
- cpuRequestsMissing
- cpuLimitsMissing
- memoryRequestsMissing
- memoryLimitsMissing
- controllerNames:
- kube2iam
rules:
- runAsPrivileged
+93
View File
@@ -48,3 +48,96 @@ controllers_to_scan:
- CronJobs
- Jobs
- ReplicationControllers
exemptions:
- controllerNames:
- dns-controller
- datadog-datadog
- kube-flannel-ds
- kube2iam
- aws-iam-authenticator
- datadog
- kube2iam
rules:
- hostNetworkSet
- controllerNames:
- aws-iam-authenticator
- aws-cluster-autoscaler
- kube-state-metrics
- dns-controller
- external-dns
- dnsmasq
- autoscaler
- kubernetes-dashboard
- install-cni
- kube2iam
rules:
- readinessProbeMissing
- livenessProbeMissing
- controllerNames:
- aws-iam-authenticator
- nginx-ingress-controller
- nginx-ingress-default-backend
- aws-cluster-autoscaler
- kube-state-metrics
- dns-controller
- external-dns
- kubedns
- dnsmasq
- autoscaler
- tiller
- kube2iam
rules:
- runAsRootAllowed
- controllerNames:
- aws-iam-authenticator
- nginx-ingress-controller
- nginx-ingress-default-backend
- aws-cluster-autoscaler
- kube-state-metrics
- dns-controller
- external-dns
- kubedns
- dnsmasq
- autoscaler
- tiller
- kube2iam
rules:
- notReadOnlyRootFileSystem
- controllerNames:
- cert-manager
- dns-controller
- kubedns
- dnsmasq
- autoscaler
- insights-agent-goldilocks-vpa-install
rules:
- cpuRequestsMissing
- cpuLimitsMissing
- memoryRequestsMissing
- memoryLimitsMissing
- controllerNames:
- kube2iam
- kube-flannel-ds
rules:
- runAsPrivileged
- controllerNames:
- kube-hunter
rules:
- hostPIDSet
- controllerNames:
- polaris
- kube-hunter
- goldilocks
- insights-agent-goldilocks-vpa-install
rules:
- notReadOnlyRootFileSystem
- controllerNames:
- insights-agent-goldilocks-controller
rules:
- livenessProbeMissing
- readinessProbeMissing
- controllerNames:
- insights-agent-goldilocks-vpa-install
- kube-hunter
rules:
- runAsRootAllowed
+15 -11
View File
@@ -41,7 +41,7 @@ import (
const (
// Version represents the current release version of Polaris
Version = "0.4.0"
Version = "0.6.0"
)
func main() {
@@ -59,8 +59,10 @@ func main() {
auditOutputURL := flag.String("output-url", "", "Destination URL to send audit results")
auditOutputFile := flag.String("output-file", "", "Destination file for audit results")
auditOutputFormat := flag.String("output-format", "json", "Output format for results - json, yaml, or score")
loadAuditFile := flag.String("load-audit-file", "", "Runs the dashboard with data saved from a past audit.")
displayName := flag.String("display-name", "", "An optional identifier for the audit")
configPath := flag.String("config", "", "Location of Polaris configuration file")
disallowExemptions := flag.Bool("disallow-exemptions", false, "Location of Polaris configuration file")
logLevel := flag.String("log-level", logrus.InfoLevel.String(), "Logrus log level")
version := flag.Bool("version", false, "Prints the version of Polaris")
disableWebhookConfigInstaller := flag.Bool("disable-webhook-config-installer", false,
@@ -68,13 +70,11 @@ func main() {
flag.Parse()
// if version is specified anywhere, print and exit
if *version {
fmt.Printf("Polaris version %s\n", Version)
os.Exit(0)
}
// Set logging level
parsedLevel, err := logrus.ParseLevel(*logLevel)
if err != nil {
logrus.Errorf("log-level flag has invalid value %s", *logLevel)
@@ -82,32 +82,31 @@ func main() {
logrus.SetLevel(parsedLevel)
}
// Parse the config file
c, err := conf.ParseFile(*configPath)
if err != nil {
logrus.Errorf("Error parsing config at %s: %v", *configPath, err)
os.Exit(1)
}
// Override display name on reports if defined in CLI flags
if *displayName != "" {
c.DisplayName = *displayName
}
// default to run as audit if no "run-mode" is defined
if *disallowExemptions {
c.DisallowExemptions = true
}
if !*dashboard && !*webhook && !*audit {
*audit = true
}
// perform the action for the desired "run-mode"
if *webhook {
startWebhookServer(c, *disableWebhookConfigInstaller, *webhookPort)
} else if *dashboard {
startDashboardServer(c, *auditPath, *dashboardPort, *dashboardBasePath)
startDashboardServer(c, *auditPath, *loadAuditFile, *dashboardPort, *dashboardBasePath)
} else if *audit {
auditData := runAndReportAudit(c, *auditPath, *auditOutputFile, *auditOutputURL, *auditOutputFormat)
// exit code 3 if any errors in the audit else if score is under desired minimum, exit 4
if *setExitCode && auditData.ClusterSummary.Results.Totals.Errors > 0 {
logrus.Infof("%d errors found in audit", auditData.ClusterSummary.Results.Totals.Errors)
os.Exit(3)
@@ -118,8 +117,13 @@ func main() {
}
}
func startDashboardServer(c conf.Configuration, auditPath string, port int, basePath string) {
router := dashboard.GetRouter(c, auditPath, port, basePath)
func startDashboardServer(c conf.Configuration, auditPath string, loadAuditFile string, port int, basePath string) {
var auditDataPtr *validator.AuditData
if loadAuditFile != "" {
auditData := validator.ReadAuditFromFile(loadAuditFile)
auditDataPtr = &auditData
}
router := dashboard.GetRouter(c, auditPath, port, basePath, auditDataPtr)
router.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("OK"))
})
+17 -9
View File
@@ -30,13 +30,21 @@ import (
// Configuration contains all of the config for the validation checks.
type Configuration struct {
DisplayName string `json:"displayName"`
Resources Resources `json:"resources"`
HealthChecks HealthChecks `json:"healthChecks"`
Images Images `json:"images"`
Networking Networking `json:"networking"`
Security Security `json:"security"`
ControllersToScan []SupportedController `json:"controllers_to_scan"`
DisplayName string `json:"displayName"`
Resources Resources `json:"resources"`
HealthChecks HealthChecks `json:"healthChecks"`
Images Images `json:"images"`
Networking Networking `json:"networking"`
Security Security `json:"security"`
ControllersToScan []SupportedController `json:"controllers_to_scan"`
Exemptions []Exemption `json:"exemptions"`
DisallowExemptions bool `json:"disallowExemptions"`
}
// Exemption represents an exemption to normal rules
type Exemption struct {
Rules []string `json:"rules"`
ControllerNames []string `json:"controllerNames"`
}
// Resources contains config for resource requests and limits.
@@ -94,7 +102,7 @@ type Security struct {
HostIPCSet Severity `json:"hostIPCSet"`
HostPIDSet Severity `json:"hostPIDSet"`
RunAsRootAllowed Severity `json:"runAsRootAllowed"`
RunAsPrivileged Severity `json:"RunAsPrivileged"`
RunAsPrivileged Severity `json:"runAsPrivileged"`
NotReadOnlyRootFileSystem Severity `json:"notReadOnlyRootFileSystem"`
PrivilegeEscalationAllowed Severity `json:"privilegeEscalationAllowed"`
Capabilities SecurityCapabilities `json:"capabilities"`
@@ -117,8 +125,8 @@ type SecurityCapabilityLists struct {
func ParseFile(path string) (Configuration, error) {
var rawBytes []byte
var err error
configBox := packr.New("Config", "../../examples")
if path == "" {
configBox := packr.New("Config", "../../examples")
rawBytes, err = configBox.Find("config.yaml")
} else if strings.HasPrefix(path, "https://") || strings.HasPrefix(path, "http://") {
//path is a url
+2
View File
@@ -21,6 +21,7 @@ import (
"net/http"
"regexp"
"testing"
"time"
"github.com/stretchr/testify/assert"
"k8s.io/apimachinery/pkg/api/resource"
@@ -146,6 +147,7 @@ func TestConfigFromURL(t *testing.T) {
log.Fatalf("ListenAndServe(): %s", err)
}
}()
time.Sleep(time.Second)
parsedConf, err = ParseFile("http://localhost:8081/exampleURL")
assert.NoError(t, err, "Expected no error when parsing YAML from URL")
+38
View File
@@ -0,0 +1,38 @@
package config
import (
"reflect"
"strings"
)
// IsActionable determines whether a check is actionable given the current configuration
func (conf *Configuration) IsActionable(subConf interface{}, ruleName, controllerName string) bool {
ruleID := GetIDFromField(subConf, ruleName)
subConfRef := reflect.ValueOf(subConf)
fieldVal := reflect.Indirect(subConfRef).FieldByName(ruleName).Interface()
if severity, ok := fieldVal.(Severity); ok && !severity.IsActionable() {
return false
}
if ranges, ok := fieldVal.(ResourceRanges); ok {
if ranges.Warning.Above == nil && ranges.Warning.Below == nil &&
ranges.Error.Above == nil && ranges.Error.Below == nil {
return false
}
}
if conf.DisallowExemptions {
return true
}
for _, example := range conf.Exemptions {
for _, rule := range example.Rules {
if rule != ruleID {
continue
}
for _, controller := range example.ControllerNames {
if strings.HasPrefix(controllerName, controller) {
return false
}
}
}
}
return true
}
+19
View File
@@ -0,0 +1,19 @@
package config
import (
"reflect"
)
// GetIDFromField returns the JSON key associated with a particular field, which serves as the check ID.
func GetIDFromField(config interface{}, name string) string {
t := reflect.TypeOf(config)
field, ok := t.FieldByName(name)
if !ok {
panic("No JSON annotation for field " + name)
}
id, ok := field.Tag.Lookup("json")
if !ok {
panic("No JSON tag for field " + name)
}
return id
}
-2
View File
@@ -10,7 +10,6 @@ import (
batchv1 "k8s.io/api/batch/v1"
batchv1beta1 "k8s.io/api/batch/v1beta1"
corev1 "k8s.io/api/core/v1"
extensionsv1beta1 "k8s.io/api/extensions/v1beta1"
"k8s.io/apimachinery/pkg/runtime"
)
@@ -99,7 +98,6 @@ func (s SupportedController) ListSupportedAPIVersions() []runtime.Object {
case Deployments:
supportedVersions = []runtime.Object{
&appsv1.Deployment{},
&extensionsv1beta1.Deployment{},
}
case StatefulSets:
supportedVersions = []runtime.Object{
+10
View File
@@ -57,3 +57,13 @@ body {
text-decoration: underline;
}
.exemption-alert {
margin-top: 15px;
padding: 15px;
border: 1px solid #f26c21;
border-radius: 2px;
}
.exemption-alert .fa-exclamation {
margin-right: 10px;
color: #f26c21;
}
+68 -34
View File
@@ -19,13 +19,15 @@ import (
"encoding/json"
"html/template"
"net/http"
"net/url"
"path"
"strings"
packr "github.com/gobuffalo/packr/v2"
"github.com/gorilla/mux"
conf "github.com/fairwindsops/polaris/pkg/config"
"github.com/fairwindsops/polaris/pkg/config"
"github.com/fairwindsops/polaris/pkg/kube"
"github.com/fairwindsops/polaris/pkg/validator"
packr "github.com/gobuffalo/packr/v2"
"github.com/gorilla/mux"
"github.com/sirupsen/logrus"
"gitlab.com/golang-commonmark/markdown"
)
@@ -80,6 +82,7 @@ func GetMarkdownBox() *packr.Box {
// templateData is passed to the dashboard HTML template
type templateData struct {
BasePath string
Config config.Configuration
AuditData validator.AuditData
JSON template.JS
}
@@ -135,12 +138,28 @@ func writeTemplate(tmpl *template.Template, data *templateData, w http.ResponseW
buf.WriteTo(w)
}
func getConfigForQuery(base config.Configuration, query url.Values) config.Configuration {
c := base
exemptions := query.Get("disallowExemptions")
if exemptions == "false" {
c.DisallowExemptions = false
}
if exemptions == "true" {
c.DisallowExemptions = true
}
return c
}
// GetRouter returns a mux router serving all routes necessary for the dashboard
func GetRouter(c conf.Configuration, auditPath string, port int, basePath string) *mux.Router {
router := mux.NewRouter()
func GetRouter(c config.Configuration, auditPath string, port int, basePath string, auditData *validator.AuditData) *mux.Router {
router := mux.NewRouter().PathPrefix(basePath).Subrouter()
fileServer := http.FileServer(GetAssetBox())
router.PathPrefix("/static/").Handler(http.StripPrefix(path.Join(basePath, "/static/"), fileServer))
router.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("OK"))
})
router.HandleFunc("/favicon.ico", func(w http.ResponseWriter, r *http.Request) {
favicon, err := GetAssetBox().Find("favicon-32x32.png")
if err != nil {
@@ -150,47 +169,67 @@ func GetRouter(c conf.Configuration, auditPath string, port int, basePath string
}
w.Write(favicon)
})
router.HandleFunc("/results.json", func(w http.ResponseWriter, r *http.Request) {
k, err := kube.CreateResourceProvider(auditPath)
if err != nil {
logrus.Errorf("Error fetching Kubernetes resources %v", err)
http.Error(w, "Error fetching Kubernetes resources", http.StatusInternalServerError)
return
adjustedConf := getConfigForQuery(c, r.URL.Query())
if auditData == nil {
k, err := kube.CreateResourceProvider(auditPath)
if err != nil {
logrus.Errorf("Error fetching Kubernetes resources %v", err)
http.Error(w, "Error fetching Kubernetes resources", http.StatusInternalServerError)
return
}
auditDataObj, err := validator.RunAudit(adjustedConf, k)
if err != nil {
http.Error(w, "Error Fetching Deployments", http.StatusInternalServerError)
return
}
auditData = &auditDataObj
}
JSONHandler(w, r, c, k)
JSONHandler(w, r, auditData)
})
router.HandleFunc("/details/{category}", func(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
category := vars["category"]
category = strings.Replace(category, ".md", "", -1)
DetailsHandler(w, r, category, basePath)
})
fileServer := http.FileServer(GetAssetBox())
router.PathPrefix("/static/").Handler(http.StripPrefix("/static/", fileServer))
router.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
if r.URL.Path != "/" && r.URL.Path != basePath {
http.NotFound(w, r)
return
}
k, err := kube.CreateResourceProvider(auditPath)
if err != nil {
logrus.Errorf("Error fetching Kubernetes resources %v", err)
http.Error(w, "Error fetching Kubernetes resources", http.StatusInternalServerError)
return
adjustedConf := getConfigForQuery(c, r.URL.Query())
if auditData == nil {
k, err := kube.CreateResourceProvider(auditPath)
if err != nil {
logrus.Errorf("Error fetching Kubernetes resources %v", err)
http.Error(w, "Error fetching Kubernetes resources", http.StatusInternalServerError)
return
}
auditData, err := validator.RunAudit(adjustedConf, k)
if err != nil {
logrus.Errorf("Error getting audit data: %v", err)
http.Error(w, "Error running audit", 500)
return
}
MainHandler(w, r, adjustedConf, auditData, basePath)
} else {
MainHandler(w, r, adjustedConf, *auditData, basePath)
}
auditData, err := validator.RunAudit(c, k)
if err != nil {
logrus.Errorf("Error getting audit data: %v", err)
http.Error(w, "Error running audit", 500)
return
}
MainHandler(w, r, auditData, basePath)
})
return router
}
// MainHandler gets template data and renders the dashboard with it.
func MainHandler(w http.ResponseWriter, r *http.Request, auditData validator.AuditData, basePath string) {
func MainHandler(w http.ResponseWriter, r *http.Request, c config.Configuration, auditData validator.AuditData, basePath string) {
jsonData, err := json.Marshal(auditData)
if err != nil {
@@ -202,6 +241,7 @@ func MainHandler(w http.ResponseWriter, r *http.Request, auditData validator.Aud
BasePath: basePath,
AuditData: auditData,
JSON: template.JS(jsonData),
Config: c,
}
tmpl, err := GetBaseTemplate("main")
if err != nil {
@@ -213,13 +253,7 @@ func MainHandler(w http.ResponseWriter, r *http.Request, auditData validator.Aud
}
// JSONHandler gets template data and renders json with it.
func JSONHandler(w http.ResponseWriter, r *http.Request, c conf.Configuration, kubeResources *kube.ResourceProvider) {
auditData, err := validator.RunAudit(c, kubeResources)
if err != nil {
http.Error(w, "Error Fetching Deployments", http.StatusInternalServerError)
return
}
func JSONHandler(w http.ResponseWriter, r *http.Request, auditData *validator.AuditData) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(auditData)
+10
View File
@@ -9,6 +9,16 @@
<img class="fw-logo" src="static/images/oss-logo.png" alt="Fairwinds" />
</a>
</div>
{{ if and (not .Config.DisallowExemptions) (gt (len .Config.Exemptions) 0) }}
<div class="exemption-alert">
<i class="fas fa-exclamation"></i>
<span>
Some checks were skipped based on configured exemptions.
<a href="?disallowExemptions=true">Click here</a>
to view the report with these checks included.
</span>
</div>
{{ end }}
</div>
</div>
{{end}}
+174 -117
View File
@@ -18,7 +18,7 @@ import (
"fmt"
"strings"
conf "github.com/fairwindsops/polaris/pkg/config"
"github.com/fairwindsops/polaris/pkg/config"
"github.com/fairwindsops/polaris/pkg/validator/messages"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
@@ -38,7 +38,7 @@ type ContainerValidation struct {
// relevant podSpec in order to check certain aspects of a containerSpec.
// Perhaps there is a more ideal solution instead of attaching a parent
// podSpec to every container Validation struct...
func ValidateContainer(container *corev1.Container, parentPodResult *PodResult, cnConf *conf.Configuration, isInit bool) ContainerResult {
func ValidateContainer(container *corev1.Container, parentPodResult *PodResult, conf *config.Configuration, controllerName string, controllerType config.SupportedController, isInit bool) ContainerResult {
cv := ContainerValidation{
Container: container,
ResourceValidation: &ResourceValidation{},
@@ -57,11 +57,13 @@ func ValidateContainer(container *corev1.Container, parentPodResult *PodResult,
cv.parentPodSpec = parentPodResult.podSpec
}
cv.validateResources(&cnConf.Resources)
cv.validateHealthChecks(&cnConf.HealthChecks)
cv.validateImage(&cnConf.Images)
cv.validateNetworking(&cnConf.Networking)
cv.validateSecurity(&cnConf.Security)
cv.validateResources(conf, controllerName)
if !isInit && controllerType != config.Jobs && controllerType != config.CronJobs {
cv.validateHealthChecks(conf, controllerName)
}
cv.validateImage(conf, controllerName)
cv.validateNetworking(conf, controllerName)
cv.validateSecurity(conf, controllerName)
cRes := ContainerResult{
Name: container.Name,
@@ -72,7 +74,7 @@ func ValidateContainer(container *corev1.Container, parentPodResult *PodResult,
return cRes
}
func (cv *ContainerValidation) validateResources(resConf *conf.Resources) {
func (cv *ContainerValidation) validateResources(conf *config.Configuration, controllerName string) {
// Only validate resources for primary containers. Although it can
// be helpful to set these in certain cases, it usually isn't
if cv.IsInitContainer {
@@ -82,32 +84,56 @@ func (cv *ContainerValidation) validateResources(resConf *conf.Resources) {
category := messages.CategoryResources
res := cv.Container.Resources
if resConf.CPURequestsMissing.IsActionable() && res.Requests.Cpu().MilliValue() == 0 {
cv.addFailure(messages.CPURequestsFailure, resConf.CPURequestsMissing, category)
} else {
cv.validateResourceRange(messages.CPURequestsLabel, &resConf.CPURequestRanges, res.Requests.Cpu())
missingName := "CPURequestsMissing"
rangeName := "CPURequestRanges"
id := config.GetIDFromField(conf.Resources, missingName)
if conf.IsActionable(conf.Resources, missingName, controllerName) && res.Requests.Cpu().MilliValue() == 0 {
cv.addFailure(messages.CPURequestsFailure, conf.Resources.CPURequestsMissing, category, id)
} else if conf.IsActionable(conf.Resources, rangeName, controllerName) {
id := config.GetIDFromField(conf.Resources, rangeName)
cv.validateResourceRange(id, messages.CPURequestsLabel, &conf.Resources.CPURequestRanges, res.Requests.Cpu())
} else if conf.IsActionable(conf.Resources, missingName, controllerName) {
cv.addSuccess(fmt.Sprintf(messages.ResourcePresentSuccess, messages.CPURequestsLabel), category, id)
}
if resConf.CPULimitsMissing.IsActionable() && res.Limits.Cpu().MilliValue() == 0 {
cv.addFailure(messages.CPULimitsFailure, resConf.CPULimitsMissing, category)
} else {
cv.validateResourceRange(messages.CPULimitsLabel, &resConf.CPULimitRanges, res.Requests.Cpu())
missingName = "CPULimitsMissing"
rangeName = "CPULimitRanges"
id = config.GetIDFromField(conf.Resources, missingName)
if conf.IsActionable(conf.Resources, missingName, controllerName) && res.Limits.Cpu().MilliValue() == 0 {
cv.addFailure(messages.CPULimitsFailure, conf.Resources.CPULimitsMissing, category, id)
} else if conf.IsActionable(conf.Resources, rangeName, controllerName) {
id := config.GetIDFromField(conf.Resources, rangeName)
cv.validateResourceRange(id, messages.CPULimitsLabel, &conf.Resources.CPULimitRanges, res.Requests.Cpu())
} else if conf.IsActionable(conf.Resources, missingName, controllerName) {
cv.addSuccess(fmt.Sprintf(messages.ResourcePresentSuccess, messages.CPULimitsLabel), category, id)
}
if resConf.MemoryRequestsMissing.IsActionable() && res.Requests.Memory().MilliValue() == 0 {
cv.addFailure(messages.MemoryRequestsFailure, resConf.MemoryRequestsMissing, category)
} else {
cv.validateResourceRange(messages.MemoryRequestsLabel, &resConf.MemoryRequestRanges, res.Requests.Memory())
missingName = "MemoryRequestsMissing"
rangeName = "MemoryRequestRanges"
id = config.GetIDFromField(conf.Resources, missingName)
if conf.IsActionable(conf.Resources, missingName, controllerName) && res.Requests.Memory().MilliValue() == 0 {
cv.addFailure(messages.MemoryRequestsFailure, conf.Resources.MemoryRequestsMissing, category, id)
} else if conf.IsActionable(conf.Resources, rangeName, controllerName) {
id := config.GetIDFromField(conf.Resources, rangeName)
cv.validateResourceRange(id, messages.MemoryRequestsLabel, &conf.Resources.MemoryRequestRanges, res.Requests.Memory())
} else if conf.IsActionable(conf.Resources, missingName, controllerName) {
cv.addSuccess(fmt.Sprintf(messages.ResourcePresentSuccess, messages.MemoryRequestsLabel), category, id)
}
if resConf.MemoryLimitsMissing.IsActionable() && res.Limits.Memory().MilliValue() == 0 {
cv.addFailure(messages.MemoryLimitsFailure, resConf.MemoryLimitsMissing, category)
} else {
cv.validateResourceRange(messages.MemoryLimitsLabel, &resConf.MemoryLimitRanges, res.Limits.Memory())
missingName = "MemoryLimitsMissing"
rangeName = "MemoryLimitRanges"
id = config.GetIDFromField(conf.Resources, missingName)
if conf.IsActionable(conf.Resources, missingName, controllerName) && res.Limits.Memory().MilliValue() == 0 {
cv.addFailure(messages.MemoryLimitsFailure, conf.Resources.MemoryLimitsMissing, category, id)
} else if conf.IsActionable(conf.Resources, rangeName, controllerName) {
id := config.GetIDFromField(conf.Resources, rangeName)
cv.validateResourceRange(id, messages.MemoryLimitsLabel, &conf.Resources.MemoryLimitRanges, res.Limits.Memory())
} else if conf.IsActionable(conf.Resources, missingName, controllerName) {
cv.addSuccess(fmt.Sprintf(messages.ResourcePresentSuccess, messages.MemoryLimitsLabel), category, id)
}
}
func (cv *ContainerValidation) validateResourceRange(resourceName string, rangeConf *conf.ResourceRanges, res *resource.Quantity) {
func (cv *ContainerValidation) validateResourceRange(id, resourceName string, rangeConf *config.ResourceRanges, res *resource.Quantity) {
warnAbove := rangeConf.Warning.Above
warnBelow := rangeConf.Warning.Below
errorAbove := rangeConf.Error.Above
@@ -115,66 +141,73 @@ func (cv *ContainerValidation) validateResourceRange(resourceName string, rangeC
category := messages.CategoryResources
if errorAbove != nil && errorAbove.MilliValue() < res.MilliValue() {
cv.addError(fmt.Sprintf(messages.ResourceAmountTooHighFailure, resourceName, errorAbove.String()), category)
cv.addError(fmt.Sprintf(messages.ResourceAmountTooHighFailure, resourceName, errorAbove.String()), category, id)
} else if warnAbove != nil && warnAbove.MilliValue() < res.MilliValue() {
cv.addWarning(fmt.Sprintf(messages.ResourceAmountTooHighFailure, resourceName, warnAbove.String()), category)
cv.addWarning(fmt.Sprintf(messages.ResourceAmountTooHighFailure, resourceName, warnAbove.String()), category, id)
} else if errorBelow != nil && errorBelow.MilliValue() > res.MilliValue() {
cv.addError(fmt.Sprintf(messages.ResourceAmountTooLowFailure, resourceName, errorBelow.String()), category)
cv.addError(fmt.Sprintf(messages.ResourceAmountTooLowFailure, resourceName, errorBelow.String()), category, id)
} else if warnBelow != nil && warnBelow.MilliValue() > res.MilliValue() {
cv.addWarning(fmt.Sprintf(messages.ResourceAmountTooLowFailure, resourceName, warnBelow.String()), category)
} else {
if warnAbove != nil || warnBelow != nil || errorAbove != nil || errorBelow != nil {
cv.addSuccess(fmt.Sprintf(messages.ResourceAmountSuccess, resourceName), category)
} else {
cv.addSuccess(fmt.Sprintf(messages.ResourcePresentSuccess, resourceName), category)
}
cv.addWarning(fmt.Sprintf(messages.ResourceAmountTooLowFailure, resourceName, warnBelow.String()), category, id)
} else if errorAbove != nil && warnAbove != nil && errorBelow != nil && warnBelow != nil {
cv.addSuccess(fmt.Sprintf(messages.ResourceAmountSuccess, resourceName), category, id)
}
}
func (cv *ContainerValidation) validateHealthChecks(conf *conf.HealthChecks) {
func (cv *ContainerValidation) validateHealthChecks(conf *config.Configuration, controllerName string) {
category := messages.CategoryHealthChecks
name := "ReadinessProbeMissing"
// Don't validate readiness probes on init containers
if !cv.IsInitContainer && conf.ReadinessProbeMissing.IsActionable() {
if !cv.IsInitContainer && conf.IsActionable(conf.HealthChecks, name, controllerName) {
id := config.GetIDFromField(conf.HealthChecks, name)
if cv.Container.ReadinessProbe == nil {
cv.addFailure(messages.ReadinessProbeFailure, conf.ReadinessProbeMissing, category)
cv.addFailure(messages.ReadinessProbeFailure, conf.HealthChecks.ReadinessProbeMissing, category, id)
} else {
cv.addSuccess(messages.ReadinessProbeSuccess, category)
cv.addSuccess(messages.ReadinessProbeSuccess, category, id)
}
}
if conf.LivenessProbeMissing.IsActionable() {
name = "LivenessProbeMissing"
if conf.IsActionable(conf.HealthChecks, name, controllerName) {
id := config.GetIDFromField(conf.HealthChecks, "LivenessProbeMissing")
if cv.Container.LivenessProbe == nil {
cv.addFailure(messages.LivenessProbeFailure, conf.LivenessProbeMissing, category)
cv.addFailure(messages.LivenessProbeFailure, conf.HealthChecks.LivenessProbeMissing, category, id)
} else {
cv.addSuccess(messages.LivenessProbeSuccess, category)
cv.addSuccess(messages.LivenessProbeSuccess, category, id)
}
}
}
func (cv *ContainerValidation) validateImage(imageConf *conf.Images) {
func (cv *ContainerValidation) validateImage(conf *config.Configuration, controllerName string) {
category := messages.CategoryImages
if imageConf.PullPolicyNotAlways.IsActionable() {
name := "PullPolicyNotAlways"
if conf.IsActionable(conf.Images, name, controllerName) {
id := config.GetIDFromField(conf.Images, name)
if cv.Container.ImagePullPolicy != corev1.PullAlways {
cv.addFailure(messages.ImagePullPolicyFailure, imageConf.PullPolicyNotAlways, category)
cv.addFailure(messages.ImagePullPolicyFailure, conf.Images.PullPolicyNotAlways, category, id)
} else {
cv.addSuccess(messages.ImagePullPolicySuccess, category)
cv.addSuccess(messages.ImagePullPolicySuccess, category, id)
}
}
if imageConf.TagNotSpecified.IsActionable() {
name = "TagNotSpecified"
if conf.IsActionable(conf.Images, name, controllerName) {
id := config.GetIDFromField(conf.Images, name)
img := strings.Split(cv.Container.Image, ":")
if len(img) == 1 || img[1] == "latest" {
cv.addFailure(messages.ImageTagFailure, imageConf.TagNotSpecified, category)
cv.addFailure(messages.ImageTagFailure, conf.Images.TagNotSpecified, category, id)
} else {
cv.addSuccess(messages.ImageTagSuccess, category)
cv.addSuccess(messages.ImageTagSuccess, category, id)
}
}
}
func (cv *ContainerValidation) validateNetworking(networkConf *conf.Networking) {
func (cv *ContainerValidation) validateNetworking(conf *config.Configuration, controllerName string) {
category := messages.CategoryNetworking
if networkConf.HostPortSet.IsActionable() {
name := "HostPortSet"
if conf.IsActionable(conf.Networking, name, controllerName) {
hostPortSet := false
for _, port := range cv.Container.Ports {
if port.HostPort != 0 {
@@ -183,15 +216,16 @@ func (cv *ContainerValidation) validateNetworking(networkConf *conf.Networking)
}
}
id := config.GetIDFromField(conf.Networking, name)
if hostPortSet {
cv.addFailure(messages.HostPortFailure, networkConf.HostPortSet, category)
cv.addFailure(messages.HostPortFailure, conf.Networking.HostPortSet, category, id)
} else {
cv.addSuccess(messages.HostPortSuccess, category)
cv.addSuccess(messages.HostPortSuccess, category, id)
}
}
}
func (cv *ContainerValidation) validateSecurity(securityConf *conf.Security) {
func (cv *ContainerValidation) validateSecurity(conf *config.Configuration, controllerName string) {
category := messages.CategorySecurity
securityContext := cv.Container.SecurityContext
podSecurityContext := cv.parentPodSpec.SecurityContext
@@ -206,107 +240,130 @@ func (cv *ContainerValidation) validateSecurity(securityConf *conf.Security) {
podSecurityContext = &corev1.PodSecurityContext{}
}
if securityConf.RunAsRootAllowed.IsActionable() {
if getBoolValue(securityContext.RunAsNonRoot) {
name := "RunAsRootAllowed"
if conf.IsActionable(conf.Security, name, controllerName) {
id := config.GetIDFromField(conf.Security, name)
runAsRootSuccess := false
if getBoolValue(securityContext.RunAsNonRoot) || (securityContext.RunAsUser != nil && *securityContext.RunAsUser > 0) {
// Check if the container is explicitly set to True (pass)
cv.addSuccess(messages.RunAsRootSuccess, category)
} else if securityContext.RunAsNonRoot == nil {
// Check if the value in the container spec if nil (thus defaulting to the podspec)
// Check if the container value is not set
if getBoolValue(podSecurityContext.RunAsNonRoot) {
// if the pod spec default for containers is true, then pass
cv.addSuccess(messages.RunAsRootSuccess, category)
} else {
// else fail as RunAsNonRoot defaults to false
cv.addFailure(messages.RunAsRootFailure, securityConf.RunAsRootAllowed, category)
}
runAsRootSuccess = true
} else if securityContext.RunAsNonRoot == nil && securityContext.RunAsUser == nil {
// Or if the container values are unset, check the pod values
runAsRootSuccess = getBoolValue(podSecurityContext.RunAsNonRoot) || (podSecurityContext.RunAsUser != nil && *podSecurityContext.RunAsUser > 0)
}
if runAsRootSuccess {
cv.addSuccess(messages.RunAsRootSuccess, category, id)
} else {
cv.addFailure(messages.RunAsRootFailure, securityConf.RunAsRootAllowed, category)
cv.addFailure(messages.RunAsRootFailure, conf.Security.RunAsRootAllowed, category, id)
}
}
if securityConf.RunAsPrivileged.IsActionable() {
name = "RunAsPrivileged"
if conf.IsActionable(conf.Security, name, controllerName) {
id := config.GetIDFromField(conf.Security, name)
if getBoolValue(securityContext.Privileged) {
cv.addFailure(messages.RunAsPrivilegedFailure, securityConf.RunAsPrivileged, category)
cv.addFailure(messages.RunAsPrivilegedFailure, conf.Security.RunAsPrivileged, category, id)
} else {
cv.addSuccess(messages.RunAsPrivilegedSuccess, category)
cv.addSuccess(messages.RunAsPrivilegedSuccess, category, id)
}
}
if securityConf.NotReadOnlyRootFileSystem.IsActionable() {
name = "NotReadOnlyRootFileSystem"
if conf.IsActionable(conf.Security, name, controllerName) {
id := config.GetIDFromField(conf.Security, name)
if getBoolValue(securityContext.ReadOnlyRootFilesystem) {
cv.addSuccess(messages.ReadOnlyFilesystemSuccess, category)
cv.addSuccess(messages.ReadOnlyFilesystemSuccess, category, id)
} else {
cv.addFailure(messages.ReadOnlyFilesystemFailure, securityConf.NotReadOnlyRootFileSystem, category)
cv.addFailure(messages.ReadOnlyFilesystemFailure, conf.Security.NotReadOnlyRootFileSystem, category, id)
}
}
if securityConf.PrivilegeEscalationAllowed.IsActionable() {
name = "PrivilegeEscalationAllowed"
if conf.IsActionable(conf.Security, name, controllerName) {
id := config.GetIDFromField(conf.Security, name)
if getBoolValue(securityContext.AllowPrivilegeEscalation) {
cv.addFailure(messages.PrivilegeEscalationFailure, securityConf.PrivilegeEscalationAllowed, category)
cv.addFailure(messages.PrivilegeEscalationFailure, conf.Security.PrivilegeEscalationAllowed, category, id)
} else {
cv.addSuccess(messages.PrivilegeEscalationSuccess, category)
cv.addSuccess(messages.PrivilegeEscalationSuccess, category, id)
}
}
hasSecurityError :=
!cv.validateCapabilities(securityConf.Capabilities.Error, conf.SeverityError)
hasSecurityWarning :=
!cv.validateCapabilities(securityConf.Capabilities.Warning, conf.SeverityWarning)
hasSecurityCheck := func(confLists conf.SecurityCapabilityLists) bool {
return len(confLists.IfAnyAdded) > 0 ||
len(confLists.IfAnyAddedBeyond) > 0 ||
len(confLists.IfAnyNotDropped) > 0
}
if !hasSecurityError && !hasSecurityWarning &&
(hasSecurityCheck(securityConf.Capabilities.Error) ||
hasSecurityCheck(securityConf.Capabilities.Warning)) {
cv.addSuccess(messages.SecurityCapabilitiesSuccess, category)
name = "Capabilities"
if conf.IsActionable(conf.Security, name, controllerName) {
cv.validateCapabilities(&conf.Security.Capabilities.Warning, &conf.Security.Capabilities.Error)
}
}
func (cv *ContainerValidation) validateCapabilities(confLists conf.SecurityCapabilityLists, severity conf.Severity) bool {
func (cv *ContainerValidation) validateCapabilities(warningLists *config.SecurityCapabilityLists, errorLists *config.SecurityCapabilityLists) {
category := messages.CategorySecurity
capabilities := &corev1.Capabilities{}
if cv.Container.SecurityContext != nil && cv.Container.SecurityContext.Capabilities != nil {
capabilities = cv.Container.SecurityContext.Capabilities
}
allLists := []*config.SecurityCapabilityLists{warningLists, errorLists}
everythingOK := true
if len(confLists.IfAnyAdded) > 0 {
intersectAdds := capIntersection(capabilities.Add, confLists.IfAnyAdded)
if len(intersectAdds) > 0 {
capsString := commaSeparatedCapabilities(intersectAdds)
cv.addFailure(fmt.Sprintf(messages.SecurityCapabilitiesAddedFailure, capsString), severity, category)
everythingOK = false
} else if capContains(capabilities.Add, "ALL") {
cv.addFailure(fmt.Sprintf(messages.SecurityCapabilitiesAddedFailure, "ALL"), severity, category)
everythingOK = false
addID := "capabilitiesAdded"
hasAddFailure := false
hasAddCheck := false
for _, confLists := range allLists {
if len(confLists.IfAnyAdded) == 0 && len(confLists.IfAnyAddedBeyond) == 0 {
continue
}
hasAddCheck = true
var severity config.Severity
if confLists == warningLists {
severity = config.SeverityWarning
} else {
severity = config.SeverityError
}
badAdds := make([]corev1.Capability, 0)
if len(confLists.IfAnyAdded) > 0 {
intersectAdds := capIntersection(capabilities.Add, confLists.IfAnyAdded)
badAdds = append(badAdds, intersectAdds...)
}
if len(confLists.IfAnyAddedBeyond) > 0 {
differentAdds := capDifference(capabilities.Add, confLists.IfAnyAddedBeyond)
differentAdds = capDifference(differentAdds, badAdds)
badAdds = append(badAdds, differentAdds...)
}
if capContains(capabilities.Add, "ALL") && !capContains(badAdds, "ALL") {
badAdds = append(badAdds, "ALL")
}
if len(badAdds) > 0 {
hasAddFailure = true
capsString := commaSeparatedCapabilities(badAdds)
cv.addFailure(fmt.Sprintf(messages.SecurityCapabilitiesAddedFailure, capsString), severity, category, addID)
}
}
if len(confLists.IfAnyAddedBeyond) > 0 {
differentAdds := capDifference(capabilities.Add, confLists.IfAnyAddedBeyond)
if len(differentAdds) > 0 {
capsString := commaSeparatedCapabilities(differentAdds)
cv.addFailure(fmt.Sprintf(messages.SecurityCapabilitiesAddedFailure, capsString), severity, category)
everythingOK = false
} else if capContains(capabilities.Add, "ALL") {
cv.addFailure(fmt.Sprintf(messages.SecurityCapabilitiesAddedFailure, "ALL"), severity, category)
everythingOK = false
}
if hasAddCheck && !hasAddFailure {
cv.addSuccess(messages.SecurityCapabilitiesAddedSuccess, category, addID)
}
if len(confLists.IfAnyNotDropped) > 0 {
dropID := "capabilitiesDropped"
hasDropCheck := false
hasDropFailure := false
for _, confLists := range allLists {
if len(confLists.IfAnyNotDropped) == 0 {
continue
}
hasDropCheck = true
var severity config.Severity
if confLists == warningLists {
severity = config.SeverityWarning
} else {
severity = config.SeverityError
}
missingDrops := capDifference(confLists.IfAnyNotDropped, capabilities.Drop)
id := "capabilitiesNotDropped"
if len(missingDrops) > 0 && !capContains(capabilities.Drop, "ALL") {
hasDropFailure = true
capsString := commaSeparatedCapabilities(missingDrops)
cv.addFailure(fmt.Sprintf(messages.SecurityCapabilitiesNotDroppedFailure, capsString), severity, category)
everythingOK = false
cv.addFailure(fmt.Sprintf(messages.SecurityCapabilitiesNotDroppedFailure, capsString), severity, category, id)
}
}
return everythingOK
if hasDropCheck && !hasDropFailure {
cv.addSuccess(messages.SecurityCapabilitiesNotDroppedSuccess, category, dropID)
}
}
func commaSeparatedCapabilities(caps []corev1.Capability) string {
+424 -32
View File
@@ -15,6 +15,7 @@
package validator
import (
"fmt"
"testing"
conf "github.com/fairwindsops/polaris/pkg/config"
@@ -63,6 +64,83 @@ resources:
memoryLimitsMissing: error
`
var resourceConfExemptions = `---
resources:
cpuRequestsMissing: warning
memoryRequestsMissing: warning
cpuLimitsMissing: error
memoryLimitsMissing: error
exemptions:
- rules:
- cpuRequestsMissing
- memoryRequestsMissing
- cpuLimitsMissing
- memoryLimitsMissing
controllerNames:
- foo
`
var resourceConfRangeExemptions = `---
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
exemptions:
- rules:
- cpuRequestRanges
- memoryRequestRanges
- cpuLimitRanges
- memoryLimitRanges
controllerNames:
- foo
`
func testValidateResources(t *testing.T, container *corev1.Container, resourceConf *string, controllerName string, expectedErrors []*ResultMessage, expectedWarnings []*ResultMessage, expectedSuccesses []*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, controllerName)
assert.Len(t, cv.Warnings, len(expectedWarnings))
assert.ElementsMatch(t, expectedWarnings, cv.Warnings)
assert.Len(t, cv.Errors, len(expectedErrors))
assert.ElementsMatch(t, expectedErrors, cv.Errors)
assert.Len(t, cv.Successes, len(expectedSuccesses))
assert.ElementsMatch(t, expectedSuccesses, cv.Successes)
}
func TestValidateResourcesEmptyConfig(t *testing.T) {
container := corev1.Container{
Name: "Empty",
@@ -73,9 +151,7 @@ func TestValidateResourcesEmptyConfig(t *testing.T) {
ResourceValidation: &ResourceValidation{},
}
expected := conf.Resources{}
cv.validateResources(&expected)
cv.validateResources(&conf.Configuration{}, "")
assert.Len(t, cv.Errors, 0)
}
@@ -86,11 +162,13 @@ func TestValidateResourcesEmptyContainer(t *testing.T) {
expectedWarnings := []*ResultMessage{
{
ID: "cpuRequestsMissing",
Type: "warning",
Message: "CPU requests should be set",
Category: "Resources",
},
{
ID: "memoryRequestsMissing",
Type: "warning",
Message: "Memory requests should be set",
Category: "Resources",
@@ -99,18 +177,22 @@ func TestValidateResourcesEmptyContainer(t *testing.T) {
expectedErrors := []*ResultMessage{
{
ID: "cpuLimitsMissing",
Type: "error",
Message: "CPU limits should be set",
Category: "Resources",
},
{
ID: "memoryLimitsMissing",
Type: "error",
Message: "Memory limits should be set",
Category: "Resources",
},
}
testValidateResources(t, &container, &resourceConf2, &expectedErrors, &expectedWarnings)
expectedSuccesses := []*ResultMessage{}
testValidateResources(t, &container, &resourceConf2, "foo", expectedErrors, expectedWarnings, expectedSuccesses)
}
func TestValidateResourcesPartiallyValid(t *testing.T) {
@@ -134,11 +216,13 @@ func TestValidateResourcesPartiallyValid(t *testing.T) {
expectedWarnings := []*ResultMessage{
{
ID: "cpuRequestRanges",
Type: "warning",
Message: "CPU requests should be higher than 200m",
Category: "Resources",
},
{
ID: "cpuLimitRanges",
Type: "warning",
Message: "CPU limits should be higher than 300m",
Category: "Resources",
@@ -147,18 +231,22 @@ func TestValidateResourcesPartiallyValid(t *testing.T) {
expectedErrors := []*ResultMessage{
{
ID: "memoryRequestRanges",
Type: "error",
Message: "Memory requests should be higher than 100M",
Category: "Resources",
},
{
ID: "memoryLimitRanges",
Type: "error",
Message: "Memory limits should be higher than 200M",
Category: "Resources",
},
}
testValidateResources(t, &container, &resourceConf1, &expectedErrors, &expectedWarnings)
expectedSuccesses := []*ResultMessage{}
testValidateResources(t, &container, &resourceConf1, "foo", expectedErrors, expectedWarnings, expectedSuccesses)
}
func TestValidateResourcesInit(t *testing.T) {
@@ -175,10 +263,10 @@ func TestValidateResourcesInit(t *testing.T) {
parsedConf, err := conf.Parse([]byte(resourceConf1))
assert.NoError(t, err, "Expected no error when parsing config")
cvEmpty.validateResources(&parsedConf.Resources)
cvEmpty.validateResources(&parsedConf, "")
assert.Len(t, cvEmpty.Errors, 4)
cvInit.validateResources(&parsedConf.Resources)
cvInit.validateResources(&parsedConf, "")
assert.Len(t, cvInit.Errors, 0)
}
@@ -209,24 +297,63 @@ func TestValidateResourcesFullyValid(t *testing.T) {
},
}
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{},
expectedSuccesses := []*ResultMessage{
{
ID: "cpuRequestRanges",
Type: "success",
Message: "CPU requests are within the expected range",
Category: "Resources",
},
{
ID: "memoryRequestRanges",
Type: "success",
Message: "Memory requests are within the expected range",
Category: "Resources",
},
{
ID: "cpuLimitRanges",
Type: "success",
Message: "CPU limits are within the expected range",
Category: "Resources",
},
{
ID: "memoryLimitRanges",
Type: "success",
Message: "Memory limits are within the expected range",
Category: "Resources",
},
}
parsedConf, err := conf.Parse([]byte(*resourceConf))
assert.NoError(t, err, "Expected no error when parsing config")
testValidateResources(t, &container, &resourceConf1, "foo", []*ResultMessage{}, []*ResultMessage{}, expectedSuccesses)
cv.validateResources(&parsedConf.Resources)
assert.Len(t, cv.Warnings, len(*expectedWarnings))
assert.ElementsMatch(t, cv.Warnings, *expectedWarnings)
expectedSuccesses = []*ResultMessage{
{
ID: "cpuRequestsMissing",
Type: "success",
Message: "CPU requests are set",
Category: "Resources",
},
{
ID: "memoryRequestsMissing",
Type: "success",
Message: "Memory requests are set",
Category: "Resources",
},
{
ID: "cpuLimitsMissing",
Type: "success",
Message: "CPU limits are set",
Category: "Resources",
},
{
ID: "memoryLimitsMissing",
Type: "success",
Message: "Memory limits are set",
Category: "Resources",
},
}
assert.Len(t, cv.Errors, len(*expectedErrors))
assert.ElementsMatch(t, cv.Errors, *expectedErrors)
testValidateResources(t, &container, &resourceConf2, "foo", []*ResultMessage{}, []*ResultMessage{}, expectedSuccesses)
}
func TestValidateHealthChecks(t *testing.T) {
@@ -261,8 +388,8 @@ func TestValidateHealthChecks(t *testing.T) {
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"}
l := &ResultMessage{ID: "livenessProbeMissing", Type: "warning", Message: "Liveness probe should be configured", Category: "Health Checks"}
r := &ResultMessage{ID: "readinessProbeMissing", Type: "error", Message: "Readiness probe should be configured", Category: "Health Checks"}
f1 := []*ResultMessage{}
f2 := []*ResultMessage{r}
w1 := []*ResultMessage{l}
@@ -284,7 +411,7 @@ func TestValidateHealthChecks(t *testing.T) {
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
tt.cv.validateHealthChecks(&tt.probes)
tt.cv.validateHealthChecks(&conf.Configuration{HealthChecks: tt.probes}, "")
if tt.warnings != nil {
assert.Len(t, tt.cv.Warnings, len(*tt.warnings))
@@ -342,6 +469,7 @@ func TestValidateImage(t *testing.T) {
image: standardConf,
cv: emptyCV,
expected: []*ResultMessage{{
ID: "tagNotSpecified",
Message: "Image tag should be specified",
Type: "error",
Category: "Images",
@@ -352,6 +480,7 @@ func TestValidateImage(t *testing.T) {
image: standardConf,
cv: badCV,
expected: []*ResultMessage{{
ID: "tagNotSpecified",
Message: "Image tag should be specified",
Type: "error",
Category: "Images",
@@ -362,6 +491,7 @@ func TestValidateImage(t *testing.T) {
image: standardConf,
cv: lessBadCV,
expected: []*ResultMessage{{
ID: "tagNotSpecified",
Message: "Image tag should be specified",
Type: "error",
Category: "Images",
@@ -372,10 +502,12 @@ func TestValidateImage(t *testing.T) {
image: strongConf,
cv: badCV,
expected: []*ResultMessage{{
ID: "pullPolicyNotAlways",
Message: "Image pull policy should be \"Always\"",
Type: "error",
Category: "Images",
}, {
ID: "tagNotSpecified",
Message: "Image tag should be specified",
Type: "error",
Category: "Images",
@@ -392,7 +524,7 @@ func TestValidateImage(t *testing.T) {
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
tt.cv = resetCV(tt.cv)
tt.cv.validateImage(&tt.image)
tt.cv.validateImage(&conf.Configuration{Images: tt.image}, "")
assert.Len(t, tt.cv.Errors, len(tt.expected))
assert.ElementsMatch(t, tt.cv.Errors, tt.expected)
})
@@ -450,6 +582,7 @@ func TestValidateNetworking(t *testing.T) {
networkConf: standardConf,
cv: emptyCV,
expectedMessages: []*ResultMessage{{
ID: "hostPortSet",
Message: "Host port is not configured",
Type: "success",
Category: "Networking",
@@ -460,6 +593,7 @@ func TestValidateNetworking(t *testing.T) {
networkConf: standardConf,
cv: emptyCV,
expectedMessages: []*ResultMessage{{
ID: "hostPortSet",
Message: "Host port is not configured",
Type: "success",
Category: "Networking",
@@ -476,6 +610,7 @@ func TestValidateNetworking(t *testing.T) {
networkConf: standardConf,
cv: badCV,
expectedMessages: []*ResultMessage{{
ID: "hostPortSet",
Message: "Host port should not be configured",
Type: "warning",
Category: "Networking",
@@ -486,6 +621,7 @@ func TestValidateNetworking(t *testing.T) {
networkConf: standardConf,
cv: goodCV,
expectedMessages: []*ResultMessage{{
ID: "hostPortSet",
Message: "Host port is not configured",
Type: "success",
Category: "Networking",
@@ -496,6 +632,7 @@ func TestValidateNetworking(t *testing.T) {
networkConf: strongConf,
cv: badCV,
expectedMessages: []*ResultMessage{{
ID: "hostPortSet",
Message: "Host port should not be configured",
Type: "error",
Category: "Networking",
@@ -506,7 +643,7 @@ func TestValidateNetworking(t *testing.T) {
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
tt.cv = resetCV(tt.cv)
tt.cv.validateNetworking(&tt.networkConf)
tt.cv.validateNetworking(&conf.Configuration{Networking: tt.networkConf}, "")
assert.Len(t, tt.cv.messages(), len(tt.expectedMessages))
assert.ElementsMatch(t, tt.cv.messages(), tt.expectedMessages)
})
@@ -682,23 +819,28 @@ func TestValidateSecurity(t *testing.T) {
securityConf: standardConf,
cv: emptyCV,
expectedMessages: []*ResultMessage{{
ID: "runAsRootAllowed",
Message: "Should not be allowed to run as root",
Type: "warning",
Category: "Security",
}, {
ID: "notReadOnlyRootFileSystem",
Message: "Filesystem should be read only",
Type: "warning",
Category: "Security",
}, {
ID: "runAsPrivileged",
Message: "Not running as privileged",
Type: "success",
Category: "Security",
}, {
ID: "privilegeEscalationAllowed",
Message: "Privilege escalation not allowed",
Type: "success",
Category: "Security",
}, {
Message: "Security capabilities are within the configured limits",
ID: "capabilitiesAdded",
Message: "Disallowed security capabilities have not been added",
Type: "success",
Category: "Security",
}},
@@ -708,26 +850,32 @@ func TestValidateSecurity(t *testing.T) {
securityConf: standardConf,
cv: badCV,
expectedMessages: []*ResultMessage{{
ID: "capabilitiesAdded",
Message: "The following security capabilities should not be added: SYS_ADMIN, NET_ADMIN",
Type: "error",
Category: "Security",
}, {
ID: "privilegeEscalationAllowed",
Message: "Privilege escalation should not be allowed",
Type: "error",
Category: "Security",
}, {
ID: "runAsPrivileged",
Message: "Should not be running as privileged",
Type: "error",
Category: "Security",
}, {
ID: "capabilitiesAdded",
Message: "The following security capabilities should not be added: AUDIT_CONTROL, SYS_ADMIN, NET_ADMIN",
Type: "warning",
Category: "Security",
}, {
ID: "runAsRootAllowed",
Message: "Should not be allowed to run as root",
Type: "warning",
Category: "Security",
}, {
ID: "notReadOnlyRootFileSystem",
Message: "Filesystem should be read only",
Type: "warning",
Category: "Security",
@@ -738,26 +886,32 @@ func TestValidateSecurity(t *testing.T) {
securityConf: standardConf,
cv: badCVWithGoodPodSpec,
expectedMessages: []*ResultMessage{{
ID: "capabilitiesAdded",
Message: "The following security capabilities should not be added: SYS_ADMIN, NET_ADMIN",
Type: "error",
Category: "Security",
}, {
ID: "privilegeEscalationAllowed",
Message: "Privilege escalation should not be allowed",
Type: "error",
Category: "Security",
}, {
ID: "runAsPrivileged",
Message: "Should not be running as privileged",
Type: "error",
Category: "Security",
}, {
ID: "capabilitiesAdded",
Message: "The following security capabilities should not be added: AUDIT_CONTROL, SYS_ADMIN, NET_ADMIN",
Type: "warning",
Category: "Security",
}, {
ID: "runAsRootAllowed",
Message: "Should not be allowed to run as root",
Type: "warning",
Category: "Security",
}, {
ID: "notReadOnlyRootFileSystem",
Message: "Filesystem should be read only",
Type: "warning",
Category: "Security",
@@ -768,26 +922,32 @@ func TestValidateSecurity(t *testing.T) {
securityConf: standardConf,
cv: badCVWithBadPodSpec,
expectedMessages: []*ResultMessage{{
ID: "capabilitiesAdded",
Message: "The following security capabilities should not be added: SYS_ADMIN, NET_ADMIN",
Type: "error",
Category: "Security",
}, {
ID: "privilegeEscalationAllowed",
Message: "Privilege escalation should not be allowed",
Type: "error",
Category: "Security",
}, {
ID: "runAsPrivileged",
Message: "Should not be running as privileged",
Type: "error",
Category: "Security",
}, {
ID: "capabilitiesAdded",
Message: "The following security capabilities should not be added: AUDIT_CONTROL, SYS_ADMIN, NET_ADMIN",
Type: "warning",
Category: "Security",
}, {
ID: "runAsRootAllowed",
Message: "Should not be allowed to run as root",
Type: "warning",
Category: "Security",
}, {
ID: "notReadOnlyRootFileSystem",
Message: "Filesystem should be read only",
Type: "warning",
Category: "Security",
@@ -798,23 +958,28 @@ func TestValidateSecurity(t *testing.T) {
securityConf: standardConf,
cv: goodCV,
expectedMessages: []*ResultMessage{{
ID: "runAsRootAllowed",
Message: "Is not allowed to run as root",
Type: "success",
Category: "Security",
}, {
ID: "notReadOnlyRootFileSystem",
Message: "Filesystem is read only",
Type: "success",
Category: "Security",
}, {
ID: "runAsPrivileged",
Message: "Not running as privileged",
Type: "success",
Category: "Security",
}, {
ID: "privilegeEscalationAllowed",
Message: "Privilege escalation not allowed",
Type: "success",
Category: "Security",
}, {
Message: "Security capabilities are within the configured limits",
ID: "capabilitiesAdded",
Message: "Disallowed security capabilities have not been added",
Type: "success",
Category: "Security",
}},
@@ -824,22 +989,32 @@ func TestValidateSecurity(t *testing.T) {
securityConf: strongConf,
cv: goodCV,
expectedMessages: []*ResultMessage{{
ID: "capabilitiesNotDropped",
Message: "The following security capabilities should be dropped: DAC_OVERRIDE, SYS_CHROOT",
Type: "error",
Category: "Security",
}, {
ID: "capabilitiesAdded",
Message: "Disallowed security capabilities have not been added",
Type: "success",
Category: "Security",
}, {
ID: "runAsRootAllowed",
Message: "Is not allowed to run as root",
Type: "success",
Category: "Security",
}, {
ID: "notReadOnlyRootFileSystem",
Message: "Filesystem is read only",
Type: "success",
Category: "Security",
}, {
ID: "runAsPrivileged",
Message: "Not running as privileged",
Type: "success",
Category: "Security",
}, {
ID: "privilegeEscalationAllowed",
Message: "Privilege escalation not allowed",
Type: "success",
Category: "Security",
@@ -850,23 +1025,33 @@ func TestValidateSecurity(t *testing.T) {
securityConf: strongConf,
cv: strongCV,
expectedMessages: []*ResultMessage{{
ID: "runAsRootAllowed",
Message: "Is not allowed to run as root",
Type: "success",
Category: "Security",
}, {
ID: "notReadOnlyRootFileSystem",
Message: "Filesystem is read only",
Type: "success",
Category: "Security",
}, {
ID: "runAsPrivileged",
Message: "Not running as privileged",
Type: "success",
Category: "Security",
}, {
ID: "privilegeEscalationAllowed",
Message: "Privilege escalation not allowed",
Type: "success",
Category: "Security",
}, {
Message: "Security capabilities are within the configured limits",
ID: "capabilitiesAdded",
Message: "Disallowed security capabilities have not been added",
Type: "success",
Category: "Security",
}, {
ID: "capabilitiesDropped",
Message: "All disallowed security capabilities have been dropped",
Type: "success",
Category: "Security",
}},
@@ -876,23 +1061,33 @@ func TestValidateSecurity(t *testing.T) {
securityConf: strongConf,
cv: strongCVWithPodSpecSecurityContext,
expectedMessages: []*ResultMessage{{
ID: "runAsRootAllowed",
Message: "Is not allowed to run as root",
Type: "success",
Category: "Security",
}, {
ID: "notReadOnlyRootFileSystem",
Message: "Filesystem is read only",
Type: "success",
Category: "Security",
}, {
ID: "runAsPrivileged",
Message: "Not running as privileged",
Type: "success",
Category: "Security",
}, {
ID: "privilegeEscalationAllowed",
Message: "Privilege escalation not allowed",
Type: "success",
Category: "Security",
}, {
Message: "Security capabilities are within the configured limits",
ID: "capabilitiesAdded",
Message: "Disallowed security capabilities have not been added",
Type: "success",
Category: "Security",
}, {
ID: "capabilitiesDropped",
Message: "All disallowed security capabilities have been dropped",
Type: "success",
Category: "Security",
}},
@@ -902,23 +1097,33 @@ func TestValidateSecurity(t *testing.T) {
securityConf: strongConf,
cv: strongCVWithBadPodSpecSecurityContext,
expectedMessages: []*ResultMessage{{
ID: "runAsRootAllowed",
Message: "Is not allowed to run as root",
Type: "success",
Category: "Security",
}, {
ID: "notReadOnlyRootFileSystem",
Message: "Filesystem is read only",
Type: "success",
Category: "Security",
}, {
ID: "runAsPrivileged",
Message: "Not running as privileged",
Type: "success",
Category: "Security",
}, {
ID: "privilegeEscalationAllowed",
Message: "Privilege escalation not allowed",
Type: "success",
Category: "Security",
}, {
Message: "Security capabilities are within the configured limits",
ID: "capabilitiesAdded",
Message: "Disallowed security capabilities have not been added",
Type: "success",
Category: "Security",
}, {
ID: "capabilitiesDropped",
Message: "All disallowed security capabilities have been dropped",
Type: "success",
Category: "Security",
}},
@@ -928,13 +1133,200 @@ func TestValidateSecurity(t *testing.T) {
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
tt.cv = resetCV(tt.cv)
tt.cv.validateSecurity(&tt.securityConf)
tt.cv.validateSecurity(&conf.Configuration{Security: tt.securityConf}, "")
assert.Len(t, tt.cv.messages(), len(tt.expectedMessages))
assert.ElementsMatch(t, tt.cv.messages(), tt.expectedMessages)
})
}
}
func TestValidateRunAsRoot(t *testing.T) {
falseVar := false
trueVar := true
nonRootUser := int64(1000)
rootUser := int64(0)
config := conf.Configuration{
Security: conf.Security{
RunAsRootAllowed: conf.SeverityWarning,
},
}
testCases := []struct {
cv ContainerValidation
message ResultMessage
}{
{
cv: ContainerValidation{
ResourceValidation: &ResourceValidation{},
Container: &corev1.Container{Name: "", SecurityContext: &corev1.SecurityContext{
RunAsNonRoot: nil,
}},
parentPodSpec: corev1.PodSpec{
SecurityContext: &corev1.PodSecurityContext{
RunAsNonRoot: &falseVar,
},
},
},
message: ResultMessage{
ID: "runAsRootAllowed",
Message: "Should not be allowed to run as root",
Type: "warning",
Category: "Security",
},
},
{
cv: ContainerValidation{
ResourceValidation: &ResourceValidation{},
Container: &corev1.Container{Name: "", SecurityContext: &corev1.SecurityContext{
RunAsNonRoot: &trueVar,
}},
parentPodSpec: corev1.PodSpec{
SecurityContext: &corev1.PodSecurityContext{
RunAsNonRoot: &falseVar,
},
},
},
message: ResultMessage{
ID: "runAsRootAllowed",
Message: "Is not allowed to run as root",
Type: "success",
Category: "Security",
},
},
{
cv: ContainerValidation{
ResourceValidation: &ResourceValidation{},
Container: &corev1.Container{Name: "", SecurityContext: &corev1.SecurityContext{
RunAsUser: &nonRootUser,
}},
},
message: ResultMessage{
ID: "runAsRootAllowed",
Message: "Is not allowed to run as root",
Type: "success",
Category: "Security",
},
},
{
cv: ContainerValidation{
ResourceValidation: &ResourceValidation{},
Container: &corev1.Container{Name: "", SecurityContext: &corev1.SecurityContext{}},
parentPodSpec: corev1.PodSpec{
SecurityContext: &corev1.PodSecurityContext{
RunAsUser: &nonRootUser,
},
},
},
message: ResultMessage{
ID: "runAsRootAllowed",
Message: "Is not allowed to run as root",
Type: "success",
Category: "Security",
},
},
{
cv: ContainerValidation{
ResourceValidation: &ResourceValidation{},
Container: &corev1.Container{Name: "", SecurityContext: &corev1.SecurityContext{
RunAsUser: &rootUser,
}},
parentPodSpec: corev1.PodSpec{
SecurityContext: &corev1.PodSecurityContext{
RunAsUser: &nonRootUser,
},
},
},
message: ResultMessage{
ID: "runAsRootAllowed",
Message: "Should not be allowed to run as root",
Type: "warning",
Category: "Security",
},
},
{
cv: ContainerValidation{
ResourceValidation: &ResourceValidation{},
Container: &corev1.Container{Name: "", SecurityContext: &corev1.SecurityContext{
RunAsNonRoot: &falseVar,
}},
parentPodSpec: corev1.PodSpec{
SecurityContext: &corev1.PodSecurityContext{
RunAsUser: &nonRootUser,
},
},
},
message: ResultMessage{
ID: "runAsRootAllowed",
Message: "Should not be allowed to run as root",
Type: "warning",
Category: "Security",
},
},
}
for idx, tt := range testCases {
tt.cv.validateSecurity(&config, "")
assert.Len(t, tt.cv.messages(), 1)
assert.Equal(t, &tt.message, tt.cv.messages()[0], fmt.Sprintf("Test case %d failed", idx))
}
}
func TestValidateResourcesExemption(t *testing.T) {
container := corev1.Container{
Name: "Empty",
}
expectedWarnings := []*ResultMessage{}
expectedErrors := []*ResultMessage{}
expectedSuccesses := []*ResultMessage{}
testValidateResources(t, &container, &resourceConfExemptions, "foo", expectedErrors, expectedWarnings, expectedSuccesses)
expectedWarnings = []*ResultMessage{
{
ID: "cpuRequestsMissing",
Type: "warning",
Message: "CPU requests should be set",
Category: "Resources",
},
{
ID: "memoryRequestsMissing",
Type: "warning",
Message: "Memory requests should be set",
Category: "Resources",
},
}
expectedErrors = []*ResultMessage{
{
ID: "cpuLimitsMissing",
Type: "error",
Message: "CPU limits should be set",
Category: "Resources",
},
{
ID: "memoryLimitsMissing",
Type: "error",
Message: "Memory limits should be set",
Category: "Resources",
},
}
disallowExemptionsConf := resourceConfExemptions + "\ndisallowExemptions: true"
testValidateResources(t, &container, &disallowExemptionsConf, "foo", expectedErrors, expectedWarnings, expectedSuccesses)
}
func TestValidateResourceRangeExemption(t *testing.T) {
container := corev1.Container{
Name: "Empty",
}
expectedWarnings := []*ResultMessage{}
expectedErrors := []*ResultMessage{}
expectedSuccesses := []*ResultMessage{}
testValidateResources(t, &container, &resourceConfRangeExemptions, "foo", expectedErrors, expectedWarnings, expectedSuccesses)
}
func resetCV(cv ContainerValidation) ContainerValidation {
cv.Errors = []*ResultMessage{}
cv.Successes = []*ResultMessage{}
+16 -2
View File
@@ -15,6 +15,8 @@
package validator
import (
"strings"
conf "github.com/fairwindsops/polaris/pkg/config"
"github.com/fairwindsops/polaris/pkg/kube"
"github.com/fairwindsops/polaris/pkg/validator/controllers"
@@ -22,12 +24,15 @@ import (
"github.com/sirupsen/logrus"
)
const exemptionAnnotationKey = "polaris.fairwinds.com/exempt"
// ValidateController validates a single controller, returns a ControllerResult.
func ValidateController(conf conf.Configuration, controller controller.Interface) ControllerResult {
controllerType := controller.GetType()
pod := controller.GetPodSpec()
podResult := ValidatePod(conf, pod)
podResult := ValidatePod(conf, pod, controller.GetName(), controllerType)
return ControllerResult{
Type: controller.GetType().String(),
Type: controllerType.String(),
Name: controller.GetName(),
PodResult: podResult,
}
@@ -43,6 +48,9 @@ func ValidateControllers(config conf.Configuration, kubeResources *kube.Resource
}
for _, controller := range controllersToAudit {
if !config.DisallowExemptions && hasExemptionAnnotation(controller) {
continue
}
controllerResult := ValidateController(config, controller)
nsResult := nsResults.getNamespaceResult(controller.GetNamespace())
nsResult.Summary.appendResults(*controllerResult.PodResult.Summary)
@@ -51,3 +59,9 @@ func ValidateControllers(config conf.Configuration, kubeResources *kube.Resource
}
}
}
func hasExemptionAnnotation(ctrl controller.Interface) bool {
annot := ctrl.GetAnnotations()
val := annot[exemptionAnnotationKey]
return strings.ToLower(val) == "true"
}
+179
View File
@@ -0,0 +1,179 @@
// Copyright 2019 FairwindsOps Inc
//
// 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"
"github.com/stretchr/testify/assert"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
conf "github.com/fairwindsops/polaris/pkg/config"
"github.com/fairwindsops/polaris/pkg/kube"
controller "github.com/fairwindsops/polaris/pkg/validator/controllers"
"github.com/fairwindsops/polaris/test"
)
func TestValidateController(t *testing.T) {
c := conf.Configuration{
Security: conf.Security{
HostIPCSet: conf.SeverityError,
HostPIDSet: conf.SeverityError,
},
}
deployment := controller.NewDeploymentController(test.MockDeploy())
expectedSum := ResultSummary{
Totals: CountSummary{
Successes: uint(2),
Warnings: uint(0),
Errors: uint(0),
},
ByCategory: make(map[string]*CountSummary),
}
expectedSum.ByCategory["Security"] = &CountSummary{
Successes: uint(2),
Warnings: uint(0),
Errors: uint(0),
}
expectedMessages := []*ResultMessage{
{ID: "hostIPCSet", Message: "Host IPC is not configured", Type: "success", Category: "Security"},
{ID: "hostPIDSet", Message: "Host PID is not configured", Type: "success", Category: "Security"},
}
actualResult := ValidateController(c, deployment)
assert.Equal(t, "Deployments", actualResult.Type)
assert.Equal(t, 1, len(actualResult.PodResult.ContainerResults), "should be equal")
assert.EqualValues(t, &expectedSum, actualResult.PodResult.Summary)
assert.EqualValues(t, expectedMessages, actualResult.PodResult.Messages)
}
func TestSkipHealthChecks(t *testing.T) {
c := conf.Configuration{
HealthChecks: conf.HealthChecks{
ReadinessProbeMissing: conf.SeverityError,
LivenessProbeMissing: conf.SeverityWarning,
},
ControllersToScan: []conf.SupportedController{
conf.Deployments,
conf.StatefulSets,
conf.DaemonSets,
conf.Jobs,
conf.CronJobs,
conf.ReplicationControllers,
},
}
deploymentBase := test.MockDeploy()
deploymentBase.Spec.Template.Spec.InitContainers = []corev1.Container{test.MockContainer("test")}
deployment := controller.NewDeploymentController(deploymentBase)
expectedSum := ResultSummary{
Totals: CountSummary{
Successes: uint(0),
Warnings: uint(1),
Errors: uint(1),
},
ByCategory: make(map[string]*CountSummary),
}
expectedSum.ByCategory["Health Checks"] = &CountSummary{
Successes: uint(0),
Warnings: uint(1),
Errors: uint(1),
}
expectedMessages := []*ResultMessage{
{ID: "readinessProbeMissing", Message: "Readiness probe should be configured", Type: "error", Category: "Health Checks"},
{ID: "livenessProbeMissing", Message: "Liveness probe should be configured", Type: "warning", Category: "Health Checks"},
}
actualResult := ValidateController(c, deployment)
assert.Equal(t, "Deployments", actualResult.Type)
assert.Equal(t, 2, len(actualResult.PodResult.ContainerResults), "should be equal")
assert.EqualValues(t, &expectedSum, actualResult.PodResult.Summary)
assert.EqualValues(t, []*ResultMessage{}, actualResult.PodResult.ContainerResults[0].Messages)
assert.EqualValues(t, expectedMessages, actualResult.PodResult.ContainerResults[1].Messages)
job := controller.NewJobController(test.MockJob())
expectedSum = ResultSummary{
Totals: CountSummary{
Successes: uint(0),
Warnings: uint(0),
Errors: uint(0),
},
ByCategory: make(map[string]*CountSummary),
}
expectedMessages = []*ResultMessage{}
actualResult = ValidateController(c, job)
assert.Equal(t, "Jobs", actualResult.Type)
assert.Equal(t, 1, len(actualResult.PodResult.ContainerResults), "should be equal")
assert.EqualValues(t, &expectedSum, actualResult.PodResult.Summary)
assert.EqualValues(t, expectedMessages, actualResult.PodResult.ContainerResults[0].Messages)
cronjob := controller.NewCronJobController(test.MockCronJob())
expectedSum = ResultSummary{
Totals: CountSummary{
Successes: uint(0),
Warnings: uint(0),
Errors: uint(0),
},
ByCategory: make(map[string]*CountSummary),
}
expectedMessages = []*ResultMessage{}
actualResult = ValidateController(c, cronjob)
assert.Equal(t, "CronJobs", actualResult.Type)
assert.Equal(t, 1, len(actualResult.PodResult.ContainerResults), "should be equal")
assert.EqualValues(t, &expectedSum, actualResult.PodResult.Summary)
assert.EqualValues(t, expectedMessages, actualResult.PodResult.ContainerResults[0].Messages)
}
func TestControllerExemptions(t *testing.T) {
c := conf.Configuration{
HealthChecks: conf.HealthChecks{
ReadinessProbeMissing: conf.SeverityError,
LivenessProbeMissing: conf.SeverityWarning,
},
ControllersToScan: []conf.SupportedController{
conf.Deployments,
},
}
resources := &kube.ResourceProvider{
Deployments: []appsv1.Deployment{test.MockDeploy()},
}
expectedSum := ResultSummary{
Totals: CountSummary{
Successes: uint(0),
Warnings: uint(1),
Errors: uint(1),
},
ByCategory: make(map[string]*CountSummary),
}
expectedSum.ByCategory["Health Checks"] = &CountSummary{
Successes: uint(0),
Warnings: uint(1),
Errors: uint(1),
}
nsResults := NamespacedResults{}
ValidateControllers(c, resources, &nsResults)
actualResult := nsResults[""].DeploymentResults[0]
assert.Equal(t, "Deployments", actualResult.Type)
assert.EqualValues(t, &expectedSum, actualResult.PodResult.Summary)
resources.Deployments[0].ObjectMeta.Annotations = map[string]string{
exemptionAnnotationKey: "true",
}
nsResults = NamespacedResults{}
ValidateControllers(c, resources, &nsResults)
assert.Equal(t, (*NamespaceResult)(nil), nsResults[""])
}
+5
View File
@@ -27,6 +27,11 @@ func (c CronJobController) GetType() config.SupportedController {
return config.CronJobs
}
// GetAnnotations returns the controller's annotations
func (c CronJobController) GetAnnotations() map[string]string {
return c.K8SResource.ObjectMeta.Annotations
}
// NewCronJobController builds a new controller interface for Deployments
func NewCronJobController(originalDeploymentResource kubeAPIBatchV1beta1.CronJob) Interface {
controller := CronJobController{}
+5
View File
@@ -22,6 +22,11 @@ func (d DaemonSetController) GetPodSpec() *kubeAPICoreV1.PodSpec {
return &d.K8SResource.Spec.Template.Spec
}
// GetAnnotations returns the controller's annotations
func (d DaemonSetController) GetAnnotations() map[string]string {
return d.K8SResource.ObjectMeta.Annotations
}
// GetType returns the supportedcontroller enum type
func (d DaemonSetController) GetType() config.SupportedController {
return config.DaemonSets
+5
View File
@@ -22,6 +22,11 @@ func (d DeploymentController) GetPodSpec() *kubeAPICoreV1.PodSpec {
return &d.K8SResource.Spec.Template.Spec
}
// GetAnnotations returns the controller's annotations
func (d DeploymentController) GetAnnotations() map[string]string {
return d.K8SResource.ObjectMeta.Annotations
}
// GetType returns the supportedcontroller enum type
func (d DeploymentController) GetType() config.SupportedController {
return config.Deployments
+1
View File
@@ -15,6 +15,7 @@ type Interface interface {
GetPodTemplate() *kubeAPICoreV1.PodTemplateSpec
GetPodSpec() *kubeAPICoreV1.PodSpec
GetType() config.SupportedController
GetAnnotations() map[string]string
}
// GenericController is a base implementation with some free methods for inherited structs
+5
View File
@@ -22,6 +22,11 @@ func (j JobController) GetPodSpec() *kubeAPICoreV1.PodSpec {
return &j.K8SResource.Spec.Template.Spec
}
// GetAnnotations returns the controller's annotations
func (j JobController) GetAnnotations() map[string]string {
return j.K8SResource.ObjectMeta.Annotations
}
// GetType returns the supportedcontroller enum type
func (j JobController) GetType() config.SupportedController {
return config.Jobs
@@ -24,6 +24,11 @@ func (r ReplicationControllerController) GetPodSpec() *kubeAPICoreV1.PodSpec {
return &r.K8SResource.Spec.Template.Spec
}
// GetAnnotations returns the controller's annotations
func (r ReplicationControllerController) GetAnnotations() map[string]string {
return r.K8SResource.ObjectMeta.Annotations
}
// GetType returns the supportedcontroller enum type
func (r ReplicationControllerController) GetType() config.SupportedController {
return config.ReplicationControllers
@@ -22,6 +22,11 @@ func (s StatefulSetController) GetPodSpec() *kubeAPICoreV1.PodSpec {
return &s.K8SResource.Spec.Template.Spec
}
// GetAnnotations returns the controller's annotations
func (s StatefulSetController) GetAnnotations() map[string]string {
return s.K8SResource.ObjectMeta.Annotations
}
// GetType returns the supportedcontroller enum type
func (s StatefulSetController) GetType() config.SupportedController {
return config.StatefulSets
-32
View File
@@ -7,38 +7,6 @@ import (
"github.com/fairwindsops/polaris/pkg/kube"
)
const (
// PolarisOutputVersion is the version of the current output structure
PolarisOutputVersion = "0.2"
)
// ClusterSummary contains Polaris results as well as some high-level stats
type ClusterSummary struct {
Results ResultSummary
Version string
Nodes int
Pods int
Namespaces int
Deployments int
StatefulSets int
DaemonSets int
Jobs int
CronJobs int
ReplicationControllers int
Score uint
}
// AuditData contains all the data from a full Polaris audit
type AuditData struct {
PolarisOutputVersion string
AuditTime string
SourceType string
SourceName string
DisplayName string
ClusterSummary ClusterSummary
NamespacedResults NamespacedResults
}
// RunAudit runs a full Polaris audit and returns an AuditData object
func RunAudit(config conf.Configuration, kubeResources *kube.ResourceProvider) (AuditData, error) {
nsResults := NamespacedResults{}
+7 -12
View File
@@ -33,21 +33,16 @@ func TestGetTemplateData(t *testing.T) {
// TODO: split out the logic for calculating summaries into another set of tests
sum := ResultSummary{
Totals: CountSummary{
Successes: uint(24),
Warnings: uint(6),
Errors: uint(6),
Successes: uint(0),
Warnings: uint(4),
Errors: uint(4),
},
ByCategory: CategorySummary{},
}
sum.ByCategory["Health Checks"] = &CountSummary{
Successes: uint(0),
Warnings: uint(6),
Errors: uint(6),
}
sum.ByCategory["Resources"] = &CountSummary{
Successes: uint(24),
Warnings: uint(0),
Errors: uint(0),
Warnings: uint(4),
Errors: uint(4),
}
actualAudit, err := RunAudit(c, resources)
@@ -60,10 +55,10 @@ func TestGetTemplateData(t *testing.T) {
assert.Equal(t, 1, len(actualAudit.NamespacedResults["test"].DeploymentResults), "should be equal")
assert.Equal(t, 1, len(actualAudit.NamespacedResults["test"].DeploymentResults), "should be equal")
assert.Equal(t, 1, len(actualAudit.NamespacedResults["test"].DeploymentResults[0].PodResult.ContainerResults), "should be equal")
assert.Equal(t, 6, len(actualAudit.NamespacedResults["test"].DeploymentResults[0].PodResult.ContainerResults[0].Messages), "should be equal")
assert.Equal(t, 2, len(actualAudit.NamespacedResults["test"].DeploymentResults[0].PodResult.ContainerResults[0].Messages), "should be equal")
assert.Equal(t, 1, len(actualAudit.NamespacedResults["test"].StatefulSetResults), "should be equal")
assert.Equal(t, 1, len(actualAudit.NamespacedResults["test"].StatefulSetResults), "should be equal")
assert.Equal(t, 1, len(actualAudit.NamespacedResults["test"].StatefulSetResults[0].PodResult.ContainerResults), "should be equal")
assert.Equal(t, 6, len(actualAudit.NamespacedResults["test"].StatefulSetResults[0].PodResult.ContainerResults[0].Messages), "should be equal")
assert.Equal(t, 2, len(actualAudit.NamespacedResults["test"].StatefulSetResults[0].PodResult.ContainerResults[0].Messages), "should be equal")
}
+4 -2
View File
@@ -73,12 +73,14 @@ const (
PrivilegeEscalationFailure = "Privilege escalation should not be allowed"
// PrivilegeEscalationSuccess message
PrivilegeEscalationSuccess = "Privilege escalation not allowed"
// SecurityCapabilitiesAddedSuccess message
SecurityCapabilitiesAddedSuccess = "Disallowed security capabilities have not been added"
// SecurityCapabilitiesAddedFailure message
SecurityCapabilitiesAddedFailure = "The following security capabilities should not be added: %v"
// SecurityCapabilitiesNotDroppedSuccess message
SecurityCapabilitiesNotDroppedSuccess = "All disallowed security capabilities have been dropped"
// SecurityCapabilitiesNotDroppedFailure message
SecurityCapabilitiesNotDroppedFailure = "The following security capabilities should be dropped: %v"
// SecurityCapabilitiesSuccess message
SecurityCapabilitiesSuccess = "Security capabilities are within the configured limits"
// HostAliasFailure message
HostAliasFailure = "Host alias should not be configured"
+25 -19
View File
@@ -15,7 +15,7 @@
package validator
import (
conf "github.com/fairwindsops/polaris/pkg/config"
"github.com/fairwindsops/polaris/pkg/config"
"github.com/fairwindsops/polaris/pkg/validator/messages"
corev1 "k8s.io/api/core/v1"
)
@@ -27,14 +27,14 @@ type PodValidation struct {
}
// ValidatePod validates that each pod conforms to the Polaris config, returns a ResourceResult.
func ValidatePod(podConf conf.Configuration, pod *corev1.PodSpec) PodResult {
func ValidatePod(conf config.Configuration, pod *corev1.PodSpec, controllerName string, controllerType config.SupportedController) PodResult {
pv := PodValidation{
Pod: pod,
ResourceValidation: &ResourceValidation{},
}
pv.validateSecurity(&podConf.Security)
pv.validateNetworking(&podConf.Networking)
pv.validateSecurity(&conf, controllerName)
pv.validateNetworking(&conf, controllerName)
pRes := PodResult{
Messages: pv.messages(),
@@ -43,8 +43,8 @@ func ValidatePod(podConf conf.Configuration, pod *corev1.PodSpec) PodResult {
podSpec: *pod,
}
pv.validateContainers(pod.InitContainers, &pRes, &podConf, true)
pv.validateContainers(pod.Containers, &pRes, &podConf, false)
pv.validateContainers(pod.InitContainers, &pRes, &conf, controllerName, controllerType, true)
pv.validateContainers(pod.Containers, &pRes, &conf, controllerName, controllerType, false)
for _, cRes := range pRes.ContainerResults {
pRes.Summary.appendResults(*cRes.Summary)
@@ -53,41 +53,47 @@ func ValidatePod(podConf conf.Configuration, pod *corev1.PodSpec) PodResult {
return pRes
}
func (pv *PodValidation) validateContainers(containers []corev1.Container, pRes *PodResult, podConf *conf.Configuration, isInit bool) {
func (pv *PodValidation) validateContainers(containers []corev1.Container, pRes *PodResult, conf *config.Configuration, controllerName string, controllerType config.SupportedController, isInit bool) {
for _, container := range containers {
cRes := ValidateContainer(&container, pRes, podConf, isInit)
cRes := ValidateContainer(&container, pRes, conf, controllerName, controllerType, isInit)
pRes.ContainerResults = append(pRes.ContainerResults, cRes)
}
}
func (pv *PodValidation) validateSecurity(securityConf *conf.Security) {
func (pv *PodValidation) validateSecurity(conf *config.Configuration, controllerName string) {
category := messages.CategorySecurity
if securityConf.HostIPCSet.IsActionable() {
name := "HostIPCSet"
if conf.IsActionable(conf.Security, name, controllerName) {
id := config.GetIDFromField(conf.Security, name)
if pv.Pod.HostIPC {
pv.addFailure(messages.HostIPCFailure, securityConf.HostIPCSet, category)
pv.addFailure(messages.HostIPCFailure, conf.Security.HostIPCSet, category, id)
} else {
pv.addSuccess(messages.HostIPCSuccess, category)
pv.addSuccess(messages.HostIPCSuccess, category, id)
}
}
if securityConf.HostPIDSet.IsActionable() {
name = "HostPIDSet"
if conf.IsActionable(conf.Security, name, controllerName) {
id := config.GetIDFromField(conf.Security, name)
if pv.Pod.HostPID {
pv.addFailure(messages.HostPIDFailure, securityConf.HostPIDSet, category)
pv.addFailure(messages.HostPIDFailure, conf.Security.HostPIDSet, category, id)
} else {
pv.addSuccess(messages.HostPIDSuccess, category)
pv.addSuccess(messages.HostPIDSuccess, category, id)
}
}
}
func (pv *PodValidation) validateNetworking(networkConf *conf.Networking) {
func (pv *PodValidation) validateNetworking(conf *config.Configuration, controllerName string) {
category := messages.CategoryNetworking
if networkConf.HostNetworkSet.IsActionable() {
name := "HostNetworkSet"
if conf.IsActionable(conf.Networking, name, controllerName) {
id := config.GetIDFromField(conf.Networking, name)
if pv.Pod.HostNetwork {
pv.addFailure(messages.HostNetworkFailure, networkConf.HostNetworkSet, category)
pv.addFailure(messages.HostNetworkFailure, conf.Networking.HostNetworkSet, category, id)
} else {
pv.addSuccess(messages.HostNetworkSuccess, category)
pv.addSuccess(messages.HostNetworkSuccess, category, id)
}
}
}
+73 -41
View File
@@ -40,7 +40,7 @@ func TestValidatePod(t *testing.T) {
expectedSum := ResultSummary{
Totals: CountSummary{
Successes: uint(8),
Successes: uint(4),
Warnings: uint(0),
Errors: uint(0),
},
@@ -51,11 +51,6 @@ func TestValidatePod(t *testing.T) {
Warnings: uint(0),
Errors: uint(0),
}
expectedSum.ByCategory["Resources"] = &CountSummary{
Successes: uint(4),
Warnings: uint(0),
Errors: uint(0),
}
expectedSum.ByCategory["Security"] = &CountSummary{
Successes: uint(2),
Warnings: uint(0),
@@ -63,12 +58,12 @@ func TestValidatePod(t *testing.T) {
}
expectedMessages := []*ResultMessage{
{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"},
{ID: "hostIPCSet", Message: "Host IPC is not configured", Type: "success", Category: "Security"},
{ID: "hostPIDSet", Message: "Host PID is not configured", Type: "success", Category: "Security"},
{ID: "hostNetworkSet", Message: "Host network is not configured", Type: "success", Category: "Networking"},
}
actualPodResult := ValidatePod(c, &pod.Spec)
actualPodResult := ValidatePod(c, &pod.Spec, "", conf.Deployments)
assert.Equal(t, 1, len(actualPodResult.ContainerResults), "should be equal")
assert.EqualValues(t, &expectedSum, actualPodResult.Summary)
@@ -94,7 +89,7 @@ func TestInvalidIPCPod(t *testing.T) {
expectedSum := ResultSummary{
Totals: CountSummary{
Successes: uint(7),
Successes: uint(3),
Warnings: uint(0),
Errors: uint(1),
},
@@ -105,23 +100,18 @@ func TestInvalidIPCPod(t *testing.T) {
Warnings: uint(0),
Errors: uint(0),
}
expectedSum.ByCategory["Resources"] = &CountSummary{
Successes: uint(4),
Warnings: uint(0),
Errors: uint(0),
}
expectedSum.ByCategory["Security"] = &CountSummary{
Successes: uint(1),
Warnings: uint(0),
Errors: uint(1),
}
expectedMessages := []*ResultMessage{
{Message: "Host IPC should not be configured", Type: "error", Category: "Security"},
{Message: "Host PID is not configured", Type: "success", Category: "Security"},
{Message: "Host network is not configured", Type: "success", Category: "Networking"},
{ID: "hostIPCSet", Message: "Host IPC should not be configured", Type: "error", Category: "Security"},
{ID: "hostPIDSet", Message: "Host PID is not configured", Type: "success", Category: "Security"},
{ID: "hostNetworkSet", Message: "Host network is not configured", Type: "success", Category: "Networking"},
}
actualPodResult := ValidatePod(c, &pod.Spec)
actualPodResult := ValidatePod(c, &pod.Spec, "", conf.Deployments)
assert.Equal(t, 1, len(actualPodResult.ContainerResults), "should be equal")
assert.EqualValues(t, &expectedSum, actualPodResult.Summary)
@@ -147,7 +137,7 @@ func TestInvalidNeworkPod(t *testing.T) {
expectedSum := ResultSummary{
Totals: CountSummary{
Successes: uint(7),
Successes: uint(3),
Warnings: uint(1),
Errors: uint(0),
},
@@ -159,12 +149,6 @@ func TestInvalidNeworkPod(t *testing.T) {
Errors: uint(0),
}
expectedSum.ByCategory["Resources"] = &CountSummary{
Successes: uint(4),
Warnings: uint(0),
Errors: uint(0),
}
expectedSum.ByCategory["Security"] = &CountSummary{
Successes: uint(2),
Warnings: uint(0),
@@ -172,12 +156,12 @@ func TestInvalidNeworkPod(t *testing.T) {
}
expectedMessages := []*ResultMessage{
{Message: "Host network should not be configured", Type: "warning", Category: "Networking"},
{Message: "Host IPC is not configured", Type: "success", Category: "Security"},
{Message: "Host PID is not configured", Type: "success", Category: "Security"},
{ID: "hostNetworkSet", Message: "Host network should not be configured", Type: "warning", Category: "Networking"},
{ID: "hostIPCSet", Message: "Host IPC is not configured", Type: "success", Category: "Security"},
{ID: "hostPIDSet", Message: "Host PID is not configured", Type: "success", Category: "Security"},
}
actualPodResult := ValidatePod(c, &pod.Spec)
actualPodResult := ValidatePod(c, &pod.Spec, "", conf.Deployments)
assert.Equal(t, 1, len(actualPodResult.ContainerResults), "should be equal")
assert.EqualValues(t, &expectedSum, actualPodResult.Summary)
@@ -203,7 +187,7 @@ func TestInvalidPIDPod(t *testing.T) {
expectedSum := ResultSummary{
Totals: CountSummary{
Successes: uint(7),
Successes: uint(3),
Warnings: uint(0),
Errors: uint(1),
},
@@ -214,11 +198,6 @@ func TestInvalidPIDPod(t *testing.T) {
Warnings: uint(0),
Errors: uint(0),
}
expectedSum.ByCategory["Resources"] = &CountSummary{
Successes: uint(4),
Warnings: uint(0),
Errors: uint(0),
}
expectedSum.ByCategory["Security"] = &CountSummary{
Successes: uint(1),
Warnings: uint(0),
@@ -226,12 +205,65 @@ func TestInvalidPIDPod(t *testing.T) {
}
expectedMessages := []*ResultMessage{
{Message: "Host PID should not be configured", Type: "error", Category: "Security"},
{Message: "Host IPC is not configured", Type: "success", Category: "Security"},
{Message: "Host network is not configured", Type: "success", Category: "Networking"},
{ID: "hostPIDSet", Message: "Host PID should not be configured", Type: "error", Category: "Security"},
{ID: "hostIPCSet", Message: "Host IPC is not configured", Type: "success", Category: "Security"},
{ID: "hostNetworkSet", Message: "Host network is not configured", Type: "success", Category: "Networking"},
}
actualPodResult := ValidatePod(c, &pod.Spec)
actualPodResult := ValidatePod(c, &pod.Spec, "", conf.Deployments)
assert.Equal(t, 1, len(actualPodResult.ContainerResults), "should be equal")
assert.EqualValues(t, &expectedSum, actualPodResult.Summary)
assert.EqualValues(t, expectedMessages, actualPodResult.Messages)
}
func TestExemption(t *testing.T) {
c := conf.Configuration{
Security: conf.Security{
HostIPCSet: conf.SeverityError,
HostPIDSet: conf.SeverityError,
},
Networking: conf.Networking{
HostNetworkSet: conf.SeverityWarning,
HostPortSet: conf.SeverityError,
},
Exemptions: []conf.Exemption{
conf.Exemption{
Rules: []string{"hostIPCSet"},
ControllerNames: []string{"foo"},
},
},
}
k8s := test.SetupTestAPI()
k8s = test.SetupAddControllers(k8s, "test")
pod := test.MockPod()
pod.Spec.HostIPC = true
expectedSum := ResultSummary{
Totals: CountSummary{
Successes: uint(3),
Warnings: uint(0),
Errors: uint(0),
},
ByCategory: make(map[string]*CountSummary),
}
expectedSum.ByCategory["Networking"] = &CountSummary{
Successes: uint(2),
Warnings: uint(0),
Errors: uint(0),
}
expectedSum.ByCategory["Security"] = &CountSummary{
Successes: uint(1),
Warnings: uint(0),
Errors: uint(0),
}
expectedMessages := []*ResultMessage{
{ID: "hostPIDSet", Message: "Host PID is not configured", Type: "success", Category: "Security"},
{ID: "hostNetworkSet", Message: "Host network is not configured", Type: "success", Category: "Networking"},
}
actualPodResult := ValidatePod(c, &pod.Spec, "foo", conf.Deployments)
assert.Equal(t, 1, len(actualPodResult.ContainerResults), "should be equal")
assert.EqualValues(t, &expectedSum, actualPodResult.Summary)
+9 -6
View File
@@ -71,34 +71,37 @@ func (rv *ResourceValidation) addMessage(message ResultMessage) {
}
}
func (rv *ResourceValidation) addFailure(message string, severity conf.Severity, category string) {
func (rv *ResourceValidation) addFailure(message string, severity conf.Severity, category string, id string) {
if severity == conf.SeverityError {
rv.addError(message, category)
rv.addError(message, category, id)
} else if severity == conf.SeverityWarning {
rv.addWarning(message, category)
rv.addWarning(message, category, id)
} else {
logrus.Errorf("Invalid severity: %s", severity)
}
}
func (rv *ResourceValidation) addError(message string, category string) {
func (rv *ResourceValidation) addError(message string, category string, id string) {
rv.Errors = append(rv.Errors, &ResultMessage{
ID: id,
Message: message,
Type: MessageTypeError,
Category: category,
})
}
func (rv *ResourceValidation) addWarning(message string, category string) {
func (rv *ResourceValidation) addWarning(message string, category string, id string) {
rv.Warnings = append(rv.Warnings, &ResultMessage{
ID: id,
Message: message,
Type: MessageTypeWarning,
Category: category,
})
}
func (rv *ResourceValidation) addSuccess(message string, category string) {
func (rv *ResourceValidation) addSuccess(message string, category string, id string) {
rv.Successes = append(rv.Successes, &ResultMessage{
ID: id,
Message: message,
Type: MessageTypeSuccess,
Category: category,
+71
View File
@@ -15,13 +15,52 @@
package validator
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"os"
"github.com/sirupsen/logrus"
"github.com/fairwindsops/polaris/pkg/config"
conf "github.com/fairwindsops/polaris/pkg/config"
corev1 "k8s.io/api/core/v1"
apiMachineryYAML "k8s.io/apimachinery/pkg/util/yaml"
)
const (
// PolarisOutputVersion is the version of the current output structure
PolarisOutputVersion = "0.3"
)
// AuditData contains all the data from a full Polaris audit
type AuditData struct {
PolarisOutputVersion string
AuditTime string
SourceType string
SourceName string
DisplayName string
ClusterSummary ClusterSummary
NamespacedResults NamespacedResults
}
// ClusterSummary contains Polaris results as well as some high-level stats
type ClusterSummary struct {
Results ResultSummary
Version string
Nodes int
Pods int
Namespaces int
Deployments int
StatefulSets int
DaemonSets int
Jobs int
CronJobs int
ReplicationControllers int
Score uint
}
// MessageType represents the type of Message
type MessageType string
@@ -193,7 +232,39 @@ type PodResult struct {
// ResultMessage contains a message and a type indicator (success, warning, or error).
type ResultMessage struct {
ID string
Message string
Type MessageType
Category string
}
// ReadAuditFromFile reads the data from a past audit stored in a JSON or YAML file.
func ReadAuditFromFile(fileName string) AuditData {
auditData := AuditData{}
oldFileBytes, err := ioutil.ReadFile(fileName)
if err != nil {
logrus.Errorf("Unable to read contents of loaded file: %v", err)
os.Exit(1)
}
auditData, err = ParseAudit(oldFileBytes)
if err != nil {
logrus.Errorf("Error parsing file contents into auditData: %v", err)
os.Exit(1)
}
return auditData
}
// ParseAudit decodes either a YAML or JSON file and returns AuditData.
func ParseAudit(oldFileBytes []byte) (AuditData, error) {
reader := bytes.NewReader(oldFileBytes)
conf := AuditData{}
d := apiMachineryYAML.NewYAMLOrJSONDecoder(reader, 4096)
for {
if err := d.Decode(&conf); err != nil {
if err == io.EOF {
return conf, nil
}
return conf, fmt.Errorf("Decoding config failed: %v", err)
}
}
}
+12 -11
View File
@@ -20,7 +20,7 @@ import (
"net/http"
"os"
conf "github.com/fairwindsops/polaris/pkg/config"
"github.com/fairwindsops/polaris/pkg/config"
validator "github.com/fairwindsops/polaris/pkg/validator"
"github.com/fairwindsops/polaris/pkg/validator/controllers"
"github.com/sirupsen/logrus"
@@ -42,7 +42,7 @@ import (
type Validator struct {
client client.Client
decoder types.Decoder
Config conf.Configuration
Config config.Configuration
}
var _ inject.Client = &Validator{}
@@ -80,8 +80,9 @@ func NewWebhook(name string, mgr manager.Manager, validator Validator, apiType r
if err != nil {
logrus.Errorf("Error building webhook: %v", err)
os.Exit(1)
} else {
logrus.Info(name + " webhook started")
}
return webhook
}
@@ -93,7 +94,7 @@ func (v *Validator) Handle(ctx context.Context, req types.Request) types.Respons
if req.AdmissionRequest.Kind.Kind == "Pod" {
pod := corev1.Pod{}
err = v.decoder.Decode(req, &pod)
podResult = validator.ValidatePod(v.Config, &pod.Spec)
podResult = validator.ValidatePod(v.Config, &pod.Spec, "", config.Unsupported)
} else {
var controller controllers.Interface
if yes := v.Config.CheckIfKindIsConfiguredForValidation(req.AdmissionRequest.Kind.Kind); !yes {
@@ -102,7 +103,7 @@ func (v *Validator) Handle(ctx context.Context, req types.Request) types.Respons
}
// We should never hit this case unless something is misconfiured in CheckIfKindIsConfiguredForValidation
controllerType, err := conf.GetSupportedControllerFromString(req.AdmissionRequest.Kind.Kind)
controllerType, err := config.GetSupportedControllerFromString(req.AdmissionRequest.Kind.Kind)
if err != nil {
msg := fmt.Errorf("Unexpected error occurred. Expected Kind to be a supported type (%s)", req.AdmissionRequest.Kind.Kind)
logrus.Error(msg)
@@ -112,27 +113,27 @@ func (v *Validator) Handle(ctx context.Context, req types.Request) types.Respons
// For each type, perform the scan
// TODO: This isn't really that elegant due to the decoder and NewXXXController setup :( could use love
switch controllerType {
case conf.Deployments:
case config.Deployments:
deploy := appsv1.Deployment{}
err = v.decoder.Decode(req, &deploy)
controller = controllers.NewDeploymentController(deploy)
case conf.StatefulSets:
case config.StatefulSets:
statefulSet := appsv1.StatefulSet{}
err = v.decoder.Decode(req, &statefulSet)
controller = controllers.NewStatefulSetController(statefulSet)
case conf.DaemonSets:
case config.DaemonSets:
daemonSet := appsv1.DaemonSet{}
err = v.decoder.Decode(req, &daemonSet)
controller = controllers.NewDaemonSetController(daemonSet)
case conf.Jobs:
case config.Jobs:
job := batchv1.Job{}
err = v.decoder.Decode(req, &job)
controller = controllers.NewJobController(job)
case conf.CronJobs:
case config.CronJobs:
cronJob := batchv1beta1.CronJob{}
err = v.decoder.Decode(req, &cronJob)
controller = controllers.NewCronJobController(cronJob)
case conf.ReplicationControllers:
case config.ReplicationControllers:
replicationController := corev1.ReplicationController{}
err = v.decoder.Decode(req, &replicationController)
controller = controllers.NewReplicationControllerController(replicationController)
+21 -14
View File
@@ -11,7 +11,8 @@ import (
"k8s.io/client-go/kubernetes/fake"
)
func mockContainer(name string) corev1.Container {
// MockContainer creates a container object
func MockContainer(name string) corev1.Container {
c := corev1.Container{
Name: name,
}
@@ -20,7 +21,7 @@ func mockContainer(name string) corev1.Container {
// MockPod creates a pod object.
func MockPod() corev1.PodTemplateSpec {
c1 := mockContainer("test")
c1 := MockContainer("test")
p := corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
@@ -31,7 +32,8 @@ func MockPod() corev1.PodTemplateSpec {
return p
}
func mockDeploy() appsv1.Deployment {
// MockDeploy creates a Deployment object.
func MockDeploy() appsv1.Deployment {
p := MockPod()
d := appsv1.Deployment{
Spec: appsv1.DeploymentSpec{
@@ -41,7 +43,8 @@ func mockDeploy() appsv1.Deployment {
return d
}
func mockStatefulSet() appsv1.StatefulSet {
// MockStatefulSet creates a StatefulSet object.
func MockStatefulSet() appsv1.StatefulSet {
p := MockPod()
s := appsv1.StatefulSet{
Spec: appsv1.StatefulSetSpec{
@@ -51,7 +54,8 @@ func mockStatefulSet() appsv1.StatefulSet {
return s
}
func mockDaemonSet() appsv1.DaemonSet {
// MockDaemonSet creates a DaemonSet object.
func MockDaemonSet() appsv1.DaemonSet {
return appsv1.DaemonSet{
Spec: appsv1.DaemonSetSpec{
Template: MockPod(),
@@ -59,7 +63,8 @@ func mockDaemonSet() appsv1.DaemonSet {
}
}
func mockJob() batchv1.Job {
// MockJob creates a Job object.
func MockJob() batchv1.Job {
return batchv1.Job{
Spec: batchv1.JobSpec{
Template: MockPod(),
@@ -67,7 +72,8 @@ func mockJob() batchv1.Job {
}
}
func mockCronJob() batchv1beta1.CronJob {
// MockCronJob creates a CronJob object.
func MockCronJob() batchv1beta1.CronJob {
return batchv1beta1.CronJob{
Spec: batchv1beta1.CronJobSpec{
JobTemplate: batchv1beta1.JobTemplateSpec{
@@ -79,7 +85,8 @@ func mockCronJob() batchv1beta1.CronJob {
}
}
func mockReplicationController() corev1.ReplicationController {
// MockReplicationController creates a ReplicationController object.
func MockReplicationController() corev1.ReplicationController {
p := MockPod()
return corev1.ReplicationController{
Spec: corev1.ReplicationControllerSpec{
@@ -95,32 +102,32 @@ func SetupTestAPI() kubernetes.Interface {
// SetupAddControllers creates mock controllers and adds them to the test clientset.
func SetupAddControllers(k kubernetes.Interface, namespace string) kubernetes.Interface {
d1 := mockDeploy()
d1 := MockDeploy()
if _, err := k.AppsV1().Deployments(namespace).Create(&d1); err != nil {
fmt.Println(err)
}
s1 := mockStatefulSet()
s1 := MockStatefulSet()
if _, err := k.AppsV1().StatefulSets(namespace).Create(&s1); err != nil {
fmt.Println(err)
}
ds1 := mockDaemonSet()
ds1 := MockDaemonSet()
if _, err := k.AppsV1().DaemonSets(namespace).Create(&ds1); err != nil {
fmt.Println(err)
}
j1 := mockJob()
j1 := MockJob()
if _, err := k.BatchV1().Jobs(namespace).Create(&j1); err != nil {
fmt.Println(err)
}
cj1 := mockCronJob()
cj1 := MockCronJob()
if _, err := k.BatchV1beta1().CronJobs(namespace).Create(&cj1); err != nil {
fmt.Println(err)
}
rc1 := mockReplicationController()
rc1 := MockReplicationController()
if _, err := k.CoreV1().ReplicationControllers(namespace).Create(&rc1); err != nil {
fmt.Println(err)
}
+37
View File
@@ -0,0 +1,37 @@
sed -ri "s|'(quay.io/fairwinds/polaris:).+'|'\1${CIRCLE_SHA1}'|" ./deploy/dashboard.yaml
function check_dashboard_is_ready() {
local timeout_epoch
timeout_epoch=$(date -d "+2 minutes" +%s)
echo "Waiting for dashboard to be ready"
while ! kubectl get pods -n polaris | grep -E "dashboard.*1/1.*Running"; do
check_timeout "${timeout_epoch}"
echo -n "."
done
echo "Dashboard Running!"
}
function check_timeout() {
local timeout_epoch="${1}"
if [[ "$(date +%s)" -ge "${timeout_epoch}" ]]; then
echo -e "Timeout hit waiting for readiness: exiting"
grab_logs
clean_up
exit 1
fi
}
kubectl apply -f ./deploy/dashboard.yaml &>/dev/null
check_dashboard_is_ready
kubectl port-forward --namespace polaris svc/polaris-dashboard 3000:80 &
sleep 30
curl -f http://localhost:3000 > /dev/null
curl -f http://localhost:3000/health > /dev/null
curl -f http://localhost:3000/favicon.ico > /dev/null
curl -f http://localhost:3000/static/css/main.css > /dev/null
curl -f http://localhost:3000/results.json > /dev/null
curl -f http://localhost:3000/details/security > /dev/null
@@ -0,0 +1,24 @@
apiVersion: batch/v1beta1
kind: CronJob
metadata:
name: test
spec:
schedule: "*/1 * * * *"
jobTemplate:
spec:
template:
spec:
containers:
- name: test
image: busybox:uclibc
args:
- whoami
securityContext:
allowPrivilegeEscalation: true
privileged: false
readOnlyRootFilesystem: true
runAsNonRoot: true
capabilities:
drop:
- ALL
restartPolicy: OnFailure
@@ -0,0 +1,48 @@
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: fluentd-elasticsearch
namespace: kube-system
labels:
k8s-app: fluentd-logging
spec:
selector:
matchLabels:
name: fluentd-elasticsearch
template:
metadata:
labels:
name: fluentd-elasticsearch
spec:
tolerations:
- key: node-role.kubernetes.io/master
effect:
containers:
- name: fluentd-elasticsearch
image: gcr.io/fluentd-elasticsearch/fluentd:v2.5.1
resources:
requests:
cpu: 100m
volumeMounts:
- name: varlog
mountPath: /var/log
- name: varlibdockercontainers
mountPath: /var/lib/docker/containers
readOnly: true
securityContext:
allowPrivilegeEscalation: true
privileged: false
readOnlyRootFilesystem: true
runAsNonRoot: true
capabilities:
drop:
- ALL
terminationGracePeriodSeconds: 30
volumes:
- name: varlog
hostPath:
path: /var/log
- name: varlibdockercontainers
hostPath:
path: /var/lib/docker/containers
@@ -0,0 +1,29 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
labels:
app: nginx
spec:
replicas: 3
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.7.9
ports:
- containerPort: 80
securityContext:
allowPrivilegeEscalation: true
privileged: false
readOnlyRootFilesystem: true
runAsNonRoot: true
capabilities:
drop:
- ALL
+21
View File
@@ -0,0 +1,21 @@
apiVersion: batch/v1
kind: Job
metadata:
name: pi-with-ttl-2
spec:
ttlSecondsAfterFinished: 100
template:
spec:
containers:
- name: pi
image: perl:5.3
command: ["perl", "-Mbignum=bpi", "-wle", "print bpi(2000)"]
securityContext:
allowPrivilegeEscalation: true
privileged: false
readOnlyRootFilesystem: true
runAsNonRoot: true
capabilities:
drop:
- ALL
restartPolicy: Never
@@ -0,0 +1,27 @@
apiVersion: v1
kind: ReplicationController
metadata:
name: nginx
spec:
replicas: 3
selector:
app: nginx
template:
metadata:
name: nginx
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.7.9
ports:
- containerPort: 80
securityContext:
allowPrivilegeEscalation: true
privileged: false
readOnlyRootFilesystem: true
runAsNonRoot: true
capabilities:
drop:
- ALL
@@ -0,0 +1,56 @@
apiVersion: v1
kind: Service
metadata:
name: nginx
labels:
app: nginx
spec:
ports:
- port: 80
name: web
clusterIP: None
selector:
app: nginx
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: web
spec:
selector:
matchLabels:
app: nginx # has to match .spec.template.metadata.labels
serviceName: "nginx"
replicas: 3 # by default is 1
template:
metadata:
labels:
app: nginx # has to match .spec.selector.matchLabels
spec:
terminationGracePeriodSeconds: 10
containers:
- name: nginx
image: k8s.gcr.io/nginx-slim:0.8
ports:
- containerPort: 80
name: web
volumeMounts:
- name: www
mountPath: /usr/share/nginx/html
securityContext:
allowPrivilegeEscalation: true
privileged: false
readOnlyRootFilesystem: true
runAsNonRoot: true
capabilities:
drop:
- ALL
volumeClaimTemplates:
- metadata:
name: www
spec:
accessModes: [ "ReadWriteOnce" ]
storageClassName: "my-storage-class"
resources:
requests:
storage: 1Gi
@@ -0,0 +1,24 @@
apiVersion: batch/v1beta1
kind: CronJob
metadata:
name: test
spec:
schedule: "*/1 * * * *"
jobTemplate:
spec:
template:
spec:
containers:
- name: test
image: busybox:uclibc
args:
- whoami
securityContext:
allowPrivilegeEscalation: false
privileged: false
readOnlyRootFilesystem: true
runAsNonRoot: true
capabilities:
drop:
- ALL
restartPolicy: OnFailure
@@ -0,0 +1,48 @@
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: fluentd-elasticsearch
namespace: kube-system
labels:
k8s-app: fluentd-logging
spec:
selector:
matchLabels:
name: fluentd-elasticsearch
template:
metadata:
labels:
name: fluentd-elasticsearch
spec:
tolerations:
- key: node-role.kubernetes.io/master
effect:
containers:
- name: fluentd-elasticsearch
image: gcr.io/fluentd-elasticsearch/fluentd:v2.5.1
resources:
requests:
cpu: 100m
volumeMounts:
- name: varlog
mountPath: /var/log
- name: varlibdockercontainers
mountPath: /var/lib/docker/containers
readOnly: true
securityContext:
allowPrivilegeEscalation: false
privileged: false
readOnlyRootFilesystem: true
runAsNonRoot: true
capabilities:
drop:
- ALL
terminationGracePeriodSeconds: 30
volumes:
- name: varlog
hostPath:
path: /var/log
- name: varlibdockercontainers
hostPath:
path: /var/lib/docker/containers
@@ -0,0 +1,29 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
labels:
app: nginx
spec:
replicas: 3
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.7.9
ports:
- containerPort: 80
securityContext:
allowPrivilegeEscalation: false
privileged: false
readOnlyRootFilesystem: true
runAsNonRoot: true
capabilities:
drop:
- ALL
+21
View File
@@ -0,0 +1,21 @@
apiVersion: batch/v1
kind: Job
metadata:
name: pi-with-ttl
spec:
ttlSecondsAfterFinished: 100
template:
spec:
containers:
- name: pi
image: perl:5.3
command: ["perl", "-Mbignum=bpi", "-wle", "print bpi(2000)"]
securityContext:
allowPrivilegeEscalation: false
privileged: false
readOnlyRootFilesystem: true
runAsNonRoot: true
capabilities:
drop:
- ALL
restartPolicy: Never
@@ -0,0 +1,27 @@
apiVersion: v1
kind: ReplicationController
metadata:
name: nginx
spec:
replicas: 3
selector:
app: nginx
template:
metadata:
name: nginx
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.7.9
ports:
- containerPort: 80
securityContext:
allowPrivilegeEscalation: false
privileged: false
readOnlyRootFilesystem: true
runAsNonRoot: true
capabilities:
drop:
- ALL
@@ -0,0 +1,56 @@
apiVersion: v1
kind: Service
metadata:
name: nginx
labels:
app: nginx
spec:
ports:
- port: 80
name: web
clusterIP: None
selector:
app: nginx
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: web
spec:
selector:
matchLabels:
app: nginx # has to match .spec.template.metadata.labels
serviceName: "nginx"
replicas: 3 # by default is 1
template:
metadata:
labels:
app: nginx # has to match .spec.selector.matchLabels
spec:
terminationGracePeriodSeconds: 10
containers:
- name: nginx
image: k8s.gcr.io/nginx-slim:0.8
ports:
- containerPort: 80
name: web
volumeMounts:
- name: www
mountPath: /usr/share/nginx/html
securityContext:
allowPrivilegeEscalation: false
privileged: false
readOnlyRootFilesystem: true
runAsNonRoot: true
capabilities:
drop:
- ALL
volumeClaimTemplates:
- metadata:
name: www
spec:
accessModes: [ "ReadWriteOnce" ]
storageClassName: "my-storage-class"
resources:
requests:
storage: 1Gi
+93
View File
@@ -0,0 +1,93 @@
#!/bin/bash
set -e
#sed is replacing the polaris version with this commit sha so we are testing exactly this verison.
sed -ri "s|'(quay.io/fairwinds/polaris:).+'|'\1${CIRCLE_SHA1}'|" ./deploy/webhook.yaml
# Testing to ensure that the webhook starts up, allows a correct deployment to pass,
# and prevents a incorrectly formatted deployment.
function check_webhook_is_ready() {
# Get the epoch time in one minute from now
local timeout_epoch
# Reset another 2 minutes to wait for webhook
timeout_epoch=$(date -d "+2 minutes" +%s)
# loop until this fails (desired condition is we cannot apply this yaml doc, which means the webhook is working
echo "Waiting for webhook to be ready"
while ! kubectl get pods -n polaris | grep -E "webhook.*1/1.*Running"; do
check_timeout "${timeout_epoch}"
echo -n "."
done
echo "Webhook started!"
}
# Check if timeout is hit and exit if it is
function check_timeout() {
local timeout_epoch="${1}"
if [[ "$(date +%s)" -ge "${timeout_epoch}" ]]; then
echo -e "Timeout hit waiting for readiness: exiting"
grab_logs
clean_up
exit 1
fi
}
# Clean up all your stuff
function clean_up() {
# Clean up files you've installed (helps with local testing)
for filename in test/webhook_cases/*.yaml; do
# || true to avoid issues when we cannot delete
kubectl delete -f $filename &>/dev/null ||true
done
# Uninstall webhook and webhook config
kubectl delete validatingwebhookconfigurations polaris-webhook --wait=false &>/dev/null
kubectl -n polaris delete deploy -l app=polaris --wait=false &>/dev/null
}
function grab_logs() {
kubectl -n polaris get pods -oyaml -l app=polaris
kubectl -n polaris describe pods -l app=polaris
kubectl -n polaris logs -l app=polaris
}
# Install the webhook
kubectl apply -f ./deploy/webhook.yaml &> /dev/null
# wait for the webhook to come online
check_webhook_is_ready
sleep 30
# Webhook started, setting all tests as passed initially.
ALL_TESTS_PASSED=1
# Run tests against correctly configured objects
for filename in test/webhook_cases/passing_test.*.yaml; do
echo $filename
if ! kubectl apply -f $filename &> /dev/null; then
ALL_TESTS_PASSED=0
echo "Test Failed: Polaris prevented a deployment with no configuration issues."
fi
done
# Run tests against incorrectly configured objects
for filename in test/webhook_cases/failing_test.*.yaml; do
echo $filename
if kubectl apply -f $filename &> /dev/null; then
ALL_TESTS_PASSED=0
echo "Test Failed: Polaris should have prevented this deployment due to configuration issues."
fi
done
clean_up
#Verify that all the tests passed.
if [ $ALL_TESTS_PASSED -eq 1 ]; then
echo "Tests Passed."
else
echo "Tests Failed."
exit 1
fi