Compare commits

..
22 Commits
Author SHA1 Message Date
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
Bobby Brennan 41e64f7405 update to 0.2.0 (#147) 2019-06-14 05:58:40 -06:00
Bobby Brennan 50d32b9b4c move helm chart to reactiveops/charts (#128)
* move helm chart to reactiveops/charts

init helm in circleci

fix template command

* update release process
2019-06-13 13:07:10 -06:00
Bobby Brennan ebfb4ea9a1 Add support for stateful sets (#145) 2019-06-13 07:59:01 -06:00
Kim Schlesinger e030b61705 Merge pull request #144 from reactiveops/ks/adjust-os-exit
adjust os exit
2019-06-11 16:29:38 -05:00
kimschles b32a7afa77 update tests 2019-06-11 13:46:12 -06:00
kimschles 30ff6e6667 [WIP] change GetConfigOrDie to GetConfig 2019-06-10 13:34:09 -06:00
Kim Schlesinger 6fa27eed76 Merge pull request #138 from reactiveops/rb/cluster-name
Show cluster name/host on dashboard
2019-06-10 12:04:40 -05:00
Bobby Brennan 538b1fba1c fix padding on top-level header 2019-06-10 14:50:37 +00:00
Bobby Brennan d569605d0d add tests for creation time 2019-06-10 14:43:21 +00:00
Bobby Brennan ca4b45451f re-bump output version 2019-06-10 14:43:15 +00:00
Bobby Brennan 1d6248180e add display-name flag 2019-06-10 14:39:24 +00:00
Bobby Brennan 7cfa13f285 Better support for CI/CD use case (#127)
add score to output

add output-format option

update README with more use cases

change YAML marshal strategy

fix webhook install instructions
2019-06-10 10:13:10 -04:00
Bobby Brennan 0bd8f8f507 Tweak CI for forked PRs and better release process (#115)
* build new image upon release

* skip certain actions during CI on forked PRs
2019-06-07 13:47:20 -04:00
Kim Schlesinger 62b914d743 Merge pull request #140 from reactiveops/ks/dev-setup
add instructions for local development
2019-06-07 11:08:39 -05:00
kimschles 65c0f07284 update path to project and add url for cla 2019-06-07 09:49:06 -06:00
kimschles e3c0d53df5 remove extra lines 2019-06-06 15:51:53 -06:00
kimschles 8ad223f6f7 Merge branch 'master' of github.com:reactiveops/polaris into ks/dev-setup 2019-06-06 15:28:38 -06:00
kimschles ac44416d0f add instructions for how to setup a dev environment 2019-06-06 15:26:53 -06:00
Bobby Brennan 3c72515946 Fix office hours link (#139) 2019-06-06 16:33:27 -04:00
41 changed files with 912 additions and 1377 deletions
+10
View File
@@ -0,0 +1,10 @@
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
+82 -66
View File
@@ -1,6 +1,53 @@
version: 2
references:
set_environment_variables: &set_environment_variables
run:
name: Set Environment Variables
command: |
echo 'export CI_SHA1=$CIRCLE_SHA1' >> ${BASH_ENV}
echo 'export CI_BRANCH=$CIRCLE_BRANCH' >> ${BASH_ENV}
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
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
# Test scripts
update_coverage: &update_coverage
run:
name: Update Coverage
command: |
if [[ -z $CIRCLE_PR_NUMBER ]]; then
go test ./pkg/... -coverprofile=coverage.txt -covermode=count
bash <(curl -s https://codecov.io/bash)
else
echo "Skipping coverage for forked PR"
fi
test_dashboard: &test_dashboard
run:
name: Test Dashboard
command: |
go run main.go --dashboard --dashboard-port 3000 --audit-path ./examples &
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
run:
name: Install GoReleaser
@@ -9,46 +56,20 @@ references:
echo "8dbad6683d6fc9367e637e6eed8e01a0d63c9660 goreleaser.deb" | sha1sum -c
sudo dpkg -i goreleaser.deb
rm goreleaser.deb
set_environment_variables: &set_environment_variables
release_deploy_configs: &release_deploy_configs
run:
name: Set Environment Variables
name: Release deploy configs
command: |
echo 'export REPO=quay.io/reactiveops/polaris' >> ${BASH_ENV}
docker_build_and_push: &docker_build_and_push
run:
name: Docker login, build, and push
command: |
docker login quay.io -u="reactiveops+circleci" -p="${quay_token}"
docker build -t $REPO:dev-$CIRCLE_SHA1 .
docker push $REPO:dev-$CIRCLE_SHA1
docker_tag_release: &docker_tag_release
run:
name: Docker tag release
command: |
docker login quay.io -u="reactiveops+circleci" -p="${quay_token}"
docker pull $REPO:dev-$CIRCLE_SHA1
docker tag $REPO:dev-$CIRCLE_SHA1 $REPO:$CIRCLE_TAG
docker push $REPO:$CIRCLE_TAG
update_coverage: &update_coverage
run:
name: Update Coverage
command: |
go test ./pkg/... -coverprofile=coverage.txt -covermode=count
bash <(curl -s https://codecov.io/bash)
test_dashboard: &test_dashboard
run:
name: Test Dashboard
command: |
go run main.go --dashboard --dashboard-port 3000 --audit-path ./deploy/dashboard.yaml &
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
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=$(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:
@@ -73,30 +94,7 @@ jobs:
- *update_coverage
- *test_dashboard
test-deploy:
docker:
- image: quay.io/reactiveops/ci-images:v8.0-stretch
steps:
- checkout
- run:
name: Verify helm chart synced with dashboard.yaml
command: |
diff \
<(helm template deploy/helm/polaris/ --name polaris --namespace polaris --set templateOnly=true) \
deploy/dashboard.yaml || (echo "
Make sure to regenerate k8s deploy config with make helm-to-k8s" &&
exit 1)
- run:
name: Verify helm chart synced with webhook.yaml
command: |
diff \
<(helm template deploy/helm/polaris/ --name polaris --namespace polaris --set templateOnly=true --set webhook.enable=true --set dashboard.enable=false) \
deploy/webhook.yaml || (echo "
Make sure to regenerate k8s deploy config with make helm-to-k8s" &&
exit 1)
release:
release_binary:
working_directory: /go/src/github.com/reactiveops/polaris/
docker:
- image: circleci/golang:1.12
@@ -104,22 +102,30 @@ jobs:
- checkout
- setup_remote_docker
- *set_environment_variables
- *docker_tag_release
- *install_goreleaser
- run: go get -u github.com/gobuffalo/packr/v2/packr2
- run: packr2
- run: goreleaser
release_images:
working_directory: /go/src/github.com/reactiveops/polaris/
docker:
- image: quay.io/reactiveops/ci-images:v8.0-stretch
steps:
- checkout
- setup_remote_docker
- *set_environment_variables
- *docker_build_and_push
- *release_deploy_configs
workflows:
version: 2
build:
jobs:
- test
- test-deploy
- build:
requires:
- test
- test-deploy
context: org-global
# Allow using testing tags for testing circle test + build steps
filters:
@@ -127,7 +133,17 @@ workflows:
only: /^testing-.*/
release:
jobs:
- release:
- release_binary:
context: org-global
filters:
branches:
ignore: /.*/
# Testing tags are reserved for testing circle test + build steps
tags:
ignore: /^testing-.*/
- release_images:
requires:
- release_binary
context: org-global
filters:
branches:
+32 -20
View File
@@ -1,3 +1,35 @@
# x.x.x (next release)
# 0.2.1
* [Fix](https://github.com/reactiveops/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
* Added support for StatefulSets
* 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
# 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
# 0.1.3
* [Fix](https://github.com/reactiveops/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
* Added some tests and better error handling
# 0.1.0
* Dashboard fully functional
* Validating webhook functional, but still considered beta
@@ -23,23 +55,3 @@
* privilege escalation allowed
* run as root allowed
* run as privileged
# 0.1.1
* [Fix](https://github.com/reactiveops/polaris/issues/93): missing `config.yaml` and dashboard assets in binary releases
* Added some tests and better error handling
# 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.3
* [Fix](https://github.com/reactiveops/polaris/issues/109): dashboard not updating when running persistently
# 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
# 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
+48 -16
View File
@@ -1,11 +1,14 @@
# Contributing
Issues, whether bugs, tasks, or feature requests are essential for keeping Polaris great. We believe it should be as easy as possible to contribute changes that get things working in your environment. There are a few guidelines that we need contributors to follow so that we can have a chance of keeping on top of things.
Issues, whether bugs, tasks, or feature requests are essential for keeping Polaris great. We believe it should be as easy as possible to contribute changes that get things working in your environment. There are a few guidelines that we need contributors to follow so that we can keep on top of things.
## Code of Conduct
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).
## Project Structure
Polaris is built on top of [controller-runtime](https://github.com/kubernetes-sigs/controller-runtime). It can run in 3 different modes, a dashboard, a webhook, or a reporter that prints or exports validation results. All of these modes make use of the shared `validator` and `config` packages. Adding new validations is possible by only making additions to those packages.
@@ -14,6 +17,17 @@ Polaris is built on top of [controller-runtime](https://github.com/kubernetes-si
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.
## Setting Up Your Development Environment
### Prerequisites
* A properly configured Golang environment with Go 1.11 or higher
* 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`
* 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.
## Running Tests
The following commands are all required to pass as part of Polaris testing:
@@ -43,18 +57,36 @@ Each new pull request should:
- Be up to date and/or rebased on the master branch
## Creating a new release
* Change the version number everywhere. E.g. for `0.1.4` to `0.1.5`:
```
find ./ -type f ! -path "./vendor/*" ! -path "./.git/*" ! -name CHANGELOG.md -exec sed -i 's/0\.1\.4/0.1.5/g' {} \;
```
* Add relevant entries to CHANGELOG.md
* Create a PR to merge your changes
* Once the PR is merged, wait for CircleCI to finish building the merge commit
* Tag and push the latest:
```
git checkout master
git pull
git tag $VERSION
git push --tags
```
* CircleCI will add an entry to the releases page on GitHub
### Minor/patch releases
Minor and 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.
### 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. Bump the version number in:
1. main.go
2. README.md
2. Update CHANGELOG.md
3. 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
View File
@@ -1,3 +0,0 @@
helm-to-k8s:
helm template deploy/helm/polaris/ --name polaris --namespace polaris --set templateOnly=true > deploy/dashboard.yaml
helm template deploy/helm/polaris/ --name polaris --namespace polaris --set templateOnly=true --set webhook.enable=true --set dashboard.enable=false > deploy/webhook.yaml
+82 -47
View File
@@ -5,7 +5,7 @@
[![Version][version-image]][version-link] [![CircleCI][circleci-image]][circleci-link] [![Go Report Card][goreport-image]][goreport-link]
</div>
[version-image]: https://img.shields.io/static/v1.svg?label=Version&message=0.1.5&color=239922
[version-image]: https://img.shields.io/static/v1.svg?label=Version&message=0.2.1&color=239922
[version-link]: https://github.com/reactiveops/polaris
[goreport-image]: https://goreportcard.com/badge/github.com/reactiveops/polaris
@@ -19,7 +19,15 @@ Polaris helps keep your cluster healthy. It runs a variety of checks to ensure t
- 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.
**Want to learn more?** ReactiveOps holds [office hours on Zoom](https://zoom.us/j/951540265) the first Friday of every month, at 12pm Eastern. You can also reach out via email at `opensource@reactiveops.com`
**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`
## Quickstart
```
kubectl apply -f https://github.com/reactiveops/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.
## Dashboard
@@ -31,63 +39,90 @@ 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.
### Deploying
To deploy Polaris with kubectl:
```
kubectl apply -f https://raw.githubusercontent.com/reactiveops/polaris/master/deploy/dashboard.yaml
```
Polaris can also be deployed with Helm:
```
helm upgrade --install polaris deploy/helm/polaris/ --namespace polaris
```
### Viewing the Dashboard
Once the dashboard is deployed, it can be viewed by using kubectl port-forward:
```
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.
### Using a Binary Release
If you'd prefer to run Polaris locally, 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
```
When running as a binary, Polaris will use your local kubeconfig to connect to a cluster. There are a variety of options available, but the most common usage will likely be to view the dashboard:
```
polaris --dashboard
```
## 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.
### Deploying
The Polaris webhook can be deployed with kubectl:
## 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.
### kubectl
#### Dashboard
```
kubectl apply -f https://raw.githubusercontent.com/reactiveops/polaris/master/deploy/webhook.yaml
kubectl apply -f https://github.com/reactiveops/polaris/releases/latest/download/dashboard.yaml
kubectl port-forward --namespace polaris svc/polaris-dashboard 8080:80
```
Alternatively, the webhook can be enabled with Helm by setting `webhook.enable` to true:
#### 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.
```
helm upgrade --install polaris deploy/helm/polaris/ --namespace polaris --set webhook.enable=true
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
-212
View File
@@ -1,212 +0,0 @@
---
# 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
---
# 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'
verbs:
- 'get'
- 'list'
- apiGroups:
- ''
resources:
- 'nodes'
- 'namespaces'
- 'pods'
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: '6ec4a4dc87403cae67c01438398a5f1d4ef836ffeaf26a33b685c066b940495a'
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.1.5'
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
---
# Source: polaris/templates/webhook.deployment.yaml
---
# Source: polaris/templates/webhook.rbac.yaml
---
# Source: polaris/templates/webhook.secret.yaml
---
# Source: polaris/templates/webhook.service.yaml
-4
View File
@@ -1,4 +0,0 @@
apiVersion: v1
description: Validation of best practices in your Kubernetes clusters
name: polaris
version: 0.1.5
@@ -1,6 +0,0 @@
{{- if .Values.templateOnly -}}
apiVersion: v1
kind: Namespace
metadata:
name: {{ .Release.Namespace }}
{{- end -}}
-12
View File
@@ -1,12 +0,0 @@
** Please be patient while the chart is being deployed **
Enjoy Polaris and smooth sailing!
{{- if contains "ClusterIP" .Values.dashboard.service.type }}
## To view the dashboard execute these commands:
kubectl port-forward --namespace {{ .Release.Namespace }} svc/{{ include "polaris.fullname" . }}-dashboard 8080:80 &
open http://localhost:8080
{{- end }}
@@ -1,56 +0,0 @@
{{/* vim: set filetype=mustache: */}}
{{/*
Expand the name of the chart.
*/}}
{{- define "polaris.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "polaris.fullname" -}}
{{- if .Values.fullnameOverride -}}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- $name := default .Chart.Name .Values.nameOverride -}}
{{- if contains $name .Release.Name -}}
{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "polaris.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{/*
Standard labels
*/}}
{{- define "polaris.labels" -}}
app: {{ include "polaris.name" . }}
{{- if not .Values.templateOnly }}
app.kubernetes.io/name: {{ include "polaris.name" . }}
helm.sh/chart: {{ include "polaris.chart" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end -}}
{{- end -}}
{{/*
Standard selector
*/}}
{{- define "polaris.selectors" -}}
app: {{ include "polaris.name" . }}
{{- if not .Values.templateOnly }}
app.kubernetes.io/name: {{ include "polaris.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end -}}
{{- end -}}
@@ -1,9 +0,0 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "polaris.fullname" . }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "polaris.labels" . | nindent 4 }}
data:
config.yaml: {{- toYaml .Values.config | indent 2 -}}
@@ -1,72 +0,0 @@
{{- if .Values.dashboard.enable -}}
apiVersion: apps/v1
kind: Deployment
metadata:
annotations:
checksum/config: '{{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}'
name: {{ include "polaris.fullname" . }}-dashboard
namespace: {{ .Release.Namespace }}
labels:
{{- include "polaris.labels" . | nindent 4 }}
component: dashboard
spec:
replicas: {{.Values.dashboard.replicas}}
selector:
matchLabels:
{{- include "polaris.selectors" . | nindent 6 }}
component: dashboard
template:
metadata:
labels:
{{- include "polaris.selectors" . | nindent 8 }}
component: dashboard
spec:
volumes:
- name: config
configMap:
name: {{ include "polaris.fullname" . }}
containers:
- command:
- polaris
- --dashboard
- --config
- /opt/app/config.yaml
image: '{{.Values.dashboard.image.repository}}:{{.Values.dashboard.image.tag}}'
imagePullPolicy: '{{.Values.dashboard.image.pullPolicy}}'
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: {{ include "polaris.fullname" . }}-dashboard
{{- end -}}
@@ -1,49 +0,0 @@
{{- if and .Values.dashboard.enable .Values.rbac.create -}}
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "polaris.fullname" . }}-dashboard
namespace: {{ .Release.Namespace }}
labels:
{{- include "polaris.labels" . | nindent 4 }}
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
metadata:
name: {{ include "polaris.fullname" . }}-dashboard
labels:
{{- include "polaris.labels" . | nindent 4 }}
rules:
- apiGroups:
- 'apps'
- 'extensions'
resources:
- 'deployments'
verbs:
- 'get'
- 'list'
- apiGroups:
- ''
resources:
- 'nodes'
- 'namespaces'
- 'pods'
verbs:
- 'get'
- 'list'
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
name: {{ include "polaris.fullname" . }}-dashboard
labels:
{{- include "polaris.labels" . | nindent 4 }}
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: {{ include "polaris.fullname" . }}-dashboard
subjects:
- kind: ServiceAccount
name: {{ include "polaris.fullname" . }}-dashboard
namespace: {{ .Release.Namespace }}
{{- end -}}
@@ -1,19 +0,0 @@
{{- if and .Values.dashboard.enable .Values.dashboard.service -}}
apiVersion: v1
kind: Service
metadata:
name: {{ include "polaris.fullname" . }}-dashboard
namespace: {{ .Release.Namespace }}
labels:
{{- include "polaris.labels" . | nindent 4 }}
spec:
ports:
- name: dashboard
port: 80
protocol: TCP
targetPort: 8080
selector:
{{- include "polaris.selectors" . | nindent 4 }}
component: dashboard
type: {{ .Values.dashboard.service.type }}
{{- end -}}
@@ -1,90 +0,0 @@
{{- if .Values.webhook.enable -}}
apiVersion: apps/v1
kind: Deployment
metadata:
annotations:
checksum/config: '{{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}'
name: {{ include "polaris.fullname" . }}-webhook
namespace: {{ .Release.Namespace }}
labels:
{{- include "polaris.labels" . | nindent 4 }}
component: webhook
spec:
replicas: {{ .Values.webhook.replicas }}
selector:
matchLabels:
{{- include "polaris.selectors" . | nindent 6 }}
component: webhook
template:
metadata:
labels:
{{- include "polaris.selectors" . | nindent 8 }}
component: webhook
spec:
containers:
- name: webhook
command:
- polaris
- --webhook
- --config
- /opt/app/config.yaml
image: '{{.Values.webhook.image.repository}}:{{.Values.webhook.image.tag}}'
imagePullPolicy: '{{.Values.webhook.image.pullPolicy}}'
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: {{ include "polaris.fullname" . }}-webhook
volumes:
- name: config
configMap:
name: {{ include "polaris.fullname" . }}
- name: secret
secret:
secretName: polaris-webhook
- name: cr-logs
emptyDir: {}
{{- end -}}
@@ -1,90 +0,0 @@
{{- if and .Values.webhook.enable .Values.rbac.create -}}
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "polaris.fullname" . }}-webhook
namespace: {{ .Release.Namespace }}
labels:
{{- include "polaris.labels" . | nindent 4 }}
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
metadata:
name: {{ include "polaris.fullname" . }}-webhook
labels:
{{- include "polaris.labels" . | nindent 4 }}
rules:
- apiGroups:
- 'apps'
- 'extensions'
resources:
- 'deployments'
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: {{ include "polaris.fullname" . }}-webhook
labels:
{{- include "polaris.labels" . | nindent 4 }}
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: {{ include "polaris.fullname" . }}-webhook
subjects:
- kind: ServiceAccount
name: {{ include "polaris.fullname" . }}-webhook
namespace: {{ .Release.Namespace }}
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: Role
metadata:
name: {{ include "polaris.fullname" . }}-webhook
namespace: {{ .Release.Namespace }}
labels:
{{- include "polaris.labels" . | nindent 4 }}
rules:
# required for current controller-runtime bootstrap method
- apiGroups:
- ''
resources:
- 'secrets'
- 'services'
verbs:
- '*'
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: RoleBinding
metadata:
name: {{ include "polaris.fullname" . }}-webhook
namespace: {{ .Release.Namespace }}
labels:
{{- include "polaris.labels" . | nindent 4 }}
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: {{ include "polaris.fullname" . }}-webhook
subjects:
- kind: ServiceAccount
name: {{ include "polaris.fullname" . }}-webhook
namespace: {{ .Release.Namespace }}
{{- end -}}
@@ -1,12 +0,0 @@
{{- if .Values.webhook.enable -}}
apiVersion: v1
kind: Secret
metadata:
name: polaris-webhook
namespace: {{ .Release.Namespace }}
labels:
{{- include "polaris.labels" . | nindent 4 }}
type: Opaque
stringData:
cert.pem: ''
{{- end -}}
@@ -1,19 +0,0 @@
{{- if .Values.webhook.enable -}}
apiVersion: v1
kind: Service
metadata:
name: polaris-webhook
namespace: {{ .Release.Namespace }}
labels:
{{- include "polaris.labels" . | nindent 4 }}
spec:
ports:
- name: webhook
port: 443
protocol: TCP
targetPort: 9876
selector:
{{- include "polaris.selectors" . | nindent 4 }}
component: webhook
type: ClusterIP
{{- end -}}
-67
View File
@@ -1,67 +0,0 @@
---
config: |
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
dashboard:
enable: true
replicas: 1
service:
type: ClusterIP
image:
repository: quay.io/reactiveops/polaris
tag: 0.1.5
pullPolicy: Always
webhook:
enable: false
replicas: 1
image:
repository: quay.io/reactiveops/polaris
tag: 0.1.5
pullPolicy: Always
rbac:
create: true
templateOnly: false
-280
View File
@@ -1,280 +0,0 @@
---
# 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
---
# 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:
- apiGroups:
- 'apps'
- 'extensions'
resources:
- 'deployments'
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: '6ec4a4dc87403cae67c01438398a5f1d4ef836ffeaf26a33b685c066b940495a'
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.1.5'
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
volumes:
- name: config
configMap:
name: polaris
- name: secret
secret:
secretName: polaris-webhook
- name: cr-logs
emptyDir: {}
---
# Source: polaris/templates/dashboard.deployment.yaml
---
# Source: polaris/templates/dashboard.rbac.yaml
---
# Source: polaris/templates/dashboard.service.yaml
+34 -23
View File
@@ -29,7 +29,6 @@ import (
"github.com/reactiveops/polaris/pkg/validator"
fwebhook "github.com/reactiveops/polaris/pkg/webhook"
"github.com/sirupsen/logrus"
"gopkg.in/yaml.v2"
appsv1 "k8s.io/api/apps/v1"
extensionsv1beta1 "k8s.io/api/extensions/v1beta1"
apitypes "k8s.io/apimachinery/pkg/types"
@@ -38,11 +37,12 @@ import (
"sigs.k8s.io/controller-runtime/pkg/manager"
"sigs.k8s.io/controller-runtime/pkg/runtime/signals"
"sigs.k8s.io/controller-runtime/pkg/webhook"
"sigs.k8s.io/yaml"
)
const (
// Version represents the current release version of Polaris
Version = "0.1.5"
Version = "0.2.1"
)
func main() {
@@ -55,6 +55,8 @@ func main() {
webhookPort := flag.Int("webhook-port", 9876, "Port for the webhook webserver")
auditOutputURL := flag.String("output-url", "", "Destination URL to send audit results")
auditOutputFile := flag.String("output-file", "", "Destination file for audit results")
auditOutputFormat := flag.String("output-format", "json", "Output format for results - json, yaml, or score")
displayName := flag.String("display-name", "", "An optional identifier for the audit")
configPath := flag.String("config", "", "Location of Polaris configuration file")
logLevel := flag.String("log-level", logrus.InfoLevel.String(), "Logrus log level")
version := flag.Bool("version", false, "Prints the version of Polaris")
@@ -76,6 +78,9 @@ func main() {
}
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)
@@ -90,7 +95,7 @@ func main() {
} else if *dashboard {
startDashboardServer(c, *auditPath, *dashboardPort, *dashboardBasePath)
} else if *audit {
runAudit(c, *auditPath, *auditOutputFile, *auditOutputURL)
runAudit(c, *auditPath, *auditOutputFile, *auditOutputURL, *auditOutputFormat)
}
}
@@ -175,7 +180,7 @@ func startWebhookServer(c conf.Configuration, disableWebhookConfigInstaller bool
}
}
func runAudit(c conf.Configuration, auditPath string, outputFile string, outputURL string) {
func runAudit(c conf.Configuration, auditPath string, outputFile string, outputURL string, outputFormat string) {
k, err := kube.CreateResourceProvider(auditPath)
if err != nil {
logrus.Errorf("Error fetching Kubernetes resources %v", err)
@@ -187,33 +192,39 @@ func runAudit(c conf.Configuration, auditPath string, outputFile string, outputU
panic(err)
}
if outputURL == "" && outputFile == "" {
yamlBytes, err := yaml.Marshal(auditData)
if err != nil {
logrus.Errorf("Error marshalling YAML: %v", err)
os.Exit(1)
var outputBytes []byte
if outputFormat == "score" {
outputBytes = []byte(fmt.Sprint(auditData.ClusterSummary.Score))
} else if outputFormat == "yaml" {
jsonBytes, err := json.Marshal(auditData)
if err == nil {
outputBytes, err = yaml.JSONToYAML(jsonBytes)
}
os.Stdout.Write(yamlBytes)
} else {
jsonData, err := json.MarshalIndent(auditData, "", " ")
if err != nil {
logrus.Errorf("Error marshalling JSON: %v", err)
os.Exit(1)
}
outputBytes, err = json.MarshalIndent(auditData, "", " ")
}
if err != nil {
logrus.Errorf("Error marshalling audit: %v", err)
os.Exit(1)
}
if outputURL == "" && outputFile == "" {
os.Stdout.Write(outputBytes)
} else {
if outputURL != "" {
req, err := http.NewRequest("POST", outputURL, bytes.NewBuffer(jsonData))
req, err := http.NewRequest("POST", outputURL, bytes.NewBuffer(outputBytes))
if err != nil {
logrus.Errorf("Error building request for output: %v", err)
os.Exit(1)
}
req.Header.Set("Content-Type", "application/json")
if outputFormat == "json" {
req.Header.Set("Content-Type", "application/json")
} else if outputFormat == "yaml" {
req.Header.Set("Content-Type", "application/x-yaml")
} else {
req.Header.Set("Content-Type", "text/plain")
}
client := &http.Client{}
resp, err := client.Do(req)
@@ -235,7 +246,7 @@ func runAudit(c conf.Configuration, auditPath string, outputFile string, outputU
}
if outputFile != "" {
err := ioutil.WriteFile(outputFile, []byte(jsonData), 0644)
err := ioutil.WriteFile(outputFile, []byte(outputBytes), 0644)
if err != nil {
logrus.Errorf("Error writing output to file: %v", err)
os.Exit(1)
+1
View File
@@ -28,6 +28,7 @@ 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"`
+14 -7
View File
@@ -9,11 +9,14 @@
font-weight: 300;
font-size: 28px;
padding: 20px 20px;
white-space: nowrap;
overflow: hidden;
margin-left: -2px; /* Fix for kerning issue */
}
.card.cluster h3 {
padding: 0px;
padding-bottom: 10px;
padding: 4px 0px;
margin-bottom: 20px;
}
.card.namespace h3 strong {
@@ -21,9 +24,7 @@
}
.cluster-overview {
position: relative;
top: -60px;
margin-bottom: -60px;
}
.cluster-overview .graph {
@@ -38,11 +39,12 @@
display: inline-block;
width: 32%;
vertical-align: top;
padding-top: 65px;
}
.cluster-overview .cluster-score{
padding-top: 15px;
}
.cluster-overview .result-messages {
padding-top: 90px;
padding-top: 70px;
}
.cluster-overview .graph canvas {
@@ -263,6 +265,11 @@ ul.message-list li i.message-icon {
color: #a11f4c;
}
.controller-type {
display: inline-block;
min-width: 115px;
}
a.more-info {
color: #bbb;
font-size: 12px;
+9 -9
View File
@@ -87,15 +87,15 @@ type templateData struct {
// GetBaseTemplate puts together the dashboard template. Individual pieces can be overridden before rendering.
func GetBaseTemplate(name string) (*template.Template, error) {
tmpl := template.New(name).Funcs(template.FuncMap{
"getWarningWidth": getWarningWidth,
"getSuccessWidth": getSuccessWidth,
"getWeatherIcon": getWeatherIcon,
"getWeatherText": getWeatherText,
"getGrade": getGrade,
"getScore": getScore,
"getIcon": getIcon,
"getCategoryLink": getCategoryLink,
"getCategoryInfo": getCategoryInfo,
"getWarningWidth": getWarningWidth,
"getSuccessWidth": getSuccessWidth,
"getWeatherIcon": getWeatherIcon,
"getWeatherText": getWeatherText,
"getGrade": getGrade,
"getIcon": getIcon,
"getCategoryLink": getCategoryLink,
"getCategoryInfo": getCategoryInfo,
"getAllControllerResults": getAllControllerResults,
})
templateFileNames := []string{
+10 -8
View File
@@ -20,6 +20,13 @@ import (
"strings"
)
func getAllControllerResults(nr validator.NamespaceResult) []validator.ControllerResult {
results := []validator.ControllerResult{}
results = append(results, nr.DeploymentResults...)
results = append(results, nr.StatefulSetResults...)
return results
}
func getWarningWidth(counts validator.CountSummary, fullWidth int) uint {
return uint(float64(counts.Successes+counts.Warnings) / float64(counts.Successes+counts.Warnings+counts.Errors) * float64(fullWidth))
}
@@ -29,7 +36,7 @@ func getSuccessWidth(counts validator.CountSummary, fullWidth int) uint {
}
func getGrade(counts validator.CountSummary) string {
score := getScore(counts)
score := counts.GetScore()
if score >= 97 {
return "A+"
} else if score >= 93 {
@@ -59,13 +66,8 @@ func getGrade(counts validator.CountSummary) string {
}
}
func getScore(counts validator.CountSummary) uint {
total := (counts.Successes * 2) + counts.Warnings + (counts.Errors * 2)
return uint((float64(counts.Successes*2) / float64(total)) * 100)
}
func getWeatherIcon(counts validator.CountSummary) string {
score := getScore(counts)
score := counts.GetScore()
if score >= 90 {
return "fa-sun"
} else if score >= 80 {
@@ -80,7 +82,7 @@ func getWeatherIcon(counts validator.CountSummary) string {
}
func getWeatherText(counts validator.CountSummary) string {
score := getScore(counts)
score := counts.GetScore()
if score >= 90 {
return "Smooth sailing"
} else if score >= 80 {
+12 -5
View File
@@ -1,13 +1,18 @@
{{define "dashboard"}}
<div class="card cluster">
<h3>Cluster Overview</h3>
<h3>
<span>{{ .AuditData.SourceType }} Overview</span>
{{- if .AuditData.DisplayName -}}
<small>: {{ .AuditData.DisplayName }}</small>
{{ end }}
</h3>
<div class="cluster-overview">
<div class="cluster-score">
<div class="score-details">
<div class="weather"><i class="fas {{ getWeatherIcon .AuditData.ClusterSummary.Results.Totals }}"></i></div>
<div class="sailing">{{ getWeatherText .AuditData.ClusterSummary.Results.Totals }}</div>
<div class="scores"><span>Grade: </span><strong>{{ getGrade .AuditData.ClusterSummary.Results.Totals }}</strong></div>
<div class="scores"><span>Score: </span><strong>{{ getScore .AuditData.ClusterSummary.Results.Totals }}%</strong></div>
<div class="scores"><span>Score: </span><strong>{{ .AuditData.ClusterSummary.Results.Totals.GetScore }}%</strong></div>
</div>
</div>
<div class="graph">
@@ -60,7 +65,7 @@
</div>
</div>
</div>
<div class="name"><span class="caret-expander"></span>{{ $category }}<span class="category-score">Score: <strong>{{ getScore $summary }}%</strong></span></div>
<div class="name"><span class="caret-expander"></span>{{ $category }}<span class="category-score">Score: <strong>{{ $summary.GetScore }}%</strong></span></div>
<div class="result-messages expandable-content">
<p class="category-info">{{ getCategoryInfo $category }} Refer to the <a href="details/{{ getCategoryLink $category }}">Polaris documentation about {{ $category }}</a> for more information.</p>
</div>
@@ -73,7 +78,7 @@
<div class="card namespace">
<h3>Namespace: <strong>{{ $namespace }}</strong></h3>
<div class="expandable-table">
{{ range .DeploymentResults }}
{{ range getAllControllerResults $nsResult }}
<div class="resource-info">
<div class="status-bar">
<div class="status">
@@ -85,7 +90,9 @@
</div>
</div>
<div class="name"><span class="caret-expander"></span>Deployment: <strong>{{ .Name }}</strong></div>
<div class="name"><span class="caret-expander"></span>
<span class="controller-type">{{ .Type }}:</span>
<strong>{{ .Name }}</strong></div>
<div class="result-messages expandable-content">
<h4>Pod Spec:</h4>
<ul class="message-list">
+66 -44
View File
@@ -7,6 +7,7 @@ import (
"path/filepath"
"regexp"
"strings"
"time"
"github.com/sirupsen/logrus"
appsv1 "k8s.io/api/apps/v1"
@@ -21,8 +22,12 @@ 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
}
@@ -43,49 +48,17 @@ func CreateResourceProvider(directory string) (*ResourceProvider, error) {
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{},
}
addYaml := func(contents string) error {
contentBytes := []byte(contents)
decoder := k8sYaml.NewYAMLOrJSONDecoder(bytes.NewReader(contentBytes), 1000)
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
}
decoder = k8sYaml.NewYAMLOrJSONDecoder(bytes.NewReader(contentBytes), 1000)
if resource.Kind == "Deployment" {
dep := appsv1.Deployment{}
err = decoder.Decode(&dep)
if err != nil {
logrus.Errorf("Error parsing deployment %v", err)
return err
}
resources.Deployments = append(resources.Deployments, dep)
} else if resource.Kind == "Namespace" {
ns := corev1.Namespace{}
err = decoder.Decode(&ns)
if err != nil {
logrus.Errorf("Error parsing namespace %v", err)
return err
}
resources.Namespaces = append(resources.Namespaces, ns)
} else if resource.Kind == "Pod" {
pod := corev1.Pod{}
err = decoder.Decode(&pod)
if err != nil {
logrus.Errorf("Error parsing pod %v", err)
return err
}
resources.Pods = append(resources.Pods, pod)
}
return nil
return addResourceFromString(contents, &resources)
}
visitFile := func(path string, f os.FileInfo, err error) error {
@@ -120,49 +93,98 @@ func CreateResourceProviderFromPath(directory string) (*ResourceProvider, error)
// CreateResourceProviderFromCluster creates a new ResourceProvider using live data from a cluster
func CreateResourceProviderFromCluster() (*ResourceProvider, error) {
kubeConf := config.GetConfigOrDie()
kubeConf, configError := config.GetConfig()
if configError != nil {
logrus.Errorf("Error fetching KubeConfig %v", configError)
return nil, configError
}
api, err := kubernetes.NewForConfig(kubeConf)
if err != nil {
logrus.Errorf("Error creating Kubernetes client %v", err)
return nil, err
}
return CreateResourceProviderFromAPI(api)
return CreateResourceProviderFromAPI(api, kubeConf.Host)
}
// CreateResourceProviderFromAPI creates a new ResourceProvider from an existing k8s interface
func CreateResourceProviderFromAPI(kube kubernetes.Interface) (*ResourceProvider, error) {
func CreateResourceProviderFromAPI(kube kubernetes.Interface, clusterName string) (*ResourceProvider, error) {
listOpts := metav1.ListOptions{}
serverVersion, err := kube.Discovery().ServerVersion()
if err != nil {
logrus.Errorf("Error fetching Kubernetes API version %v", err)
logrus.Errorf("Error fetching Cluster API version %v", err)
return nil, err
}
deploys, err := kube.AppsV1().Deployments("").List(listOpts)
if err != nil {
logrus.Errorf("Error fetching Kubernetes Deployments %v", err)
logrus.Errorf("Error fetching Deployments %v", err)
return nil, err
}
statefulSets, err := kube.AppsV1().StatefulSets("").List(listOpts)
if err != nil {
logrus.Errorf("Error fetching StatefulSets%v", err)
return nil, err
}
nodes, err := kube.CoreV1().Nodes().List(listOpts)
if err != nil {
logrus.Errorf("Error fetching Kubernetes Nodes %v", err)
logrus.Errorf("Error fetching Nodes %v", err)
return nil, err
}
namespaces, err := kube.CoreV1().Namespaces().List(listOpts)
if err != nil {
logrus.Errorf("Error fetching Kubernetes Namespaces %v", err)
logrus.Errorf("Error fetching Namespaces %v", err)
return nil, err
}
pods, err := kube.CoreV1().Pods("").List(listOpts)
if err != nil {
logrus.Errorf("Error fetching Kubernetes Pods %v", err)
logrus.Errorf("Error fetching Pods %v", err)
return nil, err
}
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,
}
return &api, nil
}
func addResourceFromString(contents string, resources *ResourceProvider) error {
contentBytes := []byte(contents)
decoder := k8sYaml.NewYAMLOrJSONDecoder(bytes.NewReader(contentBytes), 1000)
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
}
decoder = k8sYaml.NewYAMLOrJSONDecoder(bytes.NewReader(contentBytes), 1000)
if resource.Kind == "Deployment" {
dep := appsv1.Deployment{}
err = decoder.Decode(&dep)
resources.Deployments = append(resources.Deployments, dep)
} else if resource.Kind == "StatefulSet" {
dep := appsv1.StatefulSet{}
err = decoder.Decode(&dep)
resources.StatefulSets = append(resources.StatefulSets, dep)
} else if resource.Kind == "Namespace" {
ns := corev1.Namespace{}
err = decoder.Decode(&ns)
resources.Namespaces = append(resources.Namespaces, ns)
} else if resource.Kind == "Pod" {
pod := corev1.Pod{}
err = decoder.Decode(&pod)
resources.Pods = append(resources.Pods, pod)
}
if err != nil {
logrus.Errorf("Error parsing %s: %v", resource.Kind, err)
return err
}
return nil
}
+17 -2
View File
@@ -4,6 +4,7 @@ import (
"github.com/reactiveops/polaris/test"
"github.com/stretchr/testify/assert"
"testing"
"time"
)
func TestGetResourcesFromPath(t *testing.T) {
@@ -11,13 +12,19 @@ func TestGetResourcesFromPath(t *testing.T) {
assert.Equal(t, nil, err, "Error should be nil")
assert.Equal(t, "Path", resources.SourceType, "Should have type Path")
assert.Equal(t, "./test_files/test_1", resources.SourceName, "Should have filename as name")
assert.Equal(t, "unknown", resources.ServerVersion, "Server version should be unknown")
assert.IsType(t, time.Now(), resources.CreationTime, "Creation time should be set")
assert.Equal(t, 0, len(resources.Nodes), "Should not have any nodes")
assert.Equal(t, 1, len(resources.Deployments), "Should have a deployment")
assert.Equal(t, "ubuntu", resources.Deployments[0].Spec.Template.Spec.Containers[0].Name)
assert.Equal(t, 1, len(resources.StatefulSets), "Should have a stateful set")
assert.Equal(t, "nginx", resources.StatefulSets[0].Spec.Template.Spec.Containers[0].Name)
assert.Equal(t, 1, len(resources.Namespaces), "Should have a namespace")
assert.Equal(t, "two", resources.Namespaces[0].ObjectMeta.Name)
@@ -31,7 +38,10 @@ func TestGetMultipleResourceFromSingleFile(t *testing.T) {
assert.Equal(t, nil, err, "Error should be nil")
assert.Equal(t, "Path", resources.SourceType, "Should have type Path")
assert.Equal(t, "./test_files/test_2/multi.yaml", resources.SourceName, "Should have filename as name")
assert.Equal(t, "unknown", resources.ServerVersion, "Server version should be unknown")
assert.IsType(t, time.Now(), resources.CreationTime, "Creation time should be set")
assert.Equal(t, 0, len(resources.Nodes), "Should not have any nodes")
@@ -45,12 +55,17 @@ func TestGetMultipleResourceFromSingleFile(t *testing.T) {
func TestGetResourceFromAPI(t *testing.T) {
k8s := test.SetupTestAPI()
k8s = test.SetupAddDeploys(k8s, "test")
resources, err := CreateResourceProviderFromAPI(k8s)
k8s = test.SetupAddControllers(k8s, "test")
resources, err := CreateResourceProviderFromAPI(k8s, "test")
assert.Equal(t, nil, err, "Error should be nil")
assert.Equal(t, "Cluster", resources.SourceType, "Should have type Path")
assert.Equal(t, "test", resources.SourceName, "Should have source name")
assert.IsType(t, time.Now(), resources.CreationTime, "Creation time should be set")
assert.Equal(t, 0, len(resources.Nodes), "Should not have any nodes")
assert.Equal(t, 1, len(resources.Deployments), "Should have a deployment")
assert.Equal(t, 1, len(resources.StatefulSets), "Should have a stateful set")
assert.Equal(t, 0, len(resources.Pods), "Should have a pod")
assert.Equal(t, "", resources.Deployments[0].ObjectMeta.Name)
@@ -0,0 +1,35 @@
apiVersion: apps/v1 # for k8s versions before 1.9.0 use apps/v1beta2 and before 1.8.0 use extensions/v1beta1
kind: StatefulSet
metadata:
name: web
labels:
app: nginx
spec:
serviceName: "nginx"
selector:
matchLabels:
app: nginx
replicas: 14
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: k8s.gcr.io/nginx-slim:0.8
ports:
- containerPort: 80
name: web
volumeMounts:
- name: www
mountPath: /usr/share/nginx/html
volumeClaimTemplates:
- metadata:
name: www
spec:
accessModes: [ "ReadWriteOnce" ]
resources:
requests:
storage: 1Gi
storageClassName: thin-disk
+59 -13
View File
@@ -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
}
+184
View File
@@ -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 {
+96
View File
@@ -0,0 +1,96 @@
// Copyright 2019 ReactiveOps
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package validator
import (
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"
)
// 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)
return ControllerResult{
Type: controller.Type,
Name: controller.Name,
PodResult: podResult,
}
}
// ValidateControllers validates that each deployment conforms to the Polaris config,
// returns 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))
}
for _, deploy := range kubeResources.StatefulSets {
controllers = append(controllers, ControllerFromStatefulSet(deploy))
}
for _, controller := range controllers {
controllerResult := ValidateController(config, controller)
nsResult := nsResults.getNamespaceResult(controller.Namespace)
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)
}
}
}
// 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,
}
}
-67
View File
@@ -1,67 +0,0 @@
// Copyright 2019 ReactiveOps
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package validator
import (
conf "github.com/reactiveops/polaris/pkg/config"
"github.com/reactiveops/polaris/pkg/kube"
appsv1 "k8s.io/api/apps/v1"
)
// ValidateDeployment validates a single deployment, returns a PodResult.
func ValidateDeployment(conf conf.Configuration, deploy *appsv1.Deployment) ControllerResult {
pod := deploy.Spec.Template.Spec
podResult := ValidatePod(conf, &pod)
return ControllerResult{
Name: deploy.Name,
Type: "Deployment",
PodResult: podResult,
}
}
// ValidateDeployments validates that each deployment conforms to the Polaris config,
// returns a list of ResourceResults organized by namespace.
func ValidateDeployments(config conf.Configuration, kubeResources *kube.ResourceProvider) (NamespacedResults, error) {
nsResults := NamespacedResults{}
for _, deploy := range kubeResources.Deployments {
deploymentResult := ValidateDeployment(config, &deploy)
nsResults = addResult(deploymentResult, nsResults, deploy.Namespace)
}
return nsResults, nil
}
func addResult(deploymentResult ControllerResult, nsResults NamespacedResults, nsName string) NamespacedResults {
nsResult := &NamespaceResult{}
// If there is already data stored for this namespace name,
// then append to the ResourceResults to the existing data.
switch nsResults[nsName] {
case nil:
nsResult = &NamespaceResult{
Summary: &ResultSummary{},
DeploymentResults: []ControllerResult{},
}
nsResults[nsName] = nsResult
default:
nsResult = nsResults[nsName]
}
nsResult.DeploymentResults = append(nsResult.DeploymentResults, deploymentResult)
nsResult.Summary.appendResults(*deploymentResult.PodResult.Summary)
return nsResults
}
+37 -23
View File
@@ -1,44 +1,44 @@
package validator
import (
"time"
conf "github.com/reactiveops/polaris/pkg/config"
"github.com/reactiveops/polaris/pkg/kube"
)
const (
// PolarisOutputVersion is the version of the current output structure
PolarisOutputVersion = "0.0"
PolarisOutputVersion = "0.2"
)
// ClusterSummary contains Polaris results as well as some high-level stats
type ClusterSummary struct {
Results ResultSummary
Version string
Nodes int
Pods int
Namespaces int
Deployments int
Results ResultSummary
Version string
Nodes int
Pods int
Namespaces int
Deployments int
StatefulSets int
Score uint
}
// AuditData contains all the data from a full Polaris audit
type AuditData struct {
PolarisOutputVersion string
AuditTime string
SourceType string
SourceName string
DisplayName string
ClusterSummary ClusterSummary
NamespacedResults NamespacedResults
}
// RunAudit runs a full Polaris audit and returns an AuditData object
func RunAudit(config conf.Configuration, kubeResources *kube.ResourceProvider) (AuditData, error) {
// TODO: Validate StatefulSets, DaemonSets, Cron jobs
// in addition to deployments
// TODO: Once we are validating more than deployments,
// we will need to merge the namespaceResults that get returned
// from each validation.
nsResults, err := ValidateDeployments(config, kubeResources)
if err != nil {
return AuditData{}, err
}
nsResults := NamespacedResults{}
ValidateControllers(config, kubeResources, &nsResults)
clusterResults := ResultSummary{}
@@ -47,17 +47,31 @@ func RunAudit(config conf.Configuration, kubeResources *kube.ResourceProvider) (
for _, dr := range nsRes.DeploymentResults {
clusterResults.appendResults(*dr.PodResult.Summary)
}
for _, dr := range nsRes.StatefulSetResults {
clusterResults.appendResults(*dr.PodResult.Summary)
}
}
displayName := config.DisplayName
if displayName == "" {
displayName = kubeResources.SourceName
}
auditData := AuditData{
PolarisOutputVersion: PolarisOutputVersion,
AuditTime: kubeResources.CreationTime.Format(time.RFC3339),
SourceType: kubeResources.SourceType,
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),
Results: clusterResults,
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(),
},
NamespacedResults: nsResults,
}
+16 -8
View File
@@ -11,8 +11,8 @@ import (
func TestGetTemplateData(t *testing.T) {
k8s := test.SetupTestAPI()
k8s = test.SetupAddDeploys(k8s, "test")
resources, err := kube.CreateResourceProviderFromAPI(k8s)
k8s = test.SetupAddControllers(k8s, "test")
resources, err := kube.CreateResourceProviderFromAPI(k8s, "test")
assert.Equal(t, err, nil, "error should be nil")
c := conf.Configuration{
@@ -24,19 +24,19 @@ func TestGetTemplateData(t *testing.T) {
sum := ResultSummary{
Totals: CountSummary{
Successes: uint(4),
Warnings: uint(1),
Errors: uint(1),
Successes: uint(8),
Warnings: uint(2),
Errors: uint(2),
},
ByCategory: CategorySummary{},
}
sum.ByCategory["Health Checks"] = &CountSummary{
Successes: uint(0),
Warnings: uint(1),
Errors: uint(1),
Warnings: uint(2),
Errors: uint(2),
}
sum.ByCategory["Resources"] = &CountSummary{
Successes: uint(4),
Successes: uint(8),
Warnings: uint(0),
Errors: uint(0),
}
@@ -45,8 +45,16 @@ func TestGetTemplateData(t *testing.T) {
assert.Equal(t, err, nil, "error should be nil")
assert.EqualValues(t, sum, actualAudit.ClusterSummary.Results)
assert.Equal(t, actualAudit.SourceType, "Cluster", "should be from a cluster")
assert.Equal(t, actualAudit.SourceName, "test", "should be from a cluster")
assert.Equal(t, 1, len(actualAudit.NamespacedResults["test"].DeploymentResults), "should be equal")
assert.Equal(t, 1, len(actualAudit.NamespacedResults["test"].DeploymentResults), "should be equal")
assert.Equal(t, 1, len(actualAudit.NamespacedResults["test"].DeploymentResults[0].PodResult.ContainerResults), "should be equal")
assert.Equal(t, 6, len(actualAudit.NamespacedResults["test"].DeploymentResults[0].PodResult.ContainerResults[0].Messages), "should be equal")
assert.Equal(t, 1, len(actualAudit.NamespacedResults["test"].StatefulSetResults), "should be equal")
assert.Equal(t, 1, len(actualAudit.NamespacedResults["test"].StatefulSetResults), "should be equal")
assert.Equal(t, 1, len(actualAudit.NamespacedResults["test"].StatefulSetResults[0].PodResult.ContainerResults), "should be equal")
assert.Equal(t, 6, len(actualAudit.NamespacedResults["test"].StatefulSetResults[0].PodResult.ContainerResults[0].Messages), "should be equal")
}
+2 -1
View File
@@ -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)
}
}
+1 -1
View File
@@ -35,7 +35,7 @@ func TestValidatePod(t *testing.T) {
}
k8s := test.SetupTestAPI()
k8s = test.SetupAddDeploys(k8s, "test")
k8s = test.SetupAddControllers(k8s, "test")
pod := test.MockPod()
expectedSum := ResultSummary{
+31 -5
View File
@@ -14,6 +14,8 @@
package validator
import corev1 "k8s.io/api/core/v1"
// MessageType represents the type of Message
type MessageType string
@@ -28,14 +30,31 @@ const (
MessageTypeError MessageType = "error"
)
// NamespaceResult groups container results by parent resource.
type NamespaceResult struct {
Name string
Summary *ResultSummary
DeploymentResults []ControllerResult
StatefulSetResults []ControllerResult
}
// NamespacedResults is a mapping of namespace name to the validation results.
type NamespacedResults map[string]*NamespaceResult
// NamespaceResult groups container results by parent resource.
type NamespaceResult struct {
Name string
Summary *ResultSummary
DeploymentResults []ControllerResult
func (nsResults NamespacedResults) getNamespaceResult(nsName string) *NamespaceResult {
nsResult := &NamespaceResult{}
switch nsResults[nsName] {
case nil:
nsResult = &NamespaceResult{
Summary: &ResultSummary{},
DeploymentResults: []ControllerResult{},
StatefulSetResults: []ControllerResult{},
}
nsResults[nsName] = nsResult
default:
nsResult = nsResults[nsName]
}
return nsResult
}
// CountSummary provides a high level overview of success, warnings, and errors.
@@ -45,6 +64,12 @@ type CountSummary struct {
Errors uint
}
// GetScore returns an overall score in [0, 100] for the CountSummary
func (cs *CountSummary) GetScore() uint {
total := (cs.Successes * 2) + cs.Warnings + (cs.Errors * 2)
return uint((float64(cs.Successes*2) / float64(total)) * 100)
}
func (cs *CountSummary) appendCounts(toAppend CountSummary) {
cs.Errors += toAppend.Errors
cs.Warnings += toAppend.Warnings
@@ -93,6 +118,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).
+17 -10
View File
@@ -87,19 +87,24 @@ func (v *Validator) Handle(ctx context.Context, req types.Request) types.Respons
var err error
var podResult validator.PodResult
allowed := true
reason := ""
switch req.AdmissionRequest.Kind.Kind {
case "Deployment":
deploy := appsv1.Deployment{}
err = v.decoder.Decode(req, &deploy)
deployResult := validator.ValidateDeployment(v.Config, &deploy)
podResult = deployResult.PodResult
case "Pod":
if req.AdmissionRequest.Kind.Kind == "Pod" {
pod := corev1.Pod{}
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":
deploy := appsv1.Deployment{}
err = v.decoder.Decode(req, &deploy)
controller = validator.ControllerFromDeployment(deploy)
case "StatefulSet":
statefulSet := appsv1.StatefulSet{}
err = v.decoder.Decode(req, &statefulSet)
controller = validator.ControllerFromStatefulSet(statefulSet)
}
controllerResult := validator.ValidateController(v.Config, controller)
podResult = controllerResult.PodResult
}
if err != nil {
@@ -107,6 +112,8 @@ func (v *Validator) Handle(ctx context.Context, req types.Request) types.Respons
return admission.ErrorResponse(http.StatusBadRequest, err)
}
allowed := true
reason := ""
if podResult.Summary.Totals.Errors > 0 {
allowed = false
reason = getFailureReason(podResult)
+17 -2
View File
@@ -39,17 +39,32 @@ func mockDeploy() appsv1.Deployment {
return d
}
func mockStatefulSet() appsv1.StatefulSet {
p := MockPod()
s := appsv1.StatefulSet{
Spec: appsv1.StatefulSetSpec{
Template: p,
},
}
return s
}
// SetupTestAPI creates a test kube API struct.
func SetupTestAPI() kubernetes.Interface {
return fake.NewSimpleClientset()
}
// SetupAddDeploys creates a mock deployment and adds it to the test clientset.
func SetupAddDeploys(k kubernetes.Interface, namespace string) 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 {
fmt.Println(err)
}
s1 := mockStatefulSet()
_, err = k.AppsV1().StatefulSets(namespace).Create(&s1)
if err != nil {
fmt.Println(err)
}
return k
}