Compare commits

..
44 Commits
Author SHA1 Message Date
Nick Huanca f03f97ab07 bump version (#184) 2019-08-01 09:55:40 -06:00
Nick Huanca 75f70352ba Additional Pod Controller Scans (#166)
**Changes**

- Refactored the way controllers work to be an interface
- Added configurable controllers to include in scans
- Added daemonsets, jobs and cronjobs in scans
- Added `ReplicationController` type controllers to the supported list
- Adjusted logic for failed YAML parsing to bubble up errors
- Added better logic for calculating summaries on cluster wide results
- Relocated responsibilities for counting types into validators vs spreading it around more packages
- Fixed bug where cronjob parsing was using wrong KIND
- Added fixtures for mocking new controller types
- Added example yamls to test scanning files
- Added functions to NamespacedResult(s) to reduce code complexity deep set iterations
- Refactored how results get added to namespacedresults so adding more later is easier
- Minor signature changes for interface implementing structs for controllers
2019-07-31 15:56:27 -06:00
Bobby Brennan 4aa360a933 Remove swapfile 2019-07-31 14:26:18 -04:00
Will Ledingham dce24a0a34 Merge pull request #181 from FairwindsOps/wl/config-URL
Wl/config url
2019-07-31 10:45:21 -04:00
Will Ledingham fae7a9fc17 Merge branch 'master' into wl/config-URL 2019-07-30 17:02:02 -04:00
Will Ledingham 79b0a00d5c more edits 2019-07-30 16:59:11 -04:00
Will Ledingham e4fe5ab085 more edits 2019-07-30 16:45:49 -04:00
Will Ledingham 2049698c8e small edit 2019-07-30 16:37:24 -04:00
Bobby Brennan 20bd32afb6 Rename ReactiveOps to Fairwinds (#180)
* Rename ReactiveOps to Fairwinds

* Rename ReactiveOps to Fairwinds
2019-07-30 15:29:09 -04:00
Will Ledingham c132f7ee51 more edits 2019-07-29 13:29:53 -04:00
Will Ledingham 10025764cd altered to fix tests on CircleCl. 2019-07-29 11:10:59 -04:00
Will Ledingham c8c0e07a22 altered to fix tests on CircleCl. 2019-07-29 11:09:33 -04:00
Will Ledingham b55f4717b1 typo 2019-07-29 10:44:01 -04:00
Will Ledingham f493c380fc Merge branch 'wl/config-URL' of https://github.com/FairwindsOps/polaris into wl/config-URL 2019-07-29 10:34:56 -04:00
Will Ledingham baa652197a Fixed error handling in ParseFile, added a test for parseFile where no Http server is configured 2019-07-29 10:33:27 -04:00
Will Ledingham a6525fc807 Merge branch 'master' into wl/config-URL 2019-07-25 16:37:30 -04:00
Will Ledingham e01c39230a cleaned up unnecessary comments 2019-07-25 16:34:59 -04:00
Will Ledingham 7b8b5fee4d expanded ParseFile function to include URL's and added a test for it. 2019-07-25 16:31:17 -04:00
Will Ledingham 819b77ae1a Pod tests (#177)
* add pod test

* add pod test 2

* update tests

* Added pod tests for configured Host IPC, Host PID, and Host Newtowrk.

* Fixed expected vs. actual mixup
2019-07-25 09:31:37 -04:00
Bobby Brennan edccf88ccd update fairwinds logo (#179) 2019-07-25 08:52:57 -04:00
Bobby Brennan 7f42841572 update to v0.3.1 (#176) 2019-07-23 14:06:13 -04:00
Bobby Brennan b5a180d3de update logo, copyright (#175) 2019-07-23 12:47:02 -04:00
Bobby Brennan 69efa2a0a3 Fix CircleCI badge (#174) 2019-07-22 18:47:23 -04:00
Kim Schlesinger 7f4029386d Merge pull request #171 from FairwindsOps/ks/issue-templates
Add issue templates
2019-07-19 14:30:17 -06:00
Bobby Brennan 9f7ed8214e change domain from reactiveops.com to fairwinds.com (#173) 2019-07-19 16:16:59 -04:00
Kim Schlesinger 5e67812d5a Update CONTRIBUTING.md 2019-07-19 14:13:17 -06:00
Kim Schlesinger 821de5fa8f Update other.md 2019-07-19 14:08:09 -06:00
Kim Schlesinger 20bf86ec12 Update bug_report.md 2019-07-17 14:47:31 -06:00
Kim Schlesinger 87370a4fbc Add issue templates
I've added issues templates for bug reports, feature requests and other as a response to feedback left on this PR: https://github.com/FairwindsOps/polaris/pull/168. 

The goal of these templates is to better organize our issues, as well as encourage folks who are filing a bug report to check their version of polaris and update when necessary.
2019-07-17 14:34:07 -06:00
Bobby Brennan 7b0e0e7f15 update roadmap (#170) 2019-07-17 08:37:52 -04:00
Bobby Brennan 962494e47a update to 0.3.0 (#162)
* update to 0.3.0

* disable test_k8s for update-version branches

* update README
2019-07-11 11:13:05 -04:00
Bobby Brennan c816e60441 Rename org in GitHub API call (#160) 2019-07-10 12:44:26 -04:00
Bobby Brennan d46fd6215a Update README.md (#159) 2019-07-10 09:48:07 -04:00
Bobby Brennan 7bcd01e6be add deploy configurations to source control (#156)
* add deploy configurations to source control

* add KIND testing

* add KIND testing

* updates based on feedback
2019-07-01 08:08:56 -04:00
Bobby Brennan 20a6028145 add option to fail audit below a certain score (#157)
update README
2019-06-27 09:40:16 -04:00
Kim Schlesinger f784c48217 Merge pull request #155 from reactiveops/ks/add-exit-code-flag
Add exit code flag
2019-06-24 16:03:05 -05:00
kimschles 9b755b8b93 move setExitCode logic to bottom of function 2019-06-24 13:55:07 -06:00
kimschles 29261ed8bf rename flag and change logrus error to info 2019-06-24 10:49:57 -06:00
kimschles 1b5557d6ed add functionality for --exit-code flag 2019-06-24 09:21:05 -06:00
kimschles 3fd3c04429 [WIP] add exit-code flag 2019-06-21 15:54:44 -06:00
Nick Huanca 5b9257b4ce Adjusting instructions for clarity (#152) 2019-06-21 12:36:55 -06:00
Nick Huanca d8d0783f29 Release 0.2.1 (#151)
* Added release version and updated readme
2019-06-21 12:09:34 -06:00
Nick Huanca 4c7429efbc #146 Fixing Container Security Context Logic (#149)
* Fixing Container Security Context Logic

Kubernetes rationalizes Container Security Context in conjunction with the
Pod Spec Security Context. In this scenario you can 'leave out' certain
security context settings and rely on the pod spec definition to still
set these settings for you. The RunAsNonRoot setting originally only checked
to see if the value was set at the container level, vs also checking if it
was enabled at the pod level.

I have attached the container's parent pod spec to the container validate
struct in case any other things like this arise in the future.

I have also refactored the logic for validating bool pointers, since these
can be tricky, if you want to avoid dereferences pointer issues.

Changes:
- Added parent pod spec of container to validate certain settings which affect container spec
- Refactored the logic statements for validating bool pointers (used helpers)
- Added tests for this pod.container.securityContext condition
2019-06-18 11:04:38 -06:00
Bobby Brennan 73727bd9d8 Fix image update for version tags (#150) 2019-06-14 12:16:02 -06:00
63 changed files with 2597 additions and 435 deletions
+5
View File
@@ -3,3 +3,8 @@ DOCKERFILE='Dockerfile'
EXTERNAL_REGISTRY_BASE_DOMAIN=quay.io
REPOSITORY_NAME=reactiveops/polaris
DOCKERTAG=${EXTERNAL_REGISTRY_BASE_DOMAIN}/${REPOSITORY_NAME}
if [[ -n $CI_TAG ]]; then
ADDITIONAL_DOCKER_TAG_VERSIONS=()
ADDITIONAL_DOCKER_TAG_VERSIONS+=(`echo $CI_TAG | sed -e 's/\(\w\+\)\..*$/\1/'`)
ADDITIONAL_DOCKER_TAG_VERSIONS+=(`echo $CI_TAG | sed -e 's/\(\w\+\.\w\+\)\..*$/\1/'`)
fi
+78 -28
View File
@@ -1,4 +1,9 @@
version: 2
version: 2.1
executors:
vm:
machine:
enabled: true
references:
set_environment_variables: &set_environment_variables
@@ -10,21 +15,33 @@ references:
echo 'export CI_BUILD_NUM=$CIRCLE_BUILD_NUM' >> ${BASH_ENV}
echo 'export CI_TAG=$CIRCLE_TAG' >> ${BASH_ENV}
echo 'export PUSH_ALL_VERSION_TAGS=true' >> ${BASH_ENV}
docker_build_and_push: &docker_build_and_push
install_k8s: &install_k8s
run:
name: Docker login, build, and push
name: Install K8s
command: |
docker-pull -f .circleci/build.config
docker-build -f .circleci/build.config
if [[ -n $CIRCLE_TAG ]]; then
export ADDITIONAL_DOCKER_TAG_VERSIONS=(`echo $CIRCLE_TAG | sed -e 's/\(\w\+\)\..*$/\1/'`)
fi
if [[ -z $CIRCLE_PR_NUMBER ]]; then
docker login quay.io -u="reactiveops+circleci" -p="${quay_token}"
docker-push -f .circleci/build.config
else
echo "Skipping docker push for forked PR"
fi
echo "Installing git and jq"
sudo apt-get install -yqq jq git
echo "Installing KIND"
curl -sLO https://github.com/kubernetes-sigs/kind/releases/download/0.2.1/kind-linux-amd64
chmod 0755 kind-linux-amd64
sudo mv kind-linux-amd64 /usr/local/bin/kind
kind version
echo "Installing Kubectl"
curl -sLO https://storage.googleapis.com/kubernetes-release/release/v1.12.7/bin/linux/amd64/kubectl
chmod 0755 kubectl
sudo mv kubectl /usr/local/bin/
kubectl version --client
echo "Creating Kubernetes Cluster with Kind"
kind create cluster --wait=90s
docker ps -a
echo "Setting up kubecfg"
cp $(kind get kubeconfig-path --name=kind) ~/.kube/config
kubectl version
# Test scripts
update_coverage: &update_coverage
@@ -37,7 +54,7 @@ references:
else
echo "Skipping coverage for forked PR"
fi
test_dashboard: &test_dashboard
test_binary_dashboard: &test_binary_dashboard
run:
name: Test Dashboard
command: |
@@ -49,6 +66,22 @@ references:
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
run:
name: Test Dashboard
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
# Release scripts
install_goreleaser: &install_goreleaser
@@ -59,20 +92,26 @@ references:
echo "8dbad6683d6fc9367e637e6eed8e01a0d63c9660 goreleaser.deb" | sha1sum -c
sudo dpkg -i goreleaser.deb
rm goreleaser.deb
docker_build_and_push: &docker_build_and_push
run:
name: Docker login, build, and push
command: |
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-push -f .circleci/build.config
else
echo "Skipping docker push for forked PR"
fi
release_deploy_configs: &release_deploy_configs
run:
name: Release deploy configs
command: |
git clone --branch polaris-latest https://github.com/reactiveops/charts
mkdir deploy
helm template ./charts/stable/polaris --name polaris --namespace polaris --set templateOnly=true > deploy/dashboard.yaml
helm template ./charts/stable/polaris --name polaris --namespace polaris --set templateOnly=true --set webhook.enable=true --set dashboard.enable=false > deploy/webhook.yaml
upload_url=$(curl --silent https://api.github.com/repos/reactiveops/polaris/releases/latest | grep upload_url)
upload_url=$(curl --silent https://api.github.com/repos/FairwindsOps/polaris/releases/latest | grep upload_url)
upload_url=$(echo $upload_url | sed -e 's/.*\(https.*\){.*$/\1/')
curl -X POST "$upload_url?name=dashboard.yaml" --data-binary "@./deploy/dashboard.yaml" -H "Authorization: Bearer $GITHUB_TOKEN" -H "Content-Type: application/x-yaml"
curl -X POST "$upload_url?name=webhook.yaml" --data-binary "@./deploy/webhook.yaml" -H "Authorization: Bearer $GITHUB_TOKEN" -H "Content-Type: application/x-yaml"
rm -rf ./charts
rm -rf ./deploy
jobs:
build:
@@ -84,21 +123,29 @@ jobs:
- *set_environment_variables
- *docker_build_and_push
test_k8s:
working_directory: ~/polaris
resource_class: medium
executor: vm
steps:
- checkout
- *install_k8s
- *test_kube_dashboard
test:
working_directory: /go/src/github.com/reactiveops/polaris/
working_directory: /go/src/github.com/fairwindsops/polaris/
docker:
- image: circleci/golang:1.12
steps:
- checkout
- 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
- *update_coverage
- *test_dashboard
- *test_binary_dashboard
release_binary:
working_directory: /go/src/github.com/reactiveops/polaris/
working_directory: /go/src/github.com/fairwindsops/polaris/
docker:
- image: circleci/golang:1.12
steps:
@@ -109,9 +156,10 @@ jobs:
- run: go get -u github.com/gobuffalo/packr/v2/packr2
- run: packr2
- run: goreleaser
- *release_deploy_configs
release_images:
working_directory: /go/src/github.com/reactiveops/polaris/
working_directory: /go/src/github.com/fairwindsops/polaris/
docker:
- image: quay.io/reactiveops/ci-images:v8.0-stretch
steps:
@@ -119,7 +167,6 @@ jobs:
- setup_remote_docker
- *set_environment_variables
- *docker_build_and_push
- *release_deploy_configs
workflows:
version: 2
@@ -134,6 +181,9 @@ workflows:
filters:
tags:
only: /^testing-.*/
- test_k8s:
requires:
- build
release:
jobs:
- release_binary:
+58
View File
@@ -0,0 +1,58 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
---
name: Bug report
about: Let us know about an unexpected error, a crash, or an incorrect behavior.
---
### Installation Process
<!---
Did you install polaris on your local machine, or in a kubernetes cluster? Did you use kubectl, helm, brew, a GitHub release, or the source code? Let us know below.
-->
### Polaris Version
<!---
If you are using polaris locally, run `polaris --version` to show the version, and paste the result between the ``` marks below.
If you are not running the latest version of Polaris, please try upgrading because your issue may have already been fixed.
-->
```
...
```
### Expected Behavior
<!--
What should have happened?
-->
### Actual Behavior
<!--
What actually happened?
-->
### Steps to Reproduce
<!--
Please list the full steps required to reproduce the issue, for example:
1. `polaris --audit --output-format score`
-->
### Additional Context
<!--
Are there anything atypical about your situation that we should know?
-->
### References
<!--
Are there any other GitHub issues (open or closed) or Pull Requests that should be linked here? For example:
- #6017
-->
+20
View File
@@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
+10
View File
@@ -0,0 +1,10 @@
---
name: Other
about: For misc. tasks like research or continued conversation
title: ''
labels: ''
assignees: ''
---
+1 -1
View File
@@ -21,7 +21,7 @@ changelog:
- '^test:'
brew:
github:
owner: reactiveops
owner: FairwindsOps
name: homebrew-tap
folder: Formula
description: Open Source Best Practices for Kubernetes
+21 -7
View File
@@ -1,3 +1,17 @@
# x.x.x (next release)
# 0.4.0
* Added additional Pod Controllers to scan PodSpec (`jobs`, `cronjobs`, `daemonsets`, `replicationcontrollers`)
# 0.3.1
* Changed dashboard branding to refer to new org name Fairwinds
# 0.3.0
* Added `--set-exit-code-on-error` and `--set-exit-code-below-score` flags to better support CI/CD
# 0.2.1
* [Fix](https://github.com/FairwindsOps/polaris/issues/146): Fixed logic on RunAsNonRoot check to incorporate settings in podSpec
# 0.2.0
* Added `--output-format` flag for better CI/CD support
* Added `--display-name` flag
@@ -5,23 +19,23 @@
* Show error message if no kubeconfig is set
# 0.1.5
* [Fix](https://github.com/reactiveops/polaris/issues/125): ignore limits/requests for initContainers
* [Fix](https://github.com/reactiveops/polaris/issues/132): support custom base path
* [Fix](https://github.com/FairwindsOps/polaris/issues/125): ignore limits/requests for initContainers
* [Fix](https://github.com/FairwindsOps/polaris/issues/132): support custom base path
# 0.1.4
* [Fix](https://github.com/reactiveops/polaris/issues/116): details pages getting template errors
* [Fix](https://github.com/reactiveops/polaris/issues/114): support all auth providers
* [Fix](https://github.com/reactiveops/polaris/issues/112): Ignore readiness probe for initContainers
* [Fix](https://github.com/FairwindsOps/polaris/issues/116): details pages getting template errors
* [Fix](https://github.com/FairwindsOps/polaris/issues/114): support all auth providers
* [Fix](https://github.com/FairwindsOps/polaris/issues/112): Ignore readiness probe for initContainers
# 0.1.3
* [Fix](https://github.com/reactiveops/polaris/issues/109): dashboard not updating when running persistently
* [Fix](https://github.com/FairwindsOps/polaris/issues/109): dashboard not updating when running persistently
# 0.1.2
* Stored all third-party assets (e.g. Charts.js) to local files to support offline dashboard viewing
* Fix: custom configs in `ConfigMap` not respected
# 0.1.1
* [Fix](https://github.com/reactiveops/polaris/issues/93): missing `config.yaml` and dashboard assets in binary releases
* [Fix](https://github.com/FairwindsOps/polaris/issues/93): missing `config.yaml` and dashboard assets in binary releases
* Added some tests and better error handling
# 0.1.0
+44 -28
View File
@@ -7,7 +7,7 @@ Issues, whether bugs, tasks, or feature requests are essential for keeping Polar
This project adheres to a [code of conduct](CODE_OF_CONDUCT.md). Please review this document before contributing to this project.
## Sign the CLA
Before you can contribute, you will need to sign the [Contributor License Agreement](https://cla-assistant.io/reactiveops/polaris).
Before you can contribute, you will need to sign the [Contributor License Agreement](https://cla-assistant.io/fairwinds/polaris).
## Project Structure
@@ -15,7 +15,7 @@ Polaris is built on top of [controller-runtime](https://github.com/kubernetes-si
## Getting Started
We label issues with the ["good first issue" tag](https://github.com/reactiveops/polaris/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) if we believe they'll be a good starting point for new contributors. If you're interested in working on an issue, please start a conversation on that issue, and we can help answer any questions as they come up.
We label issues with the ["good first issue" tag](https://github.com/FairwindsOps/polaris/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) if we believe they'll be a good starting point for new contributors. If you're interested in working on an issue, please start a conversation on that issue, and we can help answer any questions as they come up.
## Setting Up Your Development Environment
### Prerequisites
@@ -23,8 +23,8 @@ We label issues with the ["good first issue" tag](https://github.com/reactiveops
* If you want to see the local changes you make on a Polaris dashboard, you will need access to a Kubernetes cluster defined in `~/.kube/config`
### Installation
* Install the project with `go get github.com/reactiveops/polaris`
* Change into the polaris directory which is installed at `$GOPATH/src/github.com/reactiveops/polaris`
* Install the project with `go get github.com/fairwindsops/polaris`
* Change into the polaris directory which is installed at `$GOPATH/src/github.com/fairwindsops/polaris`
* See the dashboard with `go run main.go --dashboard`, then open http://localhost:8080/
* See the audit data `go run main.go --audit`. This command shows the audit information on the command line.
@@ -40,11 +40,8 @@ go test ./pkg/... -v -coverprofile cover.out
## Creating a New Issue
If you've encountered an issue that is not already reported, please create an issue that contains the following:
If you've encountered an issue that is not already reported, please create a [new issue](https://github.com/FairwindsOps/polaris/issues), choose `Bug Report`, `Feature Request` or `Misc.` and follow the instructions in the template.
- Clear description of the issue
- Steps to reproduce it
- Appropriate labels
## Creating a Pull Request
@@ -58,35 +55,54 @@ Each new pull request should:
## Creating a new release
### Minor/patch releases
Minor and patch releases only need to change this repo. The Helm chart and deploy scripts
### Patch releases
Patch releases only need to change this repo. The Helm chart and deploy scripts
will automatically pull in the latest changes.
To deploy a minor or patch release, follow steps 2 and 3 from "Major releases" below.
If the release involves changes to anything in the `deploy/` folder (e.g. new RBAC permissions),
it needs to be a minor or major release in order to prevent breaking the Helm chart.
### Major releases
Major releases need to change both this repository and the
[Helm chart repo](https://github.com/reactiveops/charts/).
The steps are:
1. Create a PR in the [charts repo](https://github.com/reactiveops/charts/)
1. Use a branch named `polaris-latest`
2. Bump the version number in:
1. stable/polaris/README.md
2. stable/polaris/Chart.yaml
3. stable/polaris/values.yaml
3. **Don't merge yet!**
2. Create a PR for this repo
1. Create a PR for this repo
1. Bump the version number in:
1. main.go
2. README.md
2. Update CHANGELOG.md
3. Merge your PR
2. Tag the latest branch for this repo
1. Pull the latest commit for the `master` branch (which you just merged in your PR)
2. Run `git tag $VERSION && git push --tags`
3. Make sure CircleCI runs successfully for the new tag - this will push images to quay.io and create a release in GitHub
1. If CircleCI fails, check with Codeowners ASAP
### Minor/Major releases
Minor and major releases need to change both this repository and the
[Helm chart repo](https://github.com/FairwindsOps/charts/).
The steps are:
1. Modify the [Helm chart](https://github.com/FairwindsOps/charts/stable/polaris)
1. Clone the helm charts repo
1. `git clone https://github.com/FairwindsOps/charts`
2. `git checkout -b yourname/update-polaris`
1. Bump the version number in:
1. stable/polaris/README.md
2. stable/polaris/Chart.yaml
3. stable/polaris/values.yaml
2. Make any necessary changes to the chart to support the new version of Polaris (e.g. new RBAC permissions)
3. **Don't merge yet!**
2. Create a PR for this repo
1. Create a new branch named `yourname/update-version`
2. Bump the version number in:
1. main.go
2. README.md
3. Regenerate the deployment files. Assuming you've cloned the charts repo to `./charts`:
1. `helm template ./charts/stable/polaris/ --name polaris --namespace polaris --set templateOnly=true > deploy/dashboard.yaml`
2. `helm template ./charts/stable/polaris/ --name polaris --namespace polaris --set templateOnly=true --set webhook.enable=true --set dashboard.enable=false > deploy/webhook.yaml`
4. Update CHANGELOG.md
5. Merge your PR
3. Tag the latest branch for this repo
1. Pull the latest for the `master` branch
2. Run `git tag $VERSION && git push --tags`
3. Wait for CircleCI to finish the build for the tag, which will:
1. Create the proper image tag in quay.io
2. Add an entry to the releases page on GitHub
4. Merge the PR for the charts repo you created in step 1.
3. Make sure CircleCI runs successfully for the new tag - this will push images to quay.io and create a release in GitHub
1. If CircleCI fails, check with Codeowners ASAP
4. Create and merge a PR for your changes to the Helm chart
+2 -2
View File
@@ -1,5 +1,5 @@
FROM golang:1.12.4 AS build-env
WORKDIR /go/src/github.com/reactiveops/polaris/
WORKDIR /go/src/github.com/fairwindsops/polaris/
COPY . .
RUN go get -u github.com/gobuffalo/packr/v2/packr2
@@ -11,7 +11,7 @@ RUN apk --no-cache add ca-certificates
RUN addgroup -S polaris && adduser -u 1200 -S polaris -G polaris
USER 1200
COPY --from=build-env /go/src/github.com/reactiveops/polaris/polaris .
COPY --from=build-env /go/src/github.com/fairwindsops/polaris/polaris .
WORKDIR /opt/app
+1 -1
View File
@@ -186,7 +186,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2019 ReactiveOps
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.
+23 -118
View File
@@ -5,30 +5,36 @@
[![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.2.0&color=239922
[version-link]: https://github.com/reactiveops/polaris
[version-image]: https://img.shields.io/static/v1.svg?label=Version&message=0.4.0&color=239922
[version-link]: https://github.com/FairwindsOps/polaris
[goreport-image]: https://goreportcard.com/badge/github.com/reactiveops/polaris
[goreport-link]: https://goreportcard.com/report/github.com/reactiveops/polaris
[goreport-image]: https://goreportcard.com/badge/github.com/FairwindsOps/polaris
[goreport-link]: https://goreportcard.com/report/github.com/FairwindsOps/polaris
[circleci-image]: https://circleci.com/gh/reactiveops/polaris.svg?style=svg
[circleci-link]: https://circleci.com/gh/reactiveops/polaris.svg
[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 that will avoid potential problems in the future. The project includes two primary components:
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
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
**Want to learn more?** ReactiveOps 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@reactiveops.com`
**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`
## Quickstart
# Dashboard Quickstart
```
kubectl apply -f https://github.com/reactiveops/polaris/releases/latest/download/dashboard.yaml
kubectl apply -f https://github.com/FairwindsOps/polaris/releases/latest/download/dashboard.yaml
kubectl port-forward --namespace polaris svc/polaris-dashboard 8080:80
```
With the port forwarding in place, you can open http://localhost:8080 in your browser to view the dashboard.
* * *
# 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.
@@ -39,126 +45,25 @@ The Polaris dashboard is a way to get a simple visual overview of the current st
Our default standards in Polaris are rather high, so dont be surprised if your score is lower than you might expect. A key goal for Polaris was to set a high standard and aim for great configuration by default. If the defaults weve included are too strict, its easy to adjust the configuration as part of the deployment configuration to better suit your workloads.
## 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.*
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.
## Installation and Usage
Polaris can be installed on your cluster using kubectl or Helm. It can also
be run as a local binary, which will use your kubeconfig to connect to the cluster
or run against local YAML files.
# Usage Documentation
See the [Usage Guide](/docs/usage.md) in the docs folder.
### kubectl
#### Dashboard
```
kubectl apply -f https://github.com/reactiveops/polaris/releases/latest/download/dashboard.yaml
kubectl port-forward --namespace polaris svc/polaris-dashboard 8080:80
```
#### Webhook
```
kubectl apply -f https://github.com/reactiveops/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/reactiveops/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 whenever the Polaris score drops below 90%:
```bash
score=`polaris --audit --audit-path ./deploy/ --output-format score`
if [[ $score -lt 90 ]]; then
exit 1
else
exit 0
fi
```
## Configuration
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/reactiveops/polaris/blob/master/examples/config.yaml) contains a number of those checks. This repository also includes a sample [full configuration file](https://github.com/reactiveops/polaris/blob/master/examples/config-full.yaml) that enables all available 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:
- [Health Checks](docs/health-checks.md)
- [Images](docs/images.md)
- [Networking](docs/networking.md)
- [Resources](docs/resources.md)
- [Security](docs/security.md)
## CLI Options
* `config`: Specify a location for the Polaris config
* `dashboard`: Runs the webserver for Polaris dashboard.
* `dashboard-port`: Port for the dashboard webserver (default `8080`)
* `dashboard-base-path`: Path on which the dashboard is being served (default `/`)
* `webhook`: Runs the webhook webserver.
* `webhook-port`: Port for the webhook webserver (default `9876`)
* `disable-webhook-config-installer`: disable the installer in the webhook server, so it won't install webhook configuration resources during bootstrapping
* `kubeconfig`: Paths to a kubeconfig. Only required if out-of-cluster.
## Contributing
# Contributing
PRs welcome! Check out the [Contributing Guidelines](CONTRIBUTING.md),
[Code of Conduct](CODE_OF_CONDUCT.md), and [Roadmap](ROADMAP.md) for more information.
## Further Information
# Further Information
A history of changes to this project can be viewed in the [Changelog](CHANGELOG.md)
If you'd like to learn more about Polaris, or if you'd like to speak with
a Kubernetes expert, you can contact `info@reactiveops.com` or [visit our website](https://reactiveops.com)
a Kubernetes expert, you can contact `info@fairwinds.com` or [visit our website](https://fairwinds.com)
## License
# License
Apache License 2.0
+14 -8
View File
@@ -5,7 +5,16 @@ as well as the project's overall functionality.
If you have ideas for a new check, or for new Polaris features,
you can submit a pull request for this file, or open an issue in GitHub.
## Features
## Q3 2019
Below is a list of work we plan to get done this quarter. Some more details can be found under
[Future Work](#future_work), or
[in the milestone](https://github.com/FairwindsOps/polaris/milestone/1)
* Rule exceptions - we'd like to provide a way of overriding Polaris checks on individual controllers, e.g. through annotations
* More controller types - we plan to implement checks for more than just deployments
* Image repo checks - we plan to provide a check that ensures all repos conform to a set of user-defined patterns
* OPA integration (investigate only) - we plan to investigate the potential use of OPA to let users define custom Polaris checks
## Future Work
### Validating Webhook
The validating webhook rejects incoming workloads if they cause `error`-level
issues, as defined by the Polaris configuration.
@@ -21,7 +30,7 @@ when we feel it's ready.
* Pass `warning`-level messages back to `kubectl` if possible (may require changes
to core k8s or `kubectl`)
## Ability to override checks
### Ability to override checks
We plan to add the ability to override Polaris checks for particular namespaces
and workloads. This may be something that is set in the Polaris configuration,
or an annotation that can be added to the resource definition.
@@ -30,12 +39,9 @@ This is important, as certain workloads have good reason to skip particular Pola
checks - for instance, the `dns-controller` deployment in `kube-system` needs to have
the host network configured.
## Checks
These are checks that we plan to implement. If you have requests or ideas,
let us know! You can submit a pull request for this file, or open an issue in GitHub.
### Images
* List of allowed/disallowed registries
### Images Registries Check
We'd like to allow users to restrict images to a list of allowed/disallowed registries.
We'll likely allow patterns, e.g. `*.dkr.ecr.*.amazonaws.com`
### New controller types
Currently we only look at Deployments. We plan to also validate these types of controllers:
+241
View File
@@ -0,0 +1,241 @@
---
# Source: polaris/templates/0-namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
name: polaris
---
# Source: polaris/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: polaris
namespace: polaris
labels:
app: polaris
data:
config.yaml: |
resources:
cpuRequestsMissing: warning
cpuLimitsMissing: warning
memoryRequestsMissing: warning
memoryLimitsMissing: warning
images:
tagNotSpecified: error
healthChecks:
readinessProbeMissing: warning
livenessProbeMissing: warning
networking:
hostNetworkSet: warning
hostPortSet: warning
security:
hostIPCSet: error
hostPIDSet: error
notReadOnlyRootFileSystem: warning
privilegeEscalationAllowed: error
runAsRootAllowed: warning
runAsPrivileged: error
capabilities:
error:
ifAnyAdded:
- SYS_ADMIN
- NET_ADMIN
- ALL
warning:
ifAnyAddedBeyond:
- CHOWN
- DAC_OVERRIDE
- FSETID
- FOWNER
- MKNOD
- NET_RAW
- SETGID
- SETUID
- SETFCAP
- SETPCAP
- NET_BIND_SERVICE
- SYS_CHROOT
- KILL
- AUDIT_WRITE
controllers_to_scan:
- Deployments
- StatefulSets
- DaemonSets
- Jobs
- CronJobs
- ReplicationControllers
---
# Source: polaris/templates/dashboard.rbac.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: polaris-dashboard
namespace: polaris
labels:
app: polaris
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
metadata:
name: polaris-dashboard
labels:
app: polaris
rules:
- apiGroups:
- 'apps'
- 'extensions'
resources:
- 'deployments'
- 'statefulsets'
- 'daemonsets'
verbs:
- 'get'
- 'list'
- apiGroups:
- 'batch'
resources:
- 'jobs'
- 'cronjobs'
verbs:
- 'get'
- 'list'
- apiGroups:
- ''
resources:
- 'nodes'
- 'namespaces'
- 'pods'
- 'replicationcontrollers'
verbs:
- 'get'
- 'list'
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
name: polaris-dashboard
labels:
app: polaris
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: polaris-dashboard
subjects:
- kind: ServiceAccount
name: polaris-dashboard
namespace: polaris
---
# Source: polaris/templates/dashboard.service.yaml
apiVersion: v1
kind: Service
metadata:
name: polaris-dashboard
namespace: polaris
labels:
app: polaris
spec:
ports:
- name: dashboard
port: 80
protocol: TCP
targetPort: 8080
selector:
app: polaris
component: dashboard
type: ClusterIP
---
# Source: polaris/templates/dashboard.deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
annotations:
checksum/config: '8aa5a565fba7a2db98d46752087de8c1dcc83b70cd762c5829d5ba01270d54a2'
name: polaris-dashboard
namespace: polaris
labels:
app: polaris
component: dashboard
spec:
replicas: 1
selector:
matchLabels:
app: polaris
component: dashboard
template:
metadata:
labels:
app: polaris
component: dashboard
spec:
volumes:
- name: config
configMap:
name: polaris
containers:
- command:
- polaris
- --dashboard
- --config
- /opt/app/config.yaml
image: 'quay.io/reactiveops/polaris:0.4'
imagePullPolicy: 'Always'
name: dashboard
ports:
- containerPort: 8080
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 5
periodSeconds: 20
readinessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 5
periodSeconds: 20
resources:
limits:
cpu: 100m
memory: 128Mi
requests:
cpu: 100m
memory: 128Mi
securityContext:
allowPrivilegeEscalation: false
privileged: false
readOnlyRootFilesystem: true
runAsNonRoot: true
capabilities:
drop:
- ALL
volumeMounts:
- name: config
mountPath: /opt/app/config.yaml
subPath: config.yaml
readOnly: true
serviceAccountName: polaris-dashboard
nodeSelector: {}
tolerations: []
---
# Source: polaris/templates/audit.job.yaml
---
# Source: polaris/templates/audit.rbac.yaml
---
# Source: polaris/templates/ingress.yaml
---
# Source: polaris/templates/webhook.deployment.yaml
---
# Source: polaris/templates/webhook.rbac.yaml
---
# Source: polaris/templates/webhook.secret.yaml
---
# Source: polaris/templates/webhook.service.yaml
+319
View File
@@ -0,0 +1,319 @@
---
# Source: polaris/templates/0-namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
name: polaris
---
# Source: polaris/templates/webhook.secret.yaml
apiVersion: v1
kind: Secret
metadata:
name: polaris-webhook
namespace: polaris
labels:
app: polaris
type: Opaque
stringData:
cert.pem: ''
---
# Source: polaris/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: polaris
namespace: polaris
labels:
app: polaris
data:
config.yaml: |
resources:
cpuRequestsMissing: warning
cpuLimitsMissing: warning
memoryRequestsMissing: warning
memoryLimitsMissing: warning
images:
tagNotSpecified: error
healthChecks:
readinessProbeMissing: warning
livenessProbeMissing: warning
networking:
hostNetworkSet: warning
hostPortSet: warning
security:
hostIPCSet: error
hostPIDSet: error
notReadOnlyRootFileSystem: warning
privilegeEscalationAllowed: error
runAsRootAllowed: warning
runAsPrivileged: error
capabilities:
error:
ifAnyAdded:
- SYS_ADMIN
- NET_ADMIN
- ALL
warning:
ifAnyAddedBeyond:
- CHOWN
- DAC_OVERRIDE
- FSETID
- FOWNER
- MKNOD
- NET_RAW
- SETGID
- SETUID
- SETFCAP
- SETPCAP
- NET_BIND_SERVICE
- SYS_CHROOT
- KILL
- AUDIT_WRITE
controllers_to_scan:
- Deployments
- StatefulSets
- DaemonSets
- Jobs
- CronJobs
- ReplicationControllers
---
# Source: polaris/templates/webhook.rbac.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: polaris-webhook
namespace: polaris
labels:
app: polaris
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
metadata:
name: polaris-webhook
labels:
app: polaris
rules:
# auditor rules rbac
- apiGroups:
- 'apps'
- 'extensions'
resources:
- 'deployments'
- 'statefulsets'
- 'daemonsets'
verbs:
- 'get'
- 'list'
- apiGroups:
- 'batch'
resources:
- 'jobs'
- 'cronjobs'
verbs:
- 'get'
- 'list'
- apiGroups:
- ''
resources:
- 'nodes'
- 'namespaces'
- 'pods'
- 'replicationcontrollers'
verbs:
- 'get'
- 'list'
# required by controller-runtime code doing a cluster wide lookup
# when it seems namespace would suffice
- apiGroups:
- ''
resources:
- 'secrets'
- 'services'
verbs:
- 'get'
- 'list'
- 'watch'
- apiGroups:
- 'admissionregistration.k8s.io'
resources:
- 'validatingwebhookconfigurations'
verbs:
- '*'
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
name: polaris-webhook
labels:
app: polaris
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: polaris-webhook
subjects:
- kind: ServiceAccount
name: polaris-webhook
namespace: polaris
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: Role
metadata:
name: polaris-webhook
namespace: polaris
labels:
app: polaris
rules:
# required for current controller-runtime bootstrap method
- apiGroups:
- ''
resources:
- 'secrets'
- 'services'
verbs:
- '*'
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: RoleBinding
metadata:
name: polaris-webhook
namespace: polaris
labels:
app: polaris
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: polaris-webhook
subjects:
- kind: ServiceAccount
name: polaris-webhook
namespace: polaris
---
# Source: polaris/templates/webhook.service.yaml
apiVersion: v1
kind: Service
metadata:
name: polaris-webhook
namespace: polaris
labels:
app: polaris
spec:
ports:
- name: webhook
port: 443
protocol: TCP
targetPort: 9876
selector:
app: polaris
component: webhook
type: ClusterIP
---
# Source: polaris/templates/webhook.deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
annotations:
checksum/config: '8aa5a565fba7a2db98d46752087de8c1dcc83b70cd762c5829d5ba01270d54a2'
name: polaris-webhook
namespace: polaris
labels:
app: polaris
component: webhook
spec:
replicas: 1
selector:
matchLabels:
app: polaris
component: webhook
template:
metadata:
labels:
app: polaris
component: webhook
spec:
containers:
- name: webhook
command:
- polaris
- --webhook
- --config
- /opt/app/config.yaml
image: 'quay.io/reactiveops/polaris:0.4'
imagePullPolicy: 'Always'
ports:
- containerPort: 9876
# These are fairly useless readiness/liveness probes for now
# Follow this issue for potential improvements:
# https://github.com/kubernetes-sigs/controller-runtime/issues/356
livenessProbe:
exec:
command:
- sh
- -c
- ps -ef | grep polaris
initialDelaySeconds: 5
periodSeconds: 5
readinessProbe:
exec:
command:
- sh
- -c
- ps -ef | grep polaris
initialDelaySeconds: 5
periodSeconds: 5
resources:
limits:
cpu: 100m
memory: 128Mi
requests:
cpu: 100m
memory: 128Mi
securityContext:
allowPrivilegeEscalation: false
privileged: false
readOnlyRootFilesystem: true
runAsNonRoot: true
capabilities:
drop:
- ALL
volumeMounts:
- name: config
mountPath: /opt/app/config.yaml
subPath: config.yaml
readOnly: true
- name: secret
mountPath: /opt/cert/
readOnly: true
- name: cr-logs
mountPath: /tmp/
readOnly: false
serviceAccountName: polaris-webhook
nodeSelector: {}
tolerations: []
volumes:
- name: config
configMap:
name: polaris
- name: secret
secret:
secretName: polaris-webhook
- name: cr-logs
emptyDir: {}
---
# Source: polaris/templates/audit.job.yaml
---
# Source: polaris/templates/audit.rbac.yaml
---
# Source: polaris/templates/dashboard.deployment.yaml
---
# Source: polaris/templates/dashboard.rbac.yaml
---
# Source: polaris/templates/dashboard.service.yaml
---
# Source: polaris/templates/ingress.yaml
+13
View File
@@ -0,0 +1,13 @@
# Polaris Exit Codes for Audit Runs
<dl>
<dt>Exit 0</dt>
<dd>Successful exit code</dd>
<dt>Exit 1</dt>
<dd>Could not run audit, or application had a failure while running.</dd>
<dt>Exit 2</dt>
<dd>Unused</dd>
<dt>Exit 3</dt>
<dd>Exiting due to `--set-exit-code-on-error` being set and at least one error was found after an audit.</dd>
<dt>Edit 4</dd>
<dd>Exiting due to `--set-exit-code-below-score` being set and the audit resulted in a score less than the minimum score value.</dd>
</dl>
+145
View File
@@ -0,0 +1,145 @@
# Installation and Usage
Polaris can be installed on your cluster using kubectl or Helm. It can also
be run as a local binary, which will use your kubeconfig to connect to the cluster
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 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.
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:
- [Health Checks](check-documentation/health-checks.md)
- [Images](check-documentation/images.md)
- [Networking](check-documentation/networking.md)
- [Resources](check-documentation/resources.md)
- [Security](check-documentation/security.md)
## CLI Options
```
# high-level flags
-version
Prints the version of Polaris
-config string
Location of Polaris configuration file
-kubeconfig string
Path to a kubeconfig. Only required if out-of-cluster.
-log-level string
Logrus log level (default "info")
-master string
The address of the Kubernetes API server. Overrides any value in kubeconfig. Only required if out-of-cluster.
# dashboard flags
-dashboard
Runs the webserver for Polaris dashboard.
-dashboard-base-path string
Path on which the dashboard is served (default "/")
-dashboard-port int
Port for the dashboard webserver (default 8080)
-display-name string
An optional identifier for the audit
# audit flags
-audit
Runs a one-time audit.
-audit-path string
If specified, audits one or more YAML files instead of a cluster
-output-file string
Destination file for audit results
-output-format string
Output format for results - json, yaml, or score (default "json")
-output-url string
Destination URL to send audit results
-set-exit-code-below-score int
When running with --audit, set an exit code of 4 when the score is below this threshold (1-100)
-set-exit-code-on-error
When running with --audit, set an exit code of 3 when the audit contains error-level issues.
# webhook flags
-webhook
Runs the webhook webserver.
-webhook-port int
Port for the webhook webserver (default 9876)
-disable-webhook-config-installer
disable the installer in the webhook server, so it won't install webhook configuration resources during bootstrapping
```
# 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).
+7
View File
@@ -41,3 +41,10 @@ security:
- SYS_CHROOT
- KILL
- AUDIT_WRITE
controllers_to_scan:
- Deployments
- StatefulSets
- DaemonSets
- CronJobs
- Jobs
- ReplicationControllers
+50 -19
View File
@@ -1,4 +1,4 @@
// Copyright 2019 ReactiveOps
// 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.
@@ -22,15 +22,14 @@ import (
"io/ioutil"
"net/http"
"os"
"strings"
conf "github.com/reactiveops/polaris/pkg/config"
"github.com/reactiveops/polaris/pkg/dashboard"
"github.com/reactiveops/polaris/pkg/kube"
"github.com/reactiveops/polaris/pkg/validator"
fwebhook "github.com/reactiveops/polaris/pkg/webhook"
conf "github.com/fairwindsops/polaris/pkg/config"
"github.com/fairwindsops/polaris/pkg/dashboard"
"github.com/fairwindsops/polaris/pkg/kube"
"github.com/fairwindsops/polaris/pkg/validator"
fwebhook "github.com/fairwindsops/polaris/pkg/webhook"
"github.com/sirupsen/logrus"
appsv1 "k8s.io/api/apps/v1"
extensionsv1beta1 "k8s.io/api/extensions/v1beta1"
apitypes "k8s.io/apimachinery/pkg/types"
_ "k8s.io/client-go/plugin/pkg/client/auth" // Required for other auth providers like GKE.
"sigs.k8s.io/controller-runtime/pkg/client/config"
@@ -42,14 +41,18 @@ import (
const (
// Version represents the current release version of Polaris
Version = "0.2.0"
Version = "0.4.0"
)
func main() {
// Load CLI Flags
// TODO: Split up global flags vs dashboard/webhook/audit specific flags
dashboard := flag.Bool("dashboard", false, "Runs the webserver for Polaris dashboard.")
webhook := flag.Bool("webhook", false, "Runs the webhook webserver.")
audit := flag.Bool("audit", false, "Runs a one-time audit.")
auditPath := flag.String("audit-path", "", "If specified, audits one or more YAML files instead of a cluster")
setExitCode := flag.Bool("set-exit-code-on-error", false, "When running with --audit, set an exit code of 3 when the audit contains error-level issues.")
minScore := flag.Int("set-exit-code-below-score", 0, "When running with --audit, set an exit code of 4 when the score is below this threshold (1-100)")
dashboardPort := flag.Int("dashboard-port", 8080, "Port for the dashboard webserver")
dashboardBasePath := flag.String("dashboard-base-path", "/", "Path on which the dashboard is served")
webhookPort := flag.Int("webhook-port", 9876, "Port for the webhook webserver")
@@ -65,11 +68,13 @@ 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)
@@ -77,25 +82,39 @@ func main() {
logrus.SetLevel(parsedLevel)
}
// Parse the config file
c, err := conf.ParseFile(*configPath)
if *displayName != "" {
c.DisplayName = *displayName
}
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 !*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)
} else if *audit {
runAudit(c, *auditPath, *auditOutputFile, *auditOutputURL, *auditOutputFormat)
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)
} else if *minScore != 0 && auditData.ClusterSummary.Score < uint(*minScore) {
logrus.Infof("Audit score of %d is less than the provided minimum of %d", auditData.ClusterSummary.Score, *minScore)
os.Exit(4)
}
}
}
@@ -165,10 +184,19 @@ func startWebhookServer(c conf.Configuration, disableWebhookConfigInstaller bool
logrus.Infof("Polaris webhook server listening on port %d", port)
d1 := fwebhook.NewWebhook("deployments", mgr, fwebhook.Validator{Config: c}, &appsv1.Deployment{})
d2 := fwebhook.NewWebhook("deployments-ext", mgr, fwebhook.Validator{Config: c}, &extensionsv1beta1.Deployment{})
// Iterate all the configurations supported controllers to scan and register them for webhooks
// Should only register controllers that are configured to be scanned
logrus.Debug("Registering webhooks to the webhook server")
if err = as.Register(d1, d2); err != nil {
var webhooks []webhook.Webhook
for index, controllerToScan := range c.ControllersToScan {
for innerIndex, supportedAPIType := range controllerToScan.ListSupportedAPIVersions() {
webhookName := strings.ToLower(fmt.Sprintf("%s-%d-%d", controllerToScan, index, innerIndex))
hook := fwebhook.NewWebhook(webhookName, mgr, fwebhook.Validator{Config: c}, supportedAPIType)
webhooks = append(webhooks, hook)
}
}
if err = as.Register(webhooks...); err != nil {
logrus.Debugf("Unable to register webhooks in the admission server: %v", err)
os.Exit(1)
}
@@ -180,7 +208,8 @@ func startWebhookServer(c conf.Configuration, disableWebhookConfigInstaller bool
}
}
func runAudit(c conf.Configuration, auditPath string, outputFile string, outputURL string, outputFormat string) {
func runAndReportAudit(c conf.Configuration, auditPath string, outputFile string, outputURL string, outputFormat string) validator.AuditData {
// Create a kubernetes client resource provider
k, err := kube.CreateResourceProvider(auditPath)
if err != nil {
logrus.Errorf("Error fetching Kubernetes resources %v", err)
@@ -189,12 +218,13 @@ func runAudit(c conf.Configuration, auditPath string, outputFile string, outputU
auditData, err := validator.RunAudit(c, k)
if err != nil {
panic(err)
logrus.Errorf("Error while running audit on resources: %v", err)
os.Exit(1)
}
var outputBytes []byte
if outputFormat == "score" {
outputBytes = []byte(fmt.Sprint(auditData.ClusterSummary.Score))
outputBytes = []byte(fmt.Sprintf("%d\n", auditData.ClusterSummary.Score))
} else if outputFormat == "yaml" {
jsonBytes, err := json.Marshal(auditData)
if err == nil {
@@ -253,4 +283,5 @@ func runAudit(c conf.Configuration, auditPath string, outputFile string, outputU
}
}
}
return auditData
}
+19 -8
View File
@@ -1,4 +1,4 @@
// Copyright 2019 ReactiveOps
// 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.
@@ -19,6 +19,8 @@ import (
"fmt"
"io"
"io/ioutil"
"net/http"
"strings"
packr "github.com/gobuffalo/packr/v2"
corev1 "k8s.io/api/core/v1"
@@ -28,12 +30,13 @@ 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"`
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"`
}
// Resources contains config for resource requests and limits.
@@ -112,12 +115,20 @@ type SecurityCapabilityLists struct {
// ParseFile parses config from a file.
func ParseFile(path string) (Configuration, error) {
configBox := packr.New("Config", "../../examples")
var rawBytes []byte
var err error
configBox := packr.New("Config", "../../examples")
if path == "" {
rawBytes, err = configBox.Find("config.yaml")
} else if strings.HasPrefix(path, "https://") || strings.HasPrefix(path, "http://") {
//path is a url
response, err2 := http.Get(path)
if err2 != nil {
return Configuration{}, err2
}
rawBytes, err = ioutil.ReadAll(response.Body)
} else {
//path is local
rawBytes, err = ioutil.ReadFile(path)
}
if err != nil {
+48 -2
View File
@@ -1,4 +1,4 @@
// Copyright 2019 ReactiveOps
// 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.
@@ -15,6 +15,11 @@
package config
import (
"context"
"io"
"log"
"net/http"
"regexp"
"testing"
"github.com/stretchr/testify/assert"
@@ -53,6 +58,13 @@ resources:
warning:
below: 300M
above: 4G
controllers_to_scan:
- Deployments
- StatefulSets
- Jobs
- CronJobs
- DaemonSets
- ReplicationControllers
`
var resourceConfJSON1 = `{
@@ -97,7 +109,8 @@ var resourceConfJSON1 = `{
"above": "4G"
}
}
}
},
"controllers_to_scan": ["Deployments", "StatefulSets", "Jobs", "CronJobs", "DaemonSets", "ReplicationControllers"]
}`
func TestParseError(t *testing.T) {
@@ -120,6 +133,36 @@ func TestParseJson(t *testing.T) {
testParsedConfig(t, &parsedConf)
}
func TestConfigFromURL(t *testing.T) {
var err error
var parsedConf Configuration
srv := &http.Server{Addr: ":8081"}
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
io.WriteString(w, resourceConfYAML1)
})
go func() {
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
log.Fatalf("ListenAndServe(): %s", err)
}
}()
parsedConf, err = ParseFile("http://localhost:8081/exampleURL")
assert.NoError(t, err, "Expected no error when parsing YAML from URL")
if err := srv.Shutdown(context.TODO()); err != nil {
panic(err)
}
testParsedConfig(t, &parsedConf)
}
func TestConfigNoServerError(t *testing.T) {
var err error
_, err = ParseFile("http://localhost:8081/exampleURL")
assert.Error(t, err)
assert.Regexp(t, regexp.MustCompile("connection refused"), err.Error())
}
func testParsedConfig(t *testing.T, config *Configuration) {
cpuRequests := config.Resources.CPURequestRanges
assert.Equal(t, int64(100), cpuRequests.Error.Below.ScaledValue(resource.Milli))
@@ -144,4 +187,7 @@ func testParsedConfig(t *testing.T, config *Configuration) {
assert.Equal(t, int64(6000), memLimits.Error.Above.ScaledValue(resource.Mega))
assert.Equal(t, int64(300), memLimits.Warning.Below.ScaledValue(resource.Mega))
assert.Equal(t, int64(4000), memLimits.Warning.Above.ScaledValue(resource.Mega))
controllersToScan := config.ControllersToScan
assert.ElementsMatch(t, []SupportedController{Deployments, StatefulSets, Jobs, CronJobs, DaemonSets, ReplicationControllers}, controllersToScan)
}
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright 2019 ReactiveOps
// 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.
+151
View File
@@ -0,0 +1,151 @@
package config
import (
"bytes"
"encoding/json"
"fmt"
"strings"
appsv1 "k8s.io/api/apps/v1"
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"
)
const (
// Unsupported is the default enum for non-defined controller types
Unsupported SupportedController = iota
// Deployments are a supported controller for scanning pod specs
Deployments
// StatefulSets are a supported controller for scanning pod specs
StatefulSets
// DaemonSets are a supported controller for scanning pod specs
DaemonSets
// Jobs are a supported controller for scanning pod specs
Jobs
// CronJobs are a supported controller for scanning pod specs
CronJobs
// ReplicationControllers are supported controllers for scanning pod specs
ReplicationControllers
)
// ControllerStrings are strongly ordered to match the SupportedController enum
var ControllerStrings = []string{
"Unsupported",
"Deployments",
"StatefulSets",
"DaemonSets",
"Jobs",
"CronJobs",
"ReplicationController",
}
// stringLookupForSupportedControllers is the list of lowercase singular and plural strings for string to enum lookup
var stringLookupForSupportedControllers = map[string]SupportedController{
"deployment": Deployments,
"deployments": Deployments,
"statefulset": StatefulSets,
"statefulsets": StatefulSets,
"daemonset": DaemonSets,
"daemonsets": DaemonSets,
"job": Jobs,
"jobs": Jobs,
"cronjob": CronJobs,
"cronjobs": CronJobs,
"replicationcontroller": ReplicationControllers,
"replicationcontrollers": ReplicationControllers,
}
// SupportedController is a constant item of a controller that is supported for scanning pod specs
type SupportedController int
// String returns the string name for a given SupportedController enum
func (s SupportedController) String() string {
return ControllerStrings[s]
}
// MarshalJSON manages writing the enum into json data or error on unsupported value
func (s SupportedController) MarshalJSON() ([]byte, error) {
if s == Unsupported {
return []byte{}, fmt.Errorf("Unsupported is not a valid Supported Controller")
}
buffer := bytes.NewBufferString(`"`)
buffer.WriteString(s.String())
buffer.WriteString(`"`)
return buffer.Bytes(), nil
}
// UnmarshalJSON handles reading json data into enum
func (s *SupportedController) UnmarshalJSON(b []byte) error {
var j string
err := json.Unmarshal(b, &j)
if err != nil {
return err
}
*s, err = GetSupportedControllerFromString(j)
if err != nil {
return err
}
return nil
}
// ListSupportedAPIVersions for SupportedController returns all the apimachinery object type supported
func (s SupportedController) ListSupportedAPIVersions() []runtime.Object {
var supportedVersions []runtime.Object
switch s {
case Deployments:
supportedVersions = []runtime.Object{
&appsv1.Deployment{},
&extensionsv1beta1.Deployment{},
}
case StatefulSets:
supportedVersions = []runtime.Object{
&appsv1.StatefulSet{},
}
case DaemonSets:
supportedVersions = []runtime.Object{
&appsv1.DaemonSet{},
}
case Jobs:
supportedVersions = []runtime.Object{
&batchv1.Job{},
}
case CronJobs:
supportedVersions = []runtime.Object{
&batchv1beta1.CronJob{},
}
case ReplicationControllers:
supportedVersions = []runtime.Object{
&corev1.ReplicationController{},
}
}
return supportedVersions
}
// GetSupportedControllerFromString fuzzy matches a string with a SupportedController Enum
func GetSupportedControllerFromString(str string) (SupportedController, error) {
lowerStr := strings.ToLower(str)
controller, keyFound := stringLookupForSupportedControllers[lowerStr]
if !keyFound || controller == Unsupported {
return 0, fmt.Errorf("Value ('%v') in configuration was not found in Supported Controllers: (%v)", str, strings.Join(ControllerStrings, ","))
}
return controller, nil
}
// CheckIfKindIsConfiguredForValidation takes a kind (in string format) and checks if Polaris is configured to scan this type of controller
func (c Configuration) CheckIfKindIsConfiguredForValidation(kind string) bool {
controller, err := GetSupportedControllerFromString(kind)
// if no errors then we found the kind in supported controller types
if err == nil {
// see if the kind exists in the controllers to scan config
for _, controllerToScan := range c.ControllersToScan {
if controller == controllerToScan {
return true
}
}
}
return false
}
+125
View File
@@ -0,0 +1,125 @@
// 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 config
import (
"encoding/json"
"fmt"
"testing"
"github.com/stretchr/testify/assert"
)
type checkMarshal struct {
Controllers []SupportedController `json:"controllers"`
}
func TestUnmarshalSupportedControllers(t *testing.T) {
for idx, controllerString := range ControllerStrings {
// Check taking all strings and convert them into enums
object := checkMarshal{}
jsonBytes := []byte(fmt.Sprintf(`{"controllers":["%v"]}`, controllerString))
err := json.Unmarshal(jsonBytes, &object)
if idx == 0 {
if err == nil {
// Assure the first element always should NOT unmarshal
t.Errorf("Expected the first element (%s) to fail json unmarshal. First element in this array should always be 'Unsupported'", controllerString)
}
} else if err != nil {
t.Errorf("Could not unmarshal json (%s) to a Supported controller; Received (%v)", jsonBytes, err)
}
}
badJSON := []byte(`{"controllers":[{"not":"valid_structure"}]}`)
err := json.Unmarshal(badJSON, &checkMarshal{})
if err == nil {
t.Error("expected invalid schema json to fail unmarshal")
}
}
func TestMarshalSupportedControllers(t *testing.T) {
for idx, controllerString := range ControllerStrings {
controllerType, err := GetSupportedControllerFromString(controllerString)
if idx == 0 {
if err == nil {
t.Errorf("Expected first element (%s) to fail as a non-valid supported controller. Reserved for 'Unsupported'", controllerString)
}
} else if err != nil {
t.Errorf("Unable to take the configured string (%s) and convert into Enum; Error: (%s)", controllerString, err)
}
object := checkMarshal{
Controllers: []SupportedController{controllerType},
}
_, err = json.Marshal(object)
if idx == 0 {
if err == nil {
t.Errorf("Expected (%s) to throw an error. Reserving the first element in the enum to be an invalid config", controllerString)
}
} else if err != nil {
t.Errorf("Could not write json output for element (%s); Received Error: (%s)", controllerString, err)
}
}
}
func TestCheckIfControllerKindIsConfiguredForValidation(t *testing.T) {
config := Configuration{}
for _, controllerString := range ControllerStrings[1:] {
controllerEnum, err := GetSupportedControllerFromString(controllerString)
if err != nil {
t.Errorf("Expected controller string (%s) to be convertable into enum: (%s)", controllerString, err)
}
config.ControllersToScan = append(config.ControllersToScan, controllerEnum)
}
validControllerKinds := []string{
"deployment",
"statefulset",
}
invalidControllerKinds := []string{
"nonExistent",
}
for _, kind := range validControllerKinds {
if ok := config.CheckIfKindIsConfiguredForValidation(kind); !ok {
t.Errorf("Kind (%s) expected to be valid for configuration.", kind)
}
}
for _, kind := range invalidControllerKinds {
if ok := config.CheckIfKindIsConfiguredForValidation(kind); ok {
t.Errorf("Kind (%s) should not be a valid controller to check", kind)
}
}
}
func TestGetSupportedControllerFromString(t *testing.T) {
fixture := map[string]SupportedController{
"": Unsupported,
"asdfasdf": Unsupported,
"\000": Unsupported,
"deployMENTS": Deployments,
"JOB": Jobs,
}
for inputString, expectedType := range fixture {
resolvedType, err := GetSupportedControllerFromString(inputString)
if expectedType == Unsupported && err == nil {
t.Errorf("Expected (%s) to resolve to an unsupported type and throw an error.", inputString)
}
assert.Equal(t, expectedType, resolvedType, fmt.Sprintf("Expected (%s) to return (%s) controller type.", inputString, expectedType))
}
}
+3 -10
View File
@@ -22,24 +22,17 @@ body {
}
.navbar .logo {
width: 250px;
height: 80px;
}
.navbar span.oss-text {
color: #23103A;
display: block;
font-size: 11px;
margin-bottom: 3px;
.navbar .fw-logo {
height: 64px;
}
.navbar a {
text-decoration: none;
}
.navbar .ro-logo {
height: 50px;
}
.card {
margin: 25px 20px;
padding: 20px;
+50
View File
@@ -0,0 +1,50 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 22.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 486.5 92.3" style="enable-background:new 0 0 486.5 92.3;" xml:space="preserve">
<style type="text/css">
.st0{fill:#23103A;}
</style>
<g>
<g>
<path class="st0" d="M72.5,24.3H0.4V0.4h84.4V12C84.8,18.8,79.3,24.3,72.5,24.3z"/>
<path class="st0" d="M41.8,58H0.4V34.1h53.7v11.6C54.1,52.5,48.6,58,41.8,58z"/>
<path class="st0" d="M12,91.7H0.4V67.8h23.9v11.6C24.2,86.2,18.7,91.7,12,91.7z"/>
</g>
<g>
<path class="st0" d="M121.7,77V16.9h36.9v4.2c0,3.7-3,6.8-6.8,6.8h-18.4v13.5H150v3.5c0,4.1-3.3,7.5-7.5,7.5h-9.1v19.1
c0,3-2.5,5.5-5.5,5.5H121.7z"/>
<path class="st0" d="M161.1,64.7c0-2,0.3-3.7,1-5.1c0.7-1.5,1.6-2.7,2.9-3.6c1.3-1,2.7-1.7,4.2-2.3c1.6-0.6,3.3-1,5.4-1.3l11.3-2
v-1.5c0-2-0.5-3.5-1.6-4.6c-1-1.1-2.6-1.6-4.5-1.6s-3.4,0.5-4.6,1.4c-1.2,1-1.8,2.2-1.8,3.8h-10.8c0.3-4.5,1.9-8.2,5.1-11
c3.1-2.8,7.2-4.3,12.3-4.3c5.3,0,9.5,1.3,12.6,4s4.7,6.6,4.7,11.8V77h-4.7c-2.8,0-5.1-2.1-5.5-4.8l-0.1-1.1
c-2.7,4.6-6.9,6.8-12.4,6.8c-3.9,0-7.1-1.2-9.6-3.6C162.4,72,161.1,68.8,161.1,64.7z M172.3,64c0,1.2,0.4,2.1,1.3,2.8
c0.9,0.7,2.1,1,3.7,1c1.4,0,2.7-0.3,3.9-0.9c1.2-0.6,2.2-1.5,3-2.7c1-1.4,1.5-3.2,1.5-5.3v-1.2l-8.7,1.8c-1.6,0.3-2.8,0.8-3.6,1.5
C172.7,61.8,172.3,62.7,172.3,64z"/>
<path class="st0" d="M207,20.9c0-1.8,0.7-3.6,2-4.9c1.3-1.2,2.8-1.9,4.6-1.9c1.8,0,3.6,0.6,4.9,1.9c1.4,1.3,2.1,2.9,2.1,4.8
s-0.7,3.4-2,4.7c-1.3,1.3-2.9,1.9-4.8,1.9s-3.5-0.6-4.8-1.9C207.7,24.2,207.1,22.7,207,20.9z M208.2,77V33.5h11.2v38
c0,3-2.5,5.5-5.5,5.5H208.2z"/>
<path class="st0" d="M230.4,77V33.5h9.6l1.1,6.8c0.8-2.1,2.3-3.9,4.4-5.2c2.1-1.4,4.9-2.1,8.3-2.1v11c-3.5,0-6.4,0.9-8.6,2.6
s-3.4,4.4-3.5,8.1v16.7c0,3-2.5,5.5-5.5,5.5L230.4,77L230.4,77z"/>
<path class="st0" d="M259.6,33.5h11.9l6.6,26.1l0.8,4.6l1-4.6l7.4-26.1h9.9l7.4,26.1l0.9,4.6l0.8-4.6l6.7-26.1h11.9L313,72.9
c-0.7,2.4-3,4.1-5.5,4.1H300l-7.9-25.6L285,73c-0.8,2.3-3,3.9-5.4,3.9h-7L259.6,33.5z"/>
<path class="st0" d="M330.5,20.7c0-1.8,0.7-3.4,2-4.7c1.3-1.3,2.9-1.9,4.8-1.9c1.8,0,3.4,0.6,4.8,1.9c1.3,1.3,2,2.8,2,4.7
c0,1.9-0.7,3.4-2,4.7c-1.3,1.3-2.9,1.9-4.8,1.9s-3.5-0.6-4.8-1.9C331.2,24.2,330.5,22.6,330.5,20.7z M331.7,77V33.5h11.2v38
c0,3-2.5,5.5-5.5,5.5H331.7z"/>
<path class="st0" d="M354.8,77V33.5h9.6l0.9,5.8c1.1-2,2.6-3.6,4.6-4.9c2-1.2,4.4-1.9,7.2-1.9c4.5,0,8,1.4,10.4,4.3
s3.6,6.8,3.6,11.9v22.5c0,3.2-2.6,5.7-5.7,5.7h-5.5V51.4c0-2.5-0.5-4.5-1.6-5.8c-1-1.3-2.6-2-4.6-2c-2.3,0-4.1,0.9-5.5,2.7
c-1.4,1.8-2.2,4.4-2.2,7.9v17.1c0,3.2-2.6,5.7-5.7,5.7L354.8,77L354.8,77z"/>
<path class="st0" d="M399.1,55.2c0-6.4,1.9-11.8,5.7-16.1c3.8-4.3,8.7-6.5,14.5-6.5c2.9,0,5.5,0.5,7.8,1.6s4,2.4,5.2,4.1V14.4
h11.2V77h-4.8c-2.7,0-5-2-5.4-4.7l-0.2-1.2c-3.1,4.5-7.7,6.8-13.9,6.8c-3.8,0-7.3-1-10.4-3s-5.5-4.7-7.2-8.2
C399.9,63.3,399.1,59.4,399.1,55.2z M410.5,55.2c0,3.7,1,6.6,3.1,8.9c2.1,2.3,4.7,3.4,7.8,3.4c2.9,0,5.5-1.1,7.7-3.2
c2.2-2.2,3.3-5.1,3.3-8.8V55c0-2.5-0.6-4.6-1.7-6.5s-2.5-3.2-4.1-4.1s-3.4-1.3-5.2-1.3c-3.1,0-5.7,1.1-7.8,3.4
S410.5,51.6,410.5,55.2z"/>
<path class="st0" d="M452.2,63.8h6.9c2,0,3.7,1.2,4.8,2.9c0.2,0.4,0.6,0.7,0.9,1c1.1,0.9,2.6,1.4,4.5,1.4c1.7,0,3-0.4,4-1.1
c0.9-0.7,1.4-1.8,1.4-3c0-1.4-0.5-2.5-1.6-3.3s-3.2-1.5-6.2-2.1c-4.8-0.8-8.3-2.3-10.5-4.4c-2.3-2.1-3.4-4.9-3.4-8.5
c0-4,1.4-7.4,4.2-10.1c2.8-2.7,6.8-4.1,11.9-4.1c4.6,0,8.4,1.3,11.3,3.8c2.9,2.5,4.5,5.8,4.7,9.9H474c-0.1-3.3-1.7-5-5-5
c-1.5,0-2.7,0.4-3.5,1.2c-0.8,0.8-1.3,1.8-1.3,3.1c0,1.4,0.5,2.4,1.6,3.1s3.1,1.3,6,1.8c5.1,0.9,8.8,2.4,10.9,4.5
c2.2,2.1,3.2,5,3.2,8.7c0,2.4-0.4,4.5-1.3,6.4c-0.9,1.8-2.1,3.3-3.7,4.4c-1.6,1.1-3.3,2-5.3,2.5c-2,0.6-4.1,0.8-6.5,0.8
c-5.3,0-9.4-1.3-12.3-3.8C453.9,71.6,452.3,68.2,452.2,63.8z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

+5 -5
View File
@@ -1,4 +1,4 @@
// Copyright 2019 ReactiveOps
// 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.
@@ -23,9 +23,9 @@ import (
packr "github.com/gobuffalo/packr/v2"
"github.com/gorilla/mux"
conf "github.com/reactiveops/polaris/pkg/config"
"github.com/reactiveops/polaris/pkg/kube"
"github.com/reactiveops/polaris/pkg/validator"
conf "github.com/fairwindsops/polaris/pkg/config"
"github.com/fairwindsops/polaris/pkg/kube"
"github.com/fairwindsops/polaris/pkg/validator"
"github.com/sirupsen/logrus"
"gitlab.com/golang-commonmark/markdown"
)
@@ -72,7 +72,7 @@ func GetTemplateBox() *packr.Box {
// GetMarkdownBox returns a binary-friendly set of markdown files with error details
func GetMarkdownBox() *packr.Box {
if markdownBox == (*packr.Box)(nil) {
markdownBox = packr.New("Markdown", "../../docs")
markdownBox = packr.New("Markdown", "../../docs/check-documentation")
}
return markdownBox
}
+4 -6
View File
@@ -1,4 +1,4 @@
// Copyright 2019 ReactiveOps
// 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.
@@ -16,15 +16,13 @@ package dashboard
import (
"fmt"
"github.com/reactiveops/polaris/pkg/validator"
"strings"
"github.com/fairwindsops/polaris/pkg/validator"
)
func getAllControllerResults(nr validator.NamespaceResult) []validator.ControllerResult {
results := []validator.ControllerResult{}
results = append(results, nr.DeploymentResults...)
results = append(results, nr.StatefulSetResults...)
return results
return nr.GetAllControllerResults()
}
func getWarningWidth(counts validator.CountSummary, fullWidth int) uint {
+1 -1
View File
@@ -1,5 +1,5 @@
{{define "footer"}}
<div class="footer">
<a href="https://reactiveops.com?source=polaris" target="_blank">&copy;2019 ReactiveOps Inc.</a>
<a href="https://fairwinds.com?source=polaris" target="_blank">&copy;2019 FairwindsOps Inc.</a>
</div>
{{end}}
+1 -1
View File
@@ -2,7 +2,7 @@
<base href="{{ .BasePath }}">
<meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<title>ReactiveOps Polaris</title>
<title>Fairwinds Polaris</title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
+2 -3
View File
@@ -5,9 +5,8 @@
<img class="logo" src="static/images/polaris-logo.png" alt="Polaris" />
</a>
<div class="navbar-right">
<a href="https://reactiveops.com?source=polaris" target="_blank">
<span class="oss-text">An Open Source Project By</span>
<img class="ro-logo" src="static/images/ro-logo.png" alt="ReactiveOps" />
<a href="https://fairwinds.com?source=polaris" target="_blank">
<img class="fw-logo" src="static/images/oss-logo.png" alt="Fairwinds" />
</a>
</div>
</div>
+84 -35
View File
@@ -11,6 +11,8 @@ import (
"github.com/sirupsen/logrus"
appsv1 "k8s.io/api/apps/v1"
batchv1 "k8s.io/api/batch/v1"
batchv1beta1 "k8s.io/api/batch/v1beta1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
k8sYaml "k8s.io/apimachinery/pkg/util/yaml"
@@ -21,15 +23,19 @@ import (
// ResourceProvider contains k8s resources to be audited
type ResourceProvider struct {
ServerVersion string
CreationTime time.Time
SourceName string
SourceType string
Nodes []corev1.Node
Deployments []appsv1.Deployment
StatefulSets []appsv1.StatefulSet
Namespaces []corev1.Namespace
Pods []corev1.Pod
ServerVersion string
CreationTime time.Time
SourceName string
SourceType string
Nodes []corev1.Node
Deployments []appsv1.Deployment
StatefulSets []appsv1.StatefulSet
DaemonSets []appsv1.DaemonSet
Jobs []batchv1.Job
CronJobs []batchv1beta1.CronJob
ReplicationControllers []corev1.ReplicationController
Namespaces []corev1.Namespace
Pods []corev1.Pod
}
type k8sResource struct {
@@ -47,14 +53,18 @@ func CreateResourceProvider(directory string) (*ResourceProvider, error) {
// CreateResourceProviderFromPath returns a new ResourceProvider using the YAML files in a directory
func CreateResourceProviderFromPath(directory string) (*ResourceProvider, error) {
resources := ResourceProvider{
ServerVersion: "unknown",
SourceType: "Path",
SourceName: directory,
Nodes: []corev1.Node{},
Deployments: []appsv1.Deployment{},
StatefulSets: []appsv1.StatefulSet{},
Namespaces: []corev1.Namespace{},
Pods: []corev1.Pod{},
ServerVersion: "unknown",
SourceType: "Path",
SourceName: directory,
Nodes: []corev1.Node{},
Deployments: []appsv1.Deployment{},
StatefulSets: []appsv1.StatefulSet{},
DaemonSets: []appsv1.DaemonSet{},
Jobs: []batchv1.Job{},
CronJobs: []batchv1beta1.CronJob{},
ReplicationControllers: []corev1.ReplicationController{},
Namespaces: []corev1.Namespace{},
Pods: []corev1.Pod{},
}
addYaml := func(contents string) error {
@@ -77,7 +87,7 @@ func CreateResourceProviderFromPath(directory string) (*ResourceProvider, error)
}
err = addYaml(spec)
if err != nil {
logrus.Errorf("Error parsing YAML %v", err)
logrus.Errorf("Error parsing YAML: (%v)", err)
return err
}
}
@@ -124,6 +134,26 @@ func CreateResourceProviderFromAPI(kube kubernetes.Interface, clusterName string
logrus.Errorf("Error fetching StatefulSets%v", err)
return nil, err
}
daemonSets, err := kube.AppsV1().DaemonSets("").List(listOpts)
if err != nil {
logrus.Errorf("Error fetching DaemonSets %v", err)
return nil, err
}
jobs, err := kube.BatchV1().Jobs("").List(listOpts)
if err != nil {
logrus.Errorf("Error fetching Jobs %v", err)
return nil, err
}
cronJobs, err := kube.BatchV1beta1().CronJobs("").List(listOpts)
if err != nil {
logrus.Errorf("Error fetching CronJobs %v", err)
return nil, err
}
replicationControllers, err := kube.CoreV1().ReplicationControllers("").List(listOpts)
if err != nil {
logrus.Errorf("Error fetching ReplicationControllers %v", err)
return nil, err
}
nodes, err := kube.CoreV1().Nodes().List(listOpts)
if err != nil {
logrus.Errorf("Error fetching Nodes %v", err)
@@ -141,15 +171,19 @@ func CreateResourceProviderFromAPI(kube kubernetes.Interface, clusterName string
}
api := ResourceProvider{
ServerVersion: serverVersion.Major + "." + serverVersion.Minor,
SourceType: "Cluster",
SourceName: clusterName,
CreationTime: time.Now(),
Deployments: deploys.Items,
StatefulSets: statefulSets.Items,
Nodes: nodes.Items,
Namespaces: namespaces.Items,
Pods: pods.Items,
ServerVersion: serverVersion.Major + "." + serverVersion.Minor,
SourceType: "Cluster",
SourceName: clusterName,
CreationTime: time.Now(),
Deployments: deploys.Items,
StatefulSets: statefulSets.Items,
DaemonSets: daemonSets.Items,
Jobs: jobs.Items,
CronJobs: cronJobs.Items,
ReplicationControllers: replicationControllers.Items,
Nodes: nodes.Items,
Namespaces: namespaces.Items,
Pods: pods.Items,
}
return &api, nil
}
@@ -160,19 +194,34 @@ func addResourceFromString(contents string, resources *ResourceProvider) error {
resource := k8sResource{}
err := decoder.Decode(&resource)
if err != nil {
// TODO: should we panic if the YAML is bad?
logrus.Errorf("Invalid YAML: %s", string(contents))
return nil
return err
}
decoder = k8sYaml.NewYAMLOrJSONDecoder(bytes.NewReader(contentBytes), 1000)
if resource.Kind == "Deployment" {
dep := appsv1.Deployment{}
err = decoder.Decode(&dep)
resources.Deployments = append(resources.Deployments, dep)
controller := appsv1.Deployment{}
err = decoder.Decode(&controller)
resources.Deployments = append(resources.Deployments, controller)
} else if resource.Kind == "StatefulSet" {
dep := appsv1.StatefulSet{}
err = decoder.Decode(&dep)
resources.StatefulSets = append(resources.StatefulSets, dep)
controller := appsv1.StatefulSet{}
err = decoder.Decode(&controller)
resources.StatefulSets = append(resources.StatefulSets, controller)
} else if resource.Kind == "DaemonSet" {
controller := appsv1.DaemonSet{}
err = decoder.Decode(&controller)
resources.DaemonSets = append(resources.DaemonSets, controller)
} else if resource.Kind == "Job" {
controller := batchv1.Job{}
err = decoder.Decode(&controller)
resources.Jobs = append(resources.Jobs, controller)
} else if resource.Kind == "CronJob" {
controller := batchv1beta1.CronJob{}
err = decoder.Decode(&controller)
resources.CronJobs = append(resources.CronJobs, controller)
} else if resource.Kind == "ReplicationController" {
controller := corev1.ReplicationController{}
err = decoder.Decode(&controller)
resources.ReplicationControllers = append(resources.ReplicationControllers, controller)
} else if resource.Kind == "Namespace" {
ns := corev1.Namespace{}
err = decoder.Decode(&ns)
+8 -2
View File
@@ -1,10 +1,11 @@
package kube
import (
"github.com/reactiveops/polaris/test"
"github.com/stretchr/testify/assert"
"testing"
"time"
"github.com/fairwindsops/polaris/test"
"github.com/stretchr/testify/assert"
)
func TestGetResourcesFromPath(t *testing.T) {
@@ -53,6 +54,11 @@ func TestGetMultipleResourceFromSingleFile(t *testing.T) {
assert.Equal(t, "polaris-2", resources.Namespaces[1].ObjectMeta.Name)
}
func TestGetMultipleResourceFromBadFile(t *testing.T) {
_, err := CreateResourceProviderFromPath("./test_files/test_3")
assert.NotEqual(t, nil, err, "CreateResource From Path should fail with bad yaml")
}
func TestGetResourceFromAPI(t *testing.T) {
k8s := test.SetupTestAPI()
k8s = test.SetupAddControllers(k8s, "test")
+16
View File
@@ -0,0 +1,16 @@
apiVersion: batch/v1beta1
kind: CronJob
metadata:
name: test
spec:
schedule: "*/1 * * * *"
jobTemplate:
spec:
template:
spec:
containers:
- name: test
image: busybox
args:
- whoami
restartPolicy: OnFailure
@@ -0,0 +1,20 @@
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: test
labels:
k8s-app: test
spec:
selector:
matchLabels:
name: test
template:
metadata:
labels:
name: test
spec:
containers:
- name: test
image: busybox
command: ["whoami"]
resources: {}
+12
View File
@@ -0,0 +1,12 @@
apiVersion: batch/v1
kind: Job
metadata:
name: test
spec:
template:
spec:
containers:
- name: test
image: alpine
command: ["whoami"]
restartPolicy: Never
@@ -0,0 +1,19 @@
apiVersion: v1
kind: ReplicationController
metadata:
name: test
spec:
replicas: 1
selector:
app: test
template:
metadata:
name: test
labels:
app: test
spec:
containers:
- name: test
image: busybox
command: ["tail"]
args: ["-f", "/dev/null"]
+5
View File
@@ -0,0 +1,5 @@
--
# Source: polaris/templates/dashboard.deployment.yaml
apiVersion: extensions/v1beta1
kind: Deployment
::::
+62 -16
View File
@@ -1,4 +1,4 @@
// Copyright 2019 ReactiveOps
// 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.
@@ -18,8 +18,8 @@ import (
"fmt"
"strings"
conf "github.com/reactiveops/polaris/pkg/config"
"github.com/reactiveops/polaris/pkg/validator/messages"
conf "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"
)
@@ -29,16 +29,34 @@ type ContainerValidation struct {
*ResourceValidation
Container *corev1.Container
IsInitContainer bool
parentPodSpec corev1.PodSpec
}
// ValidateContainer validates that each pod conforms to the Polaris config, returns a ResourceResult.
func ValidateContainer(cnConf *conf.Configuration, container *corev1.Container, isInit bool) ContainerResult {
// FIXME When validating a container, there are some things in a container spec
// that can be affected by the podSpec. This means we need a copy of the
// 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 {
cv := ContainerValidation{
Container: container,
ResourceValidation: &ResourceValidation{},
IsInitContainer: isInit,
}
// Support initializing
// FIXME This is a product of pulling in the podSpec, ideally we'd never
// expect this be nil but our tests have conditions in which the
// parent podResult isn't initialized in this ContainerValidation
// struct.
if parentPodResult == nil {
// initialize a blank pod spec
cv.parentPodSpec = corev1.PodSpec{}
} else {
cv.parentPodSpec = parentPodResult.podSpec
}
cv.validateResources(&cnConf.Resources)
cv.validateHealthChecks(&cnConf.HealthChecks)
cv.validateImage(&cnConf.Images)
@@ -176,39 +194,58 @@ func (cv *ContainerValidation) validateNetworking(networkConf *conf.Networking)
func (cv *ContainerValidation) validateSecurity(securityConf *conf.Security) {
category := messages.CategorySecurity
securityContext := cv.Container.SecurityContext
podSecurityContext := cv.parentPodSpec.SecurityContext
// Support an empty container security context
if securityContext == nil {
securityContext = &corev1.SecurityContext{}
}
// Support an empty pod security context
if podSecurityContext == nil {
podSecurityContext = &corev1.PodSecurityContext{}
}
if securityConf.RunAsRootAllowed.IsActionable() {
if securityContext.RunAsNonRoot == (*bool)(nil) || !*securityContext.RunAsNonRoot {
cv.addFailure(messages.RunAsRootFailure, securityConf.RunAsRootAllowed, category)
} else {
if getBoolValue(securityContext.RunAsNonRoot) {
// 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)
}
} else {
cv.addFailure(messages.RunAsRootFailure, securityConf.RunAsRootAllowed, category)
}
}
if securityConf.RunAsPrivileged.IsActionable() {
if securityContext.Privileged == (*bool)(nil) || !*securityContext.Privileged {
cv.addSuccess(messages.RunAsPrivilegedSuccess, category)
} else {
if getBoolValue(securityContext.Privileged) {
cv.addFailure(messages.RunAsPrivilegedFailure, securityConf.RunAsPrivileged, category)
} else {
cv.addSuccess(messages.RunAsPrivilegedSuccess, category)
}
}
if securityConf.NotReadOnlyRootFileSystem.IsActionable() {
if securityContext.ReadOnlyRootFilesystem == (*bool)(nil) || !*securityContext.ReadOnlyRootFilesystem {
cv.addFailure(messages.ReadOnlyFilesystemFailure, securityConf.NotReadOnlyRootFileSystem, category)
} else {
if getBoolValue(securityContext.ReadOnlyRootFilesystem) {
cv.addSuccess(messages.ReadOnlyFilesystemSuccess, category)
} else {
cv.addFailure(messages.ReadOnlyFilesystemFailure, securityConf.NotReadOnlyRootFileSystem, category)
}
}
if securityConf.PrivilegeEscalationAllowed.IsActionable() {
if securityContext.AllowPrivilegeEscalation == (*bool)(nil) || !*securityContext.AllowPrivilegeEscalation {
cv.addSuccess(messages.PrivilegeEscalationSuccess, category)
} else {
if getBoolValue(securityContext.AllowPrivilegeEscalation) {
cv.addFailure(messages.PrivilegeEscalationFailure, securityConf.PrivilegeEscalationAllowed, category)
} else {
cv.addSuccess(messages.PrivilegeEscalationSuccess, category)
}
}
@@ -323,3 +360,12 @@ func capContains(list []corev1.Capability, val corev1.Capability) bool {
return false
}
// getBoolValue returns false if nil or returns the value of the bool pointer
func getBoolValue(val *bool) bool {
if val == nil {
return false
}
return *val
}
+186 -2
View File
@@ -1,4 +1,4 @@
// Copyright 2019 ReactiveOps
// 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.
@@ -17,7 +17,7 @@ package validator
import (
"testing"
conf "github.com/reactiveops/polaris/pkg/config"
conf "github.com/fairwindsops/polaris/pkg/config"
"github.com/stretchr/testify/assert"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
@@ -567,6 +567,42 @@ func TestValidateSecurity(t *testing.T) {
ResourceValidation: &ResourceValidation{},
}
badCVWithGoodPodSpec := ContainerValidation{
Container: &corev1.Container{Name: "", SecurityContext: &corev1.SecurityContext{
RunAsNonRoot: &falseVar,
ReadOnlyRootFilesystem: &falseVar,
Privileged: &trueVar,
AllowPrivilegeEscalation: &trueVar,
Capabilities: &corev1.Capabilities{
Add: []corev1.Capability{"AUDIT_CONTROL", "SYS_ADMIN", "NET_ADMIN"},
},
}},
ResourceValidation: &ResourceValidation{},
parentPodSpec: corev1.PodSpec{
SecurityContext: &corev1.PodSecurityContext{
RunAsNonRoot: &trueVar,
},
},
}
badCVWithBadPodSpec := ContainerValidation{
Container: &corev1.Container{Name: "", SecurityContext: &corev1.SecurityContext{
RunAsNonRoot: nil, // this will use the default from the podspec
ReadOnlyRootFilesystem: &falseVar,
Privileged: &trueVar,
AllowPrivilegeEscalation: &trueVar,
Capabilities: &corev1.Capabilities{
Add: []corev1.Capability{"AUDIT_CONTROL", "SYS_ADMIN", "NET_ADMIN"},
},
}},
ResourceValidation: &ResourceValidation{},
parentPodSpec: corev1.PodSpec{
SecurityContext: &corev1.PodSecurityContext{
RunAsNonRoot: &falseVar,
},
},
}
goodCV := ContainerValidation{
Container: &corev1.Container{Name: "", SecurityContext: &corev1.SecurityContext{
RunAsNonRoot: &trueVar,
@@ -593,6 +629,42 @@ func TestValidateSecurity(t *testing.T) {
ResourceValidation: &ResourceValidation{},
}
strongCVWithPodSpecSecurityContext := ContainerValidation{
Container: &corev1.Container{Name: "", SecurityContext: &corev1.SecurityContext{
RunAsNonRoot: nil, // not set but overridden via podSpec
ReadOnlyRootFilesystem: &trueVar,
Privileged: &falseVar,
AllowPrivilegeEscalation: &falseVar,
Capabilities: &corev1.Capabilities{
Drop: []corev1.Capability{"ALL"},
},
}},
ResourceValidation: &ResourceValidation{},
parentPodSpec: corev1.PodSpec{
SecurityContext: &corev1.PodSecurityContext{
RunAsNonRoot: &trueVar,
},
},
}
strongCVWithBadPodSpecSecurityContext := ContainerValidation{
Container: &corev1.Container{Name: "", SecurityContext: &corev1.SecurityContext{
RunAsNonRoot: &trueVar, // will override the bad setting in PodSpec
ReadOnlyRootFilesystem: &trueVar,
Privileged: &falseVar,
AllowPrivilegeEscalation: &falseVar,
Capabilities: &corev1.Capabilities{
Drop: []corev1.Capability{"ALL"},
},
}},
ResourceValidation: &ResourceValidation{},
parentPodSpec: corev1.PodSpec{
SecurityContext: &corev1.PodSecurityContext{
RunAsNonRoot: &falseVar, // is overridden at container level with RunAsNonRoot:true
},
},
}
var testCases = []struct {
name string
securityConf conf.Security
@@ -661,6 +733,66 @@ func TestValidateSecurity(t *testing.T) {
Category: "Security",
}},
},
{
name: "bad security context + standard validation config with good settings in podspec",
securityConf: standardConf,
cv: badCVWithGoodPodSpec,
expectedMessages: []*ResultMessage{{
Message: "The following security capabilities should not be added: SYS_ADMIN, NET_ADMIN",
Type: "error",
Category: "Security",
}, {
Message: "Privilege escalation should not be allowed",
Type: "error",
Category: "Security",
}, {
Message: "Should not be running as privileged",
Type: "error",
Category: "Security",
}, {
Message: "The following security capabilities should not be added: AUDIT_CONTROL, SYS_ADMIN, NET_ADMIN",
Type: "warning",
Category: "Security",
}, {
Message: "Should not be allowed to run as root",
Type: "warning",
Category: "Security",
}, {
Message: "Filesystem should be read only",
Type: "warning",
Category: "Security",
}},
},
{
name: "bad security context + standard validation config from default set in podspec",
securityConf: standardConf,
cv: badCVWithBadPodSpec,
expectedMessages: []*ResultMessage{{
Message: "The following security capabilities should not be added: SYS_ADMIN, NET_ADMIN",
Type: "error",
Category: "Security",
}, {
Message: "Privilege escalation should not be allowed",
Type: "error",
Category: "Security",
}, {
Message: "Should not be running as privileged",
Type: "error",
Category: "Security",
}, {
Message: "The following security capabilities should not be added: AUDIT_CONTROL, SYS_ADMIN, NET_ADMIN",
Type: "warning",
Category: "Security",
}, {
Message: "Should not be allowed to run as root",
Type: "warning",
Category: "Security",
}, {
Message: "Filesystem should be read only",
Type: "warning",
Category: "Security",
}},
},
{
name: "good security context + standard validation config",
securityConf: standardConf,
@@ -739,6 +871,58 @@ func TestValidateSecurity(t *testing.T) {
Category: "Security",
}},
},
{
name: "strong security context + strong validation config via podspec default",
securityConf: strongConf,
cv: strongCVWithPodSpecSecurityContext,
expectedMessages: []*ResultMessage{{
Message: "Is not allowed to run as root",
Type: "success",
Category: "Security",
}, {
Message: "Filesystem is read only",
Type: "success",
Category: "Security",
}, {
Message: "Not running as privileged",
Type: "success",
Category: "Security",
}, {
Message: "Privilege escalation not allowed",
Type: "success",
Category: "Security",
}, {
Message: "Security capabilities are within the configured limits",
Type: "success",
Category: "Security",
}},
},
{
name: "strong security context + strong validation config with bad setting in podspec default",
securityConf: strongConf,
cv: strongCVWithBadPodSpecSecurityContext,
expectedMessages: []*ResultMessage{{
Message: "Is not allowed to run as root",
Type: "success",
Category: "Security",
}, {
Message: "Filesystem is read only",
Type: "success",
Category: "Security",
}, {
Message: "Not running as privileged",
Type: "success",
Category: "Security",
}, {
Message: "Privilege escalation not allowed",
Type: "success",
Category: "Security",
}, {
Message: "Security capabilities are within the configured limits",
Type: "success",
Category: "Security",
}},
},
}
for _, tt := range testCases {
+21 -64
View File
@@ -1,4 +1,4 @@
// Copyright 2019 ReactiveOps
// 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.
@@ -15,82 +15,39 @@
package validator
import (
conf "github.com/reactiveops/polaris/pkg/config"
"github.com/reactiveops/polaris/pkg/kube"
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"
"github.com/fairwindsops/polaris/pkg/validator/controllers"
controller "github.com/fairwindsops/polaris/pkg/validator/controllers"
"github.com/sirupsen/logrus"
)
// ControllerSpec is a generic type for k8s controller specs
type ControllerSpec struct {
Template corev1.PodTemplateSpec
}
// Controller is a generic type for k8s controllers (e.g. Deployments and StatefulSets)
type Controller struct {
Type string
Name string
Namespace string
Spec ControllerSpec
}
// ValidateController validates a single controller, returns a ControllerResult.
func ValidateController(conf conf.Configuration, controller Controller) ControllerResult {
pod := controller.Spec.Template.Spec
podResult := ValidatePod(conf, &pod)
func ValidateController(conf conf.Configuration, controller controller.Interface) ControllerResult {
pod := controller.GetPodSpec()
podResult := ValidatePod(conf, pod)
return ControllerResult{
Type: controller.Type,
Name: controller.Name,
Type: controller.GetType().String(),
Name: controller.GetName(),
PodResult: podResult,
}
}
// ValidateControllers validates that each deployment conforms to the Polaris config,
// returns a list of ResourceResults organized by namespace.
// builds a list of ResourceResults organized by namespace.
func ValidateControllers(config conf.Configuration, kubeResources *kube.ResourceProvider, nsResults *NamespacedResults) {
controllers := []Controller{}
for _, deploy := range kubeResources.Deployments {
controllers = append(controllers, ControllerFromDeployment(deploy))
var controllersToAudit []controller.Interface
for _, supportedControllers := range config.ControllersToScan {
loadedControllers, _ := controllers.LoadControllersByType(supportedControllers, kubeResources)
controllersToAudit = append(controllersToAudit, loadedControllers...)
}
for _, deploy := range kubeResources.StatefulSets {
controllers = append(controllers, ControllerFromStatefulSet(deploy))
}
for _, controller := range controllers {
for _, controller := range controllersToAudit {
controllerResult := ValidateController(config, controller)
nsResult := nsResults.getNamespaceResult(controller.Namespace)
nsResult := nsResults.getNamespaceResult(controller.GetNamespace())
nsResult.Summary.appendResults(*controllerResult.PodResult.Summary)
if controller.Type == "Deployment" {
nsResult.DeploymentResults = append(nsResult.DeploymentResults, controllerResult)
} else if controller.Type == "StatefulSet" {
nsResult.StatefulSetResults = append(nsResult.StatefulSetResults, controllerResult)
if err := nsResult.AddResult(controller.GetType(), controllerResult); err != nil {
logrus.Errorf("Internal Error: Failed to add a grouped result: %s", err)
}
}
}
// ControllerFrom* functions are 100% boilerplate
// ControllerFromDeployment creates a controller
func ControllerFromDeployment(c appsv1.Deployment) Controller {
spec := ControllerSpec{
Template: c.Spec.Template,
}
return Controller{
Type: "Deployment",
Name: c.Name,
Namespace: c.Namespace,
Spec: spec,
}
}
// ControllerFromStatefulSet creates a controller
func ControllerFromStatefulSet(c appsv1.StatefulSet) Controller {
spec := ControllerSpec{
Template: c.Spec.Template,
}
return Controller{
Type: "StatefulSet",
Name: c.Name,
Namespace: c.Namespace,
Spec: spec,
}
}
+37
View File
@@ -0,0 +1,37 @@
package controllers
import (
"github.com/fairwindsops/polaris/pkg/config"
kubeAPIBatchV1beta1 "k8s.io/api/batch/v1beta1"
kubeAPICoreV1 "k8s.io/api/core/v1"
)
// CronJobController is an implementation of controller for deployments
type CronJobController struct {
GenericController
K8SResource kubeAPIBatchV1beta1.CronJob
}
// GetPodTemplate returns the original template spec
func (c CronJobController) GetPodTemplate() *kubeAPICoreV1.PodTemplateSpec {
return &c.K8SResource.Spec.JobTemplate.Spec.Template
}
// GetPodSpec returns the original kubernetes template pod spec
func (c CronJobController) GetPodSpec() *kubeAPICoreV1.PodSpec {
return &c.K8SResource.Spec.JobTemplate.Spec.Template.Spec
}
// GetType returns the supportedcontroller enum type
func (c CronJobController) GetType() config.SupportedController {
return config.CronJobs
}
// NewCronJobController builds a new controller interface for Deployments
func NewCronJobController(originalDeploymentResource kubeAPIBatchV1beta1.CronJob) Interface {
controller := CronJobController{}
controller.Name = originalDeploymentResource.Name
controller.Namespace = originalDeploymentResource.Namespace
controller.K8SResource = originalDeploymentResource
return controller
}
+37
View File
@@ -0,0 +1,37 @@
package controllers
import (
"github.com/fairwindsops/polaris/pkg/config"
kubeAPIAppsV1 "k8s.io/api/apps/v1"
kubeAPICoreV1 "k8s.io/api/core/v1"
)
// DaemonSetController is an implementation of controller for deployments
type DaemonSetController struct {
GenericController
K8SResource kubeAPIAppsV1.DaemonSet
}
// GetPodTemplate returns the original template spec
func (d DaemonSetController) GetPodTemplate() *kubeAPICoreV1.PodTemplateSpec {
return &d.K8SResource.Spec.Template
}
// GetPodSpec returns the original kubernetes template pod spec
func (d DaemonSetController) GetPodSpec() *kubeAPICoreV1.PodSpec {
return &d.K8SResource.Spec.Template.Spec
}
// GetType returns the supportedcontroller enum type
func (d DaemonSetController) GetType() config.SupportedController {
return config.DaemonSets
}
// NewDaemonSetController builds a new controller interface for Deployments
func NewDaemonSetController(originalResource kubeAPIAppsV1.DaemonSet) Interface {
controller := DaemonSetController{}
controller.Name = originalResource.Name
controller.Namespace = originalResource.Namespace
controller.K8SResource = originalResource
return controller
}
+37
View File
@@ -0,0 +1,37 @@
package controllers
import (
"github.com/fairwindsops/polaris/pkg/config"
kubeAPIAppsV1 "k8s.io/api/apps/v1"
kubeAPICoreV1 "k8s.io/api/core/v1"
)
// DeploymentController is an implementation of controller for deployments
type DeploymentController struct {
GenericController
K8SResource kubeAPIAppsV1.Deployment
}
// GetPodTemplate returns the original template spec
func (d DeploymentController) GetPodTemplate() *kubeAPICoreV1.PodTemplateSpec {
return &d.K8SResource.Spec.Template
}
// GetPodSpec returns the original kubernetes template pod spec
func (d DeploymentController) GetPodSpec() *kubeAPICoreV1.PodSpec {
return &d.K8SResource.Spec.Template.Spec
}
// GetType returns the supportedcontroller enum type
func (d DeploymentController) GetType() config.SupportedController {
return config.Deployments
}
// NewDeploymentController builds a new controller interface for Deployments
func NewDeploymentController(originalDeploymentResource kubeAPIAppsV1.Deployment) Interface {
controller := DeploymentController{}
controller.Name = originalDeploymentResource.Name
controller.Namespace = originalDeploymentResource.Namespace
controller.K8SResource = originalDeploymentResource
return controller
}
+69
View File
@@ -0,0 +1,69 @@
package controllers
import (
"fmt"
"github.com/fairwindsops/polaris/pkg/config"
"github.com/fairwindsops/polaris/pkg/kube"
kubeAPICoreV1 "k8s.io/api/core/v1"
)
// Interface is an interface for k8s controllers (e.g. Deployments and StatefulSets)
type Interface interface {
GetName() string
GetNamespace() string
GetPodTemplate() *kubeAPICoreV1.PodTemplateSpec
GetPodSpec() *kubeAPICoreV1.PodSpec
GetType() config.SupportedController
}
// GenericController is a base implementation with some free methods for inherited structs
type GenericController struct {
Name string
Namespace string
}
// GetName is inherited by all controllers using generic controller to get the name of the controller
func (g GenericController) GetName() string {
return g.Name
}
// GetNamespace is inherited by all controllers using generic controller to get the namespace of the controller
func (g GenericController) GetNamespace() string {
return g.Namespace
}
// LoadControllersByType loads a list of controllers from the kubeResources by detecting their type
func LoadControllersByType(controllerType config.SupportedController, kubeResources *kube.ResourceProvider) ([]Interface, error) {
interfaces := []Interface{}
switch controllerType {
case config.Deployments:
for _, deploy := range kubeResources.Deployments {
interfaces = append(interfaces, NewDeploymentController(deploy))
}
case config.StatefulSets:
for _, statefulSet := range kubeResources.StatefulSets {
interfaces = append(interfaces, NewStatefulSetController(statefulSet))
}
case config.DaemonSets:
for _, daemonSet := range kubeResources.DaemonSets {
interfaces = append(interfaces, NewDaemonSetController(daemonSet))
}
case config.Jobs:
for _, job := range kubeResources.Jobs {
interfaces = append(interfaces, NewJobController(job))
}
case config.CronJobs:
for _, cronJob := range kubeResources.CronJobs {
interfaces = append(interfaces, NewCronJobController(cronJob))
}
case config.ReplicationControllers:
for _, replicationController := range kubeResources.ReplicationControllers {
interfaces = append(interfaces, NewReplicationControllerController(replicationController))
}
}
if len(interfaces) > 0 {
return interfaces, nil
}
return nil, fmt.Errorf("Controller type (%s) does not have a generator", controllerType)
}
+37
View File
@@ -0,0 +1,37 @@
package controllers
import (
"github.com/fairwindsops/polaris/pkg/config"
kubeAPIBatchV1 "k8s.io/api/batch/v1"
kubeAPICoreV1 "k8s.io/api/core/v1"
)
// JobController is an implementation of controller for deployments
type JobController struct {
GenericController
K8SResource kubeAPIBatchV1.Job
}
// GetPodTemplate returns the original template spec
func (j JobController) GetPodTemplate() *kubeAPICoreV1.PodTemplateSpec {
return &j.K8SResource.Spec.Template
}
// GetPodSpec returns the original kubernetes template pod spec
func (j JobController) GetPodSpec() *kubeAPICoreV1.PodSpec {
return &j.K8SResource.Spec.Template.Spec
}
// GetType returns the supportedcontroller enum type
func (j JobController) GetType() config.SupportedController {
return config.Jobs
}
// NewJobController builds a new controller interface for Deployments
func NewJobController(originalResource kubeAPIBatchV1.Job) Interface {
controller := JobController{}
controller.Name = originalResource.Name
controller.Namespace = originalResource.Namespace
controller.K8SResource = originalResource
return controller
}
@@ -0,0 +1,39 @@
package controllers
import (
"github.com/fairwindsops/polaris/pkg/config"
kubeAPICoreV1 "k8s.io/api/core/v1"
)
// NOTE: Maybe this name of ReplicationController is duplicative but it's more explicit since
// that's how kubernetes refers the the object.
// ReplicationControllerController is an implementation of controller for deployments
type ReplicationControllerController struct {
GenericController
K8SResource kubeAPICoreV1.ReplicationController
}
// GetPodTemplate returns the original template spec
func (r ReplicationControllerController) GetPodTemplate() *kubeAPICoreV1.PodTemplateSpec {
return r.K8SResource.Spec.Template
}
// GetPodSpec returns the original kubernetes template pod spec
func (r ReplicationControllerController) GetPodSpec() *kubeAPICoreV1.PodSpec {
return &r.K8SResource.Spec.Template.Spec
}
// GetType returns the supportedcontroller enum type
func (r ReplicationControllerController) GetType() config.SupportedController {
return config.ReplicationControllers
}
// NewReplicationControllerController builds a new controller interface for Deployments
func NewReplicationControllerController(originalResource kubeAPICoreV1.ReplicationController) Interface {
controller := ReplicationControllerController{}
controller.Name = originalResource.Name
controller.Namespace = originalResource.Namespace
controller.K8SResource = originalResource
return controller
}
+37
View File
@@ -0,0 +1,37 @@
package controllers
import (
"github.com/fairwindsops/polaris/pkg/config"
kubeAPIAppsV1 "k8s.io/api/apps/v1"
kubeAPICoreV1 "k8s.io/api/core/v1"
)
// StatefulSetController is an implementation of controller for deployments
type StatefulSetController struct {
GenericController
K8SResource kubeAPIAppsV1.StatefulSet
}
// GetPodTemplate returns the kubernetes template spec
func (s StatefulSetController) GetPodTemplate() *kubeAPICoreV1.PodTemplateSpec {
return &s.K8SResource.Spec.Template
}
// GetPodSpec returns the podspec from the original kubernetes resource
func (s StatefulSetController) GetPodSpec() *kubeAPICoreV1.PodSpec {
return &s.K8SResource.Spec.Template.Spec
}
// GetType returns the supportedcontroller enum type
func (s StatefulSetController) GetType() config.SupportedController {
return config.StatefulSets
}
// NewStatefulSetController builds a statefulset controller
func NewStatefulSetController(originalResource kubeAPIAppsV1.StatefulSet) Interface {
controller := StatefulSetController{}
controller.Name = originalResource.Name
controller.Namespace = originalResource.Namespace
controller.K8SResource = originalResource
return controller
}
+28 -25
View File
@@ -3,8 +3,8 @@ package validator
import (
"time"
conf "github.com/reactiveops/polaris/pkg/config"
"github.com/reactiveops/polaris/pkg/kube"
conf "github.com/fairwindsops/polaris/pkg/config"
"github.com/fairwindsops/polaris/pkg/kube"
)
const (
@@ -14,14 +14,18 @@ const (
// 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
Score uint
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
@@ -43,13 +47,8 @@ func RunAudit(config conf.Configuration, kubeResources *kube.ResourceProvider) (
clusterResults := ResultSummary{}
// Aggregate all summary counts to get a clusterwide count.
for _, nsRes := range nsResults {
for _, dr := range nsRes.DeploymentResults {
clusterResults.appendResults(*dr.PodResult.Summary)
}
for _, dr := range nsRes.StatefulSetResults {
clusterResults.appendResults(*dr.PodResult.Summary)
}
for _, result := range nsResults.GetAllControllerResults() {
clusterResults.appendResults(*result.PodResult.Summary)
}
displayName := config.DisplayName
@@ -64,14 +63,18 @@ func RunAudit(config conf.Configuration, kubeResources *kube.ResourceProvider) (
SourceName: kubeResources.SourceName,
DisplayName: displayName,
ClusterSummary: ClusterSummary{
Version: kubeResources.ServerVersion,
Nodes: len(kubeResources.Nodes),
Pods: len(kubeResources.Pods),
Namespaces: len(kubeResources.Namespaces),
Deployments: len(kubeResources.Deployments),
StatefulSets: len(kubeResources.StatefulSets),
Results: clusterResults,
Score: clusterResults.Totals.GetScore(),
Version: kubeResources.ServerVersion,
Nodes: len(kubeResources.Nodes),
Pods: len(kubeResources.Pods),
Namespaces: len(kubeResources.Namespaces),
Deployments: len(kubeResources.Deployments),
StatefulSets: len(kubeResources.StatefulSets),
DaemonSets: len(kubeResources.DaemonSets),
Jobs: len(kubeResources.Jobs),
CronJobs: len(kubeResources.CronJobs),
ReplicationControllers: len(kubeResources.ReplicationControllers),
Results: clusterResults,
Score: clusterResults.Totals.GetScore(),
},
NamespacedResults: nsResults,
}
+18 -9
View File
@@ -3,9 +3,9 @@ package validator
import (
"testing"
conf "github.com/reactiveops/polaris/pkg/config"
"github.com/reactiveops/polaris/pkg/kube"
"github.com/reactiveops/polaris/test"
conf "github.com/fairwindsops/polaris/pkg/config"
"github.com/fairwindsops/polaris/pkg/kube"
"github.com/fairwindsops/polaris/test"
"github.com/stretchr/testify/assert"
)
@@ -20,23 +20,32 @@ func TestGetTemplateData(t *testing.T) {
ReadinessProbeMissing: conf.SeverityError,
LivenessProbeMissing: conf.SeverityWarning,
},
ControllersToScan: []conf.SupportedController{
conf.Deployments,
conf.StatefulSets,
conf.DaemonSets,
conf.Jobs,
conf.CronJobs,
conf.ReplicationControllers,
},
}
// TODO: split out the logic for calculating summaries into another set of tests
sum := ResultSummary{
Totals: CountSummary{
Successes: uint(8),
Warnings: uint(2),
Errors: uint(2),
Successes: uint(24),
Warnings: uint(6),
Errors: uint(6),
},
ByCategory: CategorySummary{},
}
sum.ByCategory["Health Checks"] = &CountSummary{
Successes: uint(0),
Warnings: uint(2),
Errors: uint(2),
Warnings: uint(6),
Errors: uint(6),
}
sum.ByCategory["Resources"] = &CountSummary{
Successes: uint(8),
Successes: uint(24),
Warnings: uint(0),
Errors: uint(0),
}
+5 -4
View File
@@ -1,4 +1,4 @@
// Copyright 2019 ReactiveOps
// 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.
@@ -15,8 +15,8 @@
package validator
import (
conf "github.com/reactiveops/polaris/pkg/config"
"github.com/reactiveops/polaris/pkg/validator/messages"
conf "github.com/fairwindsops/polaris/pkg/config"
"github.com/fairwindsops/polaris/pkg/validator/messages"
corev1 "k8s.io/api/core/v1"
)
@@ -40,6 +40,7 @@ func ValidatePod(podConf conf.Configuration, pod *corev1.PodSpec) PodResult {
Messages: pv.messages(),
ContainerResults: []ContainerResult{},
Summary: pv.summary(),
podSpec: *pod,
}
pv.validateContainers(pod.InitContainers, &pRes, &podConf, true)
@@ -54,7 +55,7 @@ func ValidatePod(podConf conf.Configuration, pod *corev1.PodSpec) PodResult {
func (pv *PodValidation) validateContainers(containers []corev1.Container, pRes *PodResult, podConf *conf.Configuration, isInit bool) {
for _, container := range containers {
cRes := ValidateContainer(podConf, &container, isInit)
cRes := ValidateContainer(&container, pRes, podConf, isInit)
pRes.ContainerResults = append(pRes.ContainerResults, cRes)
}
}
+169 -6
View File
@@ -1,4 +1,4 @@
// Copyright 2019 ReactiveOps
// 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.
@@ -17,8 +17,8 @@ package validator
import (
"testing"
conf "github.com/reactiveops/polaris/pkg/config"
"github.com/reactiveops/polaris/test"
conf "github.com/fairwindsops/polaris/pkg/config"
"github.com/fairwindsops/polaris/test"
"github.com/stretchr/testify/assert"
)
@@ -70,7 +70,170 @@ func TestValidatePod(t *testing.T) {
actualPodResult := ValidatePod(c, &pod.Spec)
assert.Equal(t, len(actualPodResult.ContainerResults), 1, "should be equal")
assert.EqualValues(t, actualPodResult.Summary, &expectedSum)
assert.EqualValues(t, actualPodResult.Messages, expectedMessages)
assert.Equal(t, 1, len(actualPodResult.ContainerResults), "should be equal")
assert.EqualValues(t, &expectedSum, actualPodResult.Summary)
assert.EqualValues(t, expectedMessages, actualPodResult.Messages)
}
func TestInvalidIPCPod(t *testing.T) {
c := conf.Configuration{
Security: conf.Security{
HostIPCSet: conf.SeverityError,
HostPIDSet: conf.SeverityError,
},
Networking: conf.Networking{
HostNetworkSet: conf.SeverityWarning,
HostPortSet: conf.SeverityError,
},
}
k8s := test.SetupTestAPI()
k8s = test.SetupAddControllers(k8s, "test")
pod := test.MockPod()
pod.Spec.HostIPC = true
expectedSum := ResultSummary{
Totals: CountSummary{
Successes: uint(7),
Warnings: uint(0),
Errors: uint(1),
},
ByCategory: make(map[string]*CountSummary),
}
expectedSum.ByCategory["Networking"] = &CountSummary{
Successes: uint(2),
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"},
}
actualPodResult := ValidatePod(c, &pod.Spec)
assert.Equal(t, 1, len(actualPodResult.ContainerResults), "should be equal")
assert.EqualValues(t, &expectedSum, actualPodResult.Summary)
assert.EqualValues(t, expectedMessages, actualPodResult.Messages)
}
func TestInvalidNeworkPod(t *testing.T) {
c := conf.Configuration{
Networking: conf.Networking{
HostNetworkSet: conf.SeverityWarning,
HostPortSet: conf.SeverityError,
},
Security: conf.Security{
HostIPCSet: conf.SeverityError,
HostPIDSet: conf.SeverityError,
},
}
k8s := test.SetupTestAPI()
k8s = test.SetupAddControllers(k8s, "test")
pod := test.MockPod()
pod.Spec.HostNetwork = true
expectedSum := ResultSummary{
Totals: CountSummary{
Successes: uint(7),
Warnings: uint(1),
Errors: uint(0),
},
ByCategory: make(map[string]*CountSummary),
}
expectedSum.ByCategory["Networking"] = &CountSummary{
Successes: uint(1),
Warnings: uint(1),
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),
Errors: uint(0),
}
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"},
}
actualPodResult := ValidatePod(c, &pod.Spec)
assert.Equal(t, 1, len(actualPodResult.ContainerResults), "should be equal")
assert.EqualValues(t, &expectedSum, actualPodResult.Summary)
assert.EqualValues(t, expectedMessages, actualPodResult.Messages)
}
func TestInvalidPIDPod(t *testing.T) {
c := conf.Configuration{
Security: conf.Security{
HostIPCSet: conf.SeverityError,
HostPIDSet: conf.SeverityError,
},
Networking: conf.Networking{
HostNetworkSet: conf.SeverityWarning,
HostPortSet: conf.SeverityError,
},
}
k8s := test.SetupTestAPI()
k8s = test.SetupAddControllers(k8s, "test")
pod := test.MockPod()
pod.Spec.HostPID = true
expectedSum := ResultSummary{
Totals: CountSummary{
Successes: uint(7),
Warnings: uint(0),
Errors: uint(1),
},
ByCategory: make(map[string]*CountSummary),
}
expectedSum.ByCategory["Networking"] = &CountSummary{
Successes: uint(2),
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 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"},
}
actualPodResult := ValidatePod(c, &pod.Spec)
assert.Equal(t, 1, len(actualPodResult.ContainerResults), "should be equal")
assert.EqualValues(t, &expectedSum, actualPodResult.Summary)
assert.EqualValues(t, expectedMessages, actualPodResult.Messages)
}
+2 -2
View File
@@ -1,4 +1,4 @@
// Copyright 2019 ReactiveOps
// 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.
@@ -15,7 +15,7 @@
package validator
import (
conf "github.com/reactiveops/polaris/pkg/config"
conf "github.com/fairwindsops/polaris/pkg/config"
"github.com/sirupsen/logrus"
)
+81 -8
View File
@@ -1,4 +1,4 @@
// Copyright 2019 ReactiveOps
// 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.
@@ -14,6 +14,14 @@
package validator
import (
"fmt"
"github.com/fairwindsops/polaris/pkg/config"
conf "github.com/fairwindsops/polaris/pkg/config"
corev1 "k8s.io/api/core/v1"
)
// MessageType represents the type of Message
type MessageType string
@@ -30,23 +38,87 @@ const (
// NamespaceResult groups container results by parent resource.
type NamespaceResult struct {
Name string
Summary *ResultSummary
DeploymentResults []ControllerResult
StatefulSetResults []ControllerResult
Name string
Summary *ResultSummary
// TODO: This struct could use some love to reorganize it as just having "results"
// and then having methods to return filtered results by type
// (deploy, daemonset, etc)
// The way this is structured right now makes it difficult to add
// additional result types and potentially miss things in the metrics
// summary.
DeploymentResults []ControllerResult
StatefulSetResults []ControllerResult
DaemonSetResults []ControllerResult
JobResults []ControllerResult
CronJobResults []ControllerResult
ReplicationControllerResults []ControllerResult
}
// AddResult adds a result to the result sets by leveraging the types supported by NamespaceResult
func (n *NamespaceResult) AddResult(resourceType config.SupportedController, result ControllerResult) error {
// Iterate all the resource types supported in this struct
var results *[]ControllerResult
switch resourceType {
case conf.Deployments:
results = &n.DeploymentResults
case conf.StatefulSets:
results = &n.StatefulSetResults
case conf.DaemonSets:
results = &n.DaemonSetResults
case conf.Jobs:
results = &n.JobResults
case conf.CronJobs:
results = &n.CronJobResults
case conf.ReplicationControllers:
results = &n.ReplicationControllerResults
default:
return fmt.Errorf("Unknown Resource Type: (%s) Missing Implementation in NamespacedResult", resourceType)
}
// Append the new result to the results pointer loaded from the supported values
*results = append(*results, result)
return nil
}
// GetAllControllerResults grabs all the different types of controller results from the namespaced result as a single list for easier iteration
func (n NamespaceResult) GetAllControllerResults() []ControllerResult {
all := []ControllerResult{}
all = append(all, n.DeploymentResults...)
all = append(all, n.StatefulSetResults...)
all = append(all, n.DaemonSetResults...)
all = append(all, n.JobResults...)
all = append(all, n.CronJobResults...)
all = append(all, n.ReplicationControllerResults...)
return all
}
// NamespacedResults is a mapping of namespace name to the validation results.
type NamespacedResults map[string]*NamespaceResult
// GetAllControllerResults aggregates all the namespaced results in the set together
func (nsResults NamespacedResults) GetAllControllerResults() []ControllerResult {
all := []ControllerResult{}
for _, nsResult := range nsResults {
all = append(all, nsResult.GetAllControllerResults()...)
}
return all
}
func (nsResults NamespacedResults) getNamespaceResult(nsName string) *NamespaceResult {
nsResult := &NamespaceResult{}
switch nsResults[nsName] {
case nil:
nsResult = &NamespaceResult{
Summary: &ResultSummary{},
DeploymentResults: []ControllerResult{},
StatefulSetResults: []ControllerResult{},
Summary: &ResultSummary{},
DeploymentResults: []ControllerResult{},
StatefulSetResults: []ControllerResult{},
DaemonSetResults: []ControllerResult{},
JobResults: []ControllerResult{},
CronJobResults: []ControllerResult{},
ReplicationControllerResults: []ControllerResult{},
}
nsResults[nsName] = nsResult
default:
@@ -116,6 +188,7 @@ type PodResult struct {
Summary *ResultSummary
Messages []*ResultMessage
ContainerResults []ContainerResult
podSpec corev1.PodSpec
}
// ResultMessage contains a message and a type indicator (success, warning, or error).
+43 -9
View File
@@ -1,4 +1,4 @@
// Copyright 2019 ReactiveOps
// 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.
@@ -20,11 +20,14 @@ import (
"net/http"
"os"
conf "github.com/reactiveops/polaris/pkg/config"
validator "github.com/reactiveops/polaris/pkg/validator"
conf "github.com/fairwindsops/polaris/pkg/config"
validator "github.com/fairwindsops/polaris/pkg/validator"
"github.com/fairwindsops/polaris/pkg/validator/controllers"
"github.com/sirupsen/logrus"
admissionregistrationv1beta1 "k8s.io/api/admissionregistration/v1beta1"
appsv1 "k8s.io/api/apps/v1"
batchv1 "k8s.io/api/batch/v1"
batchv1beta1 "k8s.io/api/batch/v1beta1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
@@ -92,16 +95,47 @@ func (v *Validator) Handle(ctx context.Context, req types.Request) types.Respons
err = v.decoder.Decode(req, &pod)
podResult = validator.ValidatePod(v.Config, &pod.Spec)
} else {
var controller validator.Controller
switch req.AdmissionRequest.Kind.Kind {
case "Deployment":
var controller controllers.Interface
if yes := v.Config.CheckIfKindIsConfiguredForValidation(req.AdmissionRequest.Kind.Kind); !yes {
logrus.Warnf("Skipping, kind (%s) isn't something we are configured to scan", req.AdmissionRequest.Kind.Kind)
return admission.ValidationResponse(true, fmt.Sprintf("Skipping: (%s) isn't something we're configured to scan.", req.AdmissionRequest.Kind.Kind))
}
// We should never hit this case unless something is misconfiured in CheckIfKindIsConfiguredForValidation
controllerType, err := conf.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)
return admission.ErrorResponse(http.StatusInternalServerError, err)
}
// 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:
deploy := appsv1.Deployment{}
err = v.decoder.Decode(req, &deploy)
controller = validator.ControllerFromDeployment(deploy)
case "StatefulSet":
controller = controllers.NewDeploymentController(deploy)
case conf.StatefulSets:
statefulSet := appsv1.StatefulSet{}
err = v.decoder.Decode(req, &statefulSet)
controller = validator.ControllerFromStatefulSet(statefulSet)
controller = controllers.NewStatefulSetController(statefulSet)
case conf.DaemonSets:
daemonSet := appsv1.DaemonSet{}
err = v.decoder.Decode(req, &daemonSet)
controller = controllers.NewDaemonSetController(daemonSet)
case conf.Jobs:
job := batchv1.Job{}
err = v.decoder.Decode(req, &job)
controller = controllers.NewJobController(job)
case conf.CronJobs:
cronJob := batchv1beta1.CronJob{}
err = v.decoder.Decode(req, &cronJob)
controller = controllers.NewCronJobController(cronJob)
case conf.ReplicationControllers:
replicationController := corev1.ReplicationController{}
err = v.decoder.Decode(req, &replicationController)
controller = controllers.NewReplicationControllerController(replicationController)
}
controllerResult := validator.ValidateController(v.Config, controller)
podResult = controllerResult.PodResult
+63 -4
View File
@@ -4,6 +4,8 @@ import (
"fmt"
appsv1 "k8s.io/api/apps/v1"
batchv1 "k8s.io/api/batch/v1"
batchv1beta1 "k8s.io/api/batch/v1beta1"
corev1 "k8s.io/api/core/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/kubernetes/fake"
@@ -49,6 +51,43 @@ func mockStatefulSet() appsv1.StatefulSet {
return s
}
func mockDaemonSet() appsv1.DaemonSet {
return appsv1.DaemonSet{
Spec: appsv1.DaemonSetSpec{
Template: MockPod(),
},
}
}
func mockJob() batchv1.Job {
return batchv1.Job{
Spec: batchv1.JobSpec{
Template: MockPod(),
},
}
}
func mockCronJob() batchv1beta1.CronJob {
return batchv1beta1.CronJob{
Spec: batchv1beta1.CronJobSpec{
JobTemplate: batchv1beta1.JobTemplateSpec{
Spec: batchv1.JobSpec{
Template: MockPod(),
},
},
},
}
}
func mockReplicationController() corev1.ReplicationController {
p := MockPod()
return corev1.ReplicationController{
Spec: corev1.ReplicationControllerSpec{
Template: &p,
},
}
}
// SetupTestAPI creates a test kube API struct.
func SetupTestAPI() kubernetes.Interface {
return fake.NewSimpleClientset()
@@ -57,14 +96,34 @@ 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()
_, err := k.AppsV1().Deployments(namespace).Create(&d1)
if err != nil {
if _, err := k.AppsV1().Deployments(namespace).Create(&d1); err != nil {
fmt.Println(err)
}
s1 := mockStatefulSet()
_, err = k.AppsV1().StatefulSets(namespace).Create(&s1)
if err != nil {
if _, err := k.AppsV1().StatefulSets(namespace).Create(&s1); err != nil {
fmt.Println(err)
}
ds1 := mockDaemonSet()
if _, err := k.AppsV1().DaemonSets(namespace).Create(&ds1); err != nil {
fmt.Println(err)
}
j1 := mockJob()
if _, err := k.BatchV1().Jobs(namespace).Create(&j1); err != nil {
fmt.Println(err)
}
cj1 := mockCronJob()
if _, err := k.BatchV1beta1().CronJobs(namespace).Create(&cj1); err != nil {
fmt.Println(err)
}
rc1 := mockReplicationController()
if _, err := k.CoreV1().ReplicationControllers(namespace).Create(&rc1); err != nil {
fmt.Println(err)
}
return k
}