├── .github └── workflows │ ├── main.yml │ ├── on-push-lint-charts.yml │ └── on-push-master-publish-chart.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── Dockerfile.local ├── LICENSE ├── Makefile ├── README.md ├── charts └── k-rail │ ├── .helmignore │ ├── Chart.yaml │ ├── templates │ ├── _helpers.tpl │ └── deployment.yaml │ └── values.yaml ├── cmd └── main.go ├── contrib ├── analyze--init-container-breaking-changes.sh └── analyze-pods.sh ├── examples ├── exemptions.example.yaml ├── non-compliant-clusterrolebinding.yaml ├── non-compliant-daemonset.yaml ├── non-compliant-deployment.yaml ├── non-compliant-ingress.yaml ├── non-compliant-persistent-volume.yaml ├── non-compliant-poddisruptionbudget.yaml ├── non-compliant-rolebinding.yaml └── non-compliant-service-loadbalancer.yaml ├── go.mod ├── go.sum ├── images └── k-rail.png ├── plugins ├── examples │ ├── README.md │ ├── config.yml │ └── example.go ├── grpc.go ├── plugins.go └── proto │ ├── plugin.pb.go │ └── plugin.proto ├── policies ├── clusterrolebinding │ └── no_anonymous_cluster_role_binding.go ├── config.go ├── config_test.go ├── customresourcedefinition │ └── crd_protect.go ├── exemption.go ├── exemption_test.go ├── ingress │ ├── disallow_nginx_snippet.go │ ├── disallow_nginx_snippet_test.go │ ├── require_ingress_exemption.go │ ├── require_ingress_exemption_test.go │ └── unique_ingress_host.go ├── patch.go ├── persistentvolume │ └── no_persistent_volume_host.go ├── pod │ ├── bind_mounts.go │ ├── bind_mounts_test.go │ ├── deny_unconfined_apparmor.go │ ├── deny_unconfined_apparmor_test.go │ ├── docker_sock.go │ ├── docker_sock_test.go │ ├── empty_dir_size_limit.go │ ├── empty_dir_size_limit_test.go │ ├── immutable_image_digest.go │ ├── immutable_image_digest_test.go │ ├── mutate_default_seccomp_policy.go │ ├── mutate_default_seccomp_policy_test.go │ ├── mutate_image_pull_policy.go │ ├── mutate_image_pull_policy_test.go │ ├── mutate_safe_to_evict.go │ ├── no_exec.go │ ├── no_host_network.go │ ├── no_host_pid.go │ ├── no_new_capabilities.go │ ├── no_privileged_container.go │ ├── no_root.go │ ├── no_root_test.go │ ├── no_shareprocessnamespace.go │ ├── no_shareprocessnamespace_test.go │ ├── no_tiller.go │ ├── no_tiller_test.go │ ├── safe_to_evict.go │ ├── safe_to_evict_test.go │ ├── trusted_repository.go │ └── trusted_repository_test.go ├── poddisruptionbudget │ └── invalid_pod_disruption_budget.go ├── rolebinding │ └── no_anonymous_role_binding.go ├── service │ ├── require_service_loadbalancer_exemption.go │ ├── require_service_loadbalancer_exemption_test.go │ └── service_no_externalip.go ├── violation.go └── virtualservice │ ├── require_virtualservice_gateway_exemption.go │ └── require_virtualservice_gateway_exemption_test.go ├── resource ├── cluster_role_binding.go ├── context.go ├── context_test.go ├── decoder.go ├── decoder_test.go ├── ingress.go ├── ingress_test.go ├── persistent_volume.go ├── pod.go ├── pod_disruption_budget.go ├── pod_exec.go ├── pod_test.go ├── role_binding.go ├── service.go └── virtual_service.go └── server ├── config.go ├── metrics.go ├── policies.go ├── server.go ├── server_test.go ├── webhook.go └── webhook_test.go /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | test: 7 | strategy: 8 | matrix: 9 | go-version: [1.16.x] 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | 14 | - name: Set up Go ${{ matrix.go-version }} 15 | uses: actions/setup-go@v1 16 | with: 17 | go-version: ${{ matrix.go-version }} 18 | 19 | - name: Check out code into the Go module directory 20 | uses: actions/checkout@v1 21 | 22 | - name: Test 23 | run: make test 24 | 25 | benchmark: 26 | strategy: 27 | matrix: 28 | go-version: [1.16.x] 29 | runs-on: ubuntu-latest 30 | 31 | steps: 32 | 33 | - name: Set up Go ${{ matrix.go-version }} 34 | uses: actions/setup-go@v1 35 | with: 36 | go-version: ${{ matrix.go-version }} 37 | 38 | - name: Check out code into the Go module directory 39 | uses: actions/checkout@v1 40 | 41 | - name: Benchmark 42 | run: CGO_ENABLED=0 go test -benchmem -run=^$ github.com/cruise-automation/k-rail/v3/server -bench . 43 | 44 | # lint: 45 | # runs-on: ubuntu-latest 46 | # strategy: 47 | # matrix: 48 | # go-version: [1.16.x] 49 | 50 | # steps: 51 | 52 | # - name: Set up Go ${{ matrix.go-version }} 53 | # uses: actions/setup-go@v1 54 | # with: 55 | # go-version: ${{ matrix.go-version }} 56 | 57 | # - name: Check out code into the Go module directory 58 | # uses: actions/checkout@v1 59 | 60 | # - name: Lint 61 | # uses: matoous/golangci-lint-action@v1.1.0 62 | # # with: 63 | # # config: .golangci.yml 64 | 65 | build: 66 | strategy: 67 | matrix: 68 | go-version: [1.16.x] 69 | runs-on: ubuntu-latest 70 | 71 | steps: 72 | 73 | - name: Set up Go ${{ matrix.go-version }} 74 | uses: actions/setup-go@v1 75 | with: 76 | go-version: ${{ matrix.go-version }} 77 | - name: Install Protoc 78 | uses: arduino/setup-protoc@v1 79 | - name: Install protoc-gen-go 80 | run: | 81 | go get -u github.com/golang/protobuf/protoc-gen-go 82 | go get -u google.golang.org/grpc/cmd/protoc-gen-go-grpc 83 | - name: Check out code into the Go module directory 84 | uses: actions/checkout@v1 85 | 86 | - name: Build 87 | run: make build 88 | -------------------------------------------------------------------------------- /.github/workflows/on-push-lint-charts.yml: -------------------------------------------------------------------------------- 1 | name: Lint and Test Charts 2 | 3 | on: 4 | push: 5 | paths: 6 | - 'charts/**' 7 | - '.github/**' 8 | 9 | jobs: 10 | lint-test: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v2 15 | with: 16 | fetch-depth: 0 17 | 18 | - name: Install Helm 19 | uses: azure/setup-helm@v1 20 | with: 21 | version: v3.4.0 22 | 23 | - uses: actions/setup-python@v2 24 | with: 25 | python-version: 3.7 26 | 27 | - name: Set up chart-testing 28 | uses: helm/chart-testing-action@v2.0.1 29 | 30 | - name: Run chart-testing (list-changed) 31 | id: list-changed 32 | run: | 33 | changed=$(ct list-changed) 34 | if [[ -n "$changed" ]]; then 35 | echo "::set-output name=changed::true" 36 | fi 37 | 38 | - name: Run chart-testing (lint) 39 | run: ct lint 40 | 41 | - name: Create kind cluster 42 | uses: helm/kind-action@v1.0.0 43 | if: steps.list-changed.outputs.changed == 'true' 44 | 45 | 46 | - name: Prepare namespace 47 | run: | 48 | kubectl create namespace k-rail 49 | kubectl label namespace k-rail k-rail/ignore=true 50 | if: steps.list-changed.outputs.changed == 'true' 51 | 52 | - name: Run chart-testing (install) 53 | run: ct install --namespace k-rail --release-label ci 54 | -------------------------------------------------------------------------------- /.github/workflows/on-push-master-publish-chart.yml: -------------------------------------------------------------------------------- 1 | name: Publish helm chart 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | paths: 8 | - 'charts/**' 9 | - '.github/**' 10 | 11 | jobs: 12 | build: 13 | 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v2 19 | 20 | - name: Fetch history 21 | run: git fetch --prune --unshallow 22 | 23 | - name: Configure Git 24 | run: | 25 | git config user.name "$GITHUB_ACTOR" 26 | git config user.email "$GITHUB_ACTOR@users.noreply.github.com" 27 | 28 | - name: Install Helm 29 | uses: azure/setup-helm@v1 30 | with: 31 | version: v3.4.0 32 | 33 | - name: Run chart-releaser 34 | uses: helm/chart-releaser-action@v1.2.0 35 | env: 36 | CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}" 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /k-rail 2 | /plugin 3 | *.pem 4 | vendor 5 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | This code of conduct outlines our expectations for participants within the 4 | Cruise LLC (Cruise) community, as well as steps to reporting unacceptable 5 | behavior. We are committed to providing a welcoming and inspiring community 6 | for all and expect our code of conduct to be honored. Anyone who violates this 7 | code of conduct may be banned from the community. 8 | 9 | ## Our Commitment 10 | 11 | In the interest of fostering an open and welcoming environment, we as 12 | contributors and maintainers commit to making participation in our project and 13 | our community a harassment-free experience for everyone, regardless of age, body 14 | size, disability, ethnicity, sex characteristics, gender identity and expression, 15 | level of experience, education, socio-economic status, nationality, personal 16 | appearance, race, religion, or sexual identity and orientation. 17 | 18 | ## Our Standards 19 | 20 | Examples of behavior that contributes to creating a positive environment 21 | include: 22 | 23 | * Using welcoming and inclusive language 24 | * Being respectful of differing viewpoints and experiences 25 | * Gracefully accepting constructive criticism 26 | * Focusing on what is best for the community 27 | * Showing empathy towards other community members 28 | 29 | Examples of unacceptable behavior by participants include: 30 | 31 | * The use of sexualized language or imagery and unwelcome sexual attention or 32 | advances 33 | * Trolling, insulting/derogatory comments, and personal or political attacks 34 | * Public or private harassment 35 | * Publishing others' private information, such as a physical or electronic 36 | address, without explicit permission 37 | * Other conduct which could reasonably be considered inappropriate in a 38 | professional setting 39 | 40 | ## Our Responsibilities 41 | 42 | Project maintainers are responsible for clarifying the standards of acceptable 43 | behavior and are expected to take appropriate and fair corrective action in 44 | response to any instances of unacceptable behavior. 45 | 46 | Project maintainers have the right and responsibility to remove, edit, or 47 | reject comments, commits, code, wiki edits, issues, and other contributions 48 | that are not aligned to this Code of Conduct, or to ban temporarily or 49 | permanently any contributor for other behaviors that they deem inappropriate, 50 | threatening, offensive, or harmful. 51 | 52 | ## Scope 53 | 54 | This Code of Conduct applies both within project spaces and in public spaces 55 | when an individual is representing the project or its community. Examples of 56 | representing a project or community include using an official project e-mail 57 | address, posting via an official social media account, or acting as an appointed 58 | representative at an online or offline event. Representation of a project may be 59 | further defined and clarified by project maintainers. 60 | 61 | ## Enforcement 62 | 63 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 64 | reported by contacting the project team at opensource@getcruise.com. All 65 | complaints will be reviewed and investigated and will result in a response that 66 | is deemed necessary and appropriate to the circumstances. The project team will 67 | maintain confidentiality to the extent possible with regard to the reporter of 68 | an incident. Further details of specific enforcement policies may be posted 69 | separately. 70 | 71 | Project maintainers who do not follow or enforce the Code of Conduct in good 72 | faith may face temporary or permanent repercussions as determined by other 73 | members of the project's leadership. 74 | 75 | ## Attribution 76 | 77 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 78 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 79 | 80 | [homepage]: https://www.contributor-covenant.org -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | > The k-rail project has been deprecated and will receive no new features or bugfixes except in the case of critical security vulnerabilities. We recommend migrating to an actively developed tool like [OPA Gatekeeper](https://github.com/open-policy-agent/gatekeeper) that provides similar functionality. 4 | 5 | By submitting a Contribution this Project (terms defined below), you agree to the following Contributor License Agreement: 6 | 7 | The following terms are used throughout this agreement: 8 | 9 | * You - the person or legal entity including its affiliates asked to accept this agreement. An affiliate is any entity that controls or is controlled by the legal entity, or is under common control with it. 10 | * Project - is an umbrella term that refers to any and all open source projects from Cruise LLC. 11 | * Contribution - any type of work that is submitted to a Project, including any modifications or additions to existing work. 12 | * Submitted - conveyed to a Project via a pull request, commit, issue, or any form of electronic, written, or verbal communication with Cruise LLC, contributors or maintainers. 13 | 14 | **1. Grant of Copyright License.** 15 | 16 | Subject to the terms and conditions of this agreement, You grant to the Projects’ maintainers, contributors, users and to Cruise LLC a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, sublicense, and distribute Your contributions and such derivative works. Except for this license, You reserve all rights, title, and interest in your contributions. 17 | 18 | **2. Grant of Patent License.** 19 | 20 | Subject to the terms and conditions of this agreement, You grant to the Projects’ maintainers, contributors, users and to Cruise LLC a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer your contributions, where such license applies only to those patent claims licensable by you that are necessarily infringed by your contribution or by combination of your contribution with the project to which this contribution was submitted. 21 | If any entity institutes patent litigation - including cross-claim or counterclaim in a lawsuit - against You alleging that your contribution or any project it was submitted to constitutes or is responsible for direct or contributory patent infringement, then any patent licenses granted to that entity under this agreement shall terminate as of the date such litigation is filed. 22 | 23 | **3. Source of Contribution.** 24 | 25 | Your contribution is either your original creation, based upon previous work that, to the best of your knowledge, is covered under an appropriate open source license and you have the right under that license to submit that work with modifications, whether created in whole or in part by you, or you have clearly identified the source of the contribution and any license or other restriction (like related patents, trademarks, and license agreements) of which you are personally aware. 26 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build stage 2 | ARG GO_VERSION=1.16 3 | FROM golang:${GO_VERSION}-buster AS builder 4 | RUN apt-get update && \ 5 | apt-get -y install protobuf-compiler && \ 6 | apt-get clean && \ 7 | rm -rf /var/lib/apt/lists/* && \ 8 | rm -rf /var/cache/apt/* && \ 9 | protoc --version 10 | RUN go get -u github.com/golang/protobuf/protoc-gen-go google.golang.org/grpc/cmd/protoc-gen-go-grpc 11 | 12 | WORKDIR /build 13 | COPY ./ /build/ 14 | RUN make build 15 | RUN make test 16 | 17 | # Production image build stage 18 | FROM scratch 19 | EXPOSE 8443/tcp 20 | COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt 21 | COPY --from=builder /build/k-rail /k-rail 22 | USER 65534 23 | ENTRYPOINT ["/k-rail", "-config", "/config/config.yml"] 24 | -------------------------------------------------------------------------------- /Dockerfile.local: -------------------------------------------------------------------------------- 1 | FROM alpine as builder 2 | RUN apk add --no-cache ca-certificates 3 | 4 | FROM alpine 5 | EXPOSE 8443/tcp 6 | COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt 7 | COPY k-rail /k-rail 8 | USER 65534 9 | ENTRYPOINT ["/k-rail", "-config", "/config/config.yml"] -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Cruise LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # https://www.apache.org/licenses/LICENSE-2.0 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, 9 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | # See the License for the specific language governing permissions and 11 | # limitations under the License. 12 | 13 | default: build 14 | 15 | ensure: 16 | dep ensure 17 | 18 | build: 19 | protoc -I plugins/proto/ plugins/proto/plugin.proto --go_out=plugins=grpc:plugins/proto 20 | CGO_ENABLED=0 go build -o k-rail cmd/main.go 21 | CGO_ENABLED=0 go build -o plugin plugins/examples/example.go 22 | 23 | test: 24 | CGO_ENABLED=1 go test -race -cover $(shell go list ./... | grep -v /vendor/) 25 | 26 | run-plugin: build 27 | openssl req -newkey rsa:2048 -nodes -keyout key.pem -x509 -days 365 -out cert.pem -subj '/CN=k-rail' 28 | ./k-rail -config ./plugins/examples/config.yml -plugins-path-glob ./plugin 29 | -------------------------------------------------------------------------------- /charts/k-rail/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *~ 18 | # Various IDEs 19 | .project 20 | .idea/ 21 | *.tmproj 22 | .vscode/ 23 | -------------------------------------------------------------------------------- /charts/k-rail/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | name: k-rail 3 | description: Kubernetes security tool for policy enforcement 4 | home: https://github.com/cruise-automation/k-rail 5 | version: v3.6.1 6 | maintainers: 7 | - name: cruise-automation 8 | url: https://cruise-automation.github.io/k-rail/ 9 | -------------------------------------------------------------------------------- /charts/k-rail/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* vim: set filetype=mustache: */}} 2 | {{/* 3 | Expand the name of the chart. 4 | */}} 5 | {{- define "k-rail.name" -}} 6 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} 7 | {{- end -}} 8 | 9 | {{/* 10 | Create a default fully qualified app name. 11 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 12 | If release name contains chart name it will be used as a full name. 13 | */}} 14 | {{- define "k-rail.fullname" -}} 15 | {{- if .Values.fullnameOverride -}} 16 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} 17 | {{- else -}} 18 | {{- $name := default .Chart.Name .Values.nameOverride -}} 19 | {{- if contains $name .Release.Name -}} 20 | {{- .Release.Name | trunc 63 | trimSuffix "-" -}} 21 | {{- else -}} 22 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} 23 | {{- end -}} 24 | {{- end -}} 25 | {{- end -}} 26 | 27 | {{/* 28 | Create chart name and version as used by the chart label. 29 | */}} 30 | {{- define "k-rail.chart" -}} 31 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} 32 | {{- end -}} 33 | -------------------------------------------------------------------------------- /charts/k-rail/values.yaml: -------------------------------------------------------------------------------- 1 | replicaCount: 3 2 | 3 | # Set this to 'Fail' if you wish that resources apply fail if k-rail is unreachable. 4 | # Use 'Ignore' if you wish resources to apply if k-rail is unreachable. 5 | failurePolicy: Fail 6 | 7 | image: 8 | repository: cruise/k-rail 9 | tag: # left empty for setting override, otherwise uses template default 10 | pullPolicy: IfNotPresent 11 | 12 | resources: 13 | limits: 14 | cpu: 100m 15 | memory: 256Mi 16 | requests: 17 | cpu: 100m 18 | memory: 256Mi 19 | 20 | nodeSelector: {} 21 | 22 | tolerations: [] 23 | 24 | affinity: {} 25 | 26 | # Set to the value (in seconds) which the mutatingwebhook should use for a timeout 27 | # for slower clusters (or larger workloads) this may need to be increased 28 | webhookTimeout: 1 29 | 30 | config: 31 | cluster_name: default 32 | log_level: info 33 | tls: 34 | cert: "/cert/cert.pem" 35 | key: "/cert/key.pem" 36 | global_report_only: False 37 | policy_config: 38 | policy_require_ingress_exemption_classes: 39 | - public 40 | policy_trusted_repository_regexes: 41 | - '^k8s.gcr.io/.*' # official k8s GCR repo 42 | - '^[A-Za-z0-9\-:@]+$' # official docker hub images 43 | policy_default_seccomp_policy: "runtime/default" 44 | mutate_empty_dir_size_limit: 45 | maximum_size_limit: "1Gi" 46 | default_size_limit: "512Mi" 47 | mutate_image_pull_policy: 48 | IfNotPresent: [] 49 | Always: [] 50 | policy_require_service_loadbalancer_annotations: 51 | - annotations: 52 | - "cloud.google.com/load-balancer-type" 53 | - "networking.gke.io/load-balancer-type" 54 | allowed_values: 55 | - internal 56 | - Internal 57 | allow_missing: false 58 | - annotation: "networking.gke.io/internal-load-balancer-allow-global-access" 59 | allowed_values: 60 | - true 61 | allow_missing: false 62 | policy_require_virtualservice_gateways: 63 | allowed_gateways: 64 | - "istio-system/internal-gateway" 65 | - "mesh" 66 | allow_empty_gateways: true 67 | plugin_config: 68 | example_plugin: 69 | threshold: 1.0 70 | policies: 71 | - name: "pod_no_exec" 72 | enabled: True 73 | report_only: False 74 | - name: "pod_no_bind_mounts" 75 | enabled: True 76 | report_only: False 77 | - name: "pod_no_docker_sock" 78 | enabled: True 79 | report_only: False 80 | - name: "pod_no_root_user" 81 | enabled: True 82 | report_only: False 83 | - name: "pod_immutable_reference" 84 | enabled: True 85 | report_only: False 86 | - name: "pod_no_tiller" 87 | enabled: True 88 | report_only: False 89 | - name: "pod_trusted_repository" 90 | enabled: True 91 | report_only: False 92 | - name: "pod_no_host_network" 93 | enabled: True 94 | report_only: False 95 | - name: "pod_no_privileged_container" 96 | enabled: True 97 | report_only: False 98 | - name: "pod_no_new_capabilities" 99 | enabled: True 100 | report_only: False 101 | - name: "pod_no_host_pid" 102 | enabled: True 103 | report_only: False 104 | - name: "pod_safe_to_evict" # deprecated in favor of pod_mutate_safe_to_evict policy 105 | enabled: False 106 | report_only: True 107 | - name: "pod_mutate_safe_to_evict" 108 | enabled: True 109 | report_only: False 110 | - name: "pod_empty_dir_size_limit" 111 | enabled: True 112 | report_only: False 113 | - name: "pod_no_root_user" 114 | enabled: True 115 | report_only: False 116 | - name: "pod_default_seccomp_policy" 117 | enabled: True 118 | report_only: False 119 | - name: "pod_no_shareprocessnamespace" 120 | enabled: True 121 | report_only: False 122 | - name: "pod_image_pull_policy" 123 | enabled: True 124 | report_only: False 125 | - name: "ingress_disallow_nginx_snippet" # report_only, allow operators to override 126 | enabled: True 127 | report_only: True 128 | - name: "ingress_require_ingress_exemption" 129 | enabled: True 130 | report_only: False 131 | - name: "ingress_unique_ingress_host" 132 | enabled: True 133 | report_only: False 134 | - name: "persistent_volume_no_host_path" 135 | enabled: True 136 | report_only: False 137 | - name: "service_require_loadbalancer_exemption" 138 | enabled: True 139 | report_only: False 140 | - name: "service_require_virtualservice_gateway_exemption" 141 | enabled: True 142 | report_only: False 143 | - name: "cluster_role_binding_no_anonymous_subject" 144 | enabled: True 145 | report_only: False 146 | - name: "role_binding_no_anonymous_subject" 147 | enabled: True 148 | report_only: False 149 | - name: "service_no_external_ip" 150 | enabled: True 151 | report_only: False 152 | - name: "pod_deny_unconfined_apparmor_policy" 153 | enabled: True 154 | report_only: False 155 | - name: "luck_threshold" 156 | enabled: False 157 | report_only: True 158 | 159 | exemptions: 160 | - resource_name: "*" 161 | namespace: "kube-system" 162 | username: "*" 163 | group: "*" 164 | exempt_policies: ["*"] 165 | -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Cruise LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // https://www.apache.org/licenses/LICENSE-2.0 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | package main 14 | 15 | import ( 16 | "context" 17 | 18 | "github.com/cruise-automation/k-rail/v3/server" 19 | ) 20 | 21 | func main() { 22 | ctx, cancelFunc := context.WithCancel(context.Background()) 23 | defer cancelFunc() 24 | server.Run(ctx) 25 | } 26 | -------------------------------------------------------------------------------- /contrib/analyze--init-container-breaking-changes.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # This script was used to analyze some existing workloads for point-in-time analysis and verification. 4 | # We're providing it in case it is useful. There may be later usage and documentation at some point in the future. 5 | 6 | # This was used to analyzing the backwards-incompatible policy changes introduced in https://github.com/cruise-automation/k-rail/pull/21 7 | # and present in k-rail v1.0 8 | 9 | mkdir -p /tmp/pods 10 | rm /tmp/pods/filtered.json 11 | 12 | for cluster in $(kubectx) 13 | do 14 | kubectx $cluster 15 | 16 | echo "dumping pod specs for $cluster" 17 | kubectl get po --all-namespaces -o json > /tmp/pods/pods-$cluster.json 18 | 19 | policy="privileged" 20 | echo "filtering $policy for $cluster" 21 | jq '.items[] | select(.spec.initContainers[].securityContext.privileged==true)' /tmp/pods/pods-$cluster.json | \ 22 | jq "{namespace: .metadata.namespace, name: .metadata.name, cluster: \"$cluster\", policy: \"$policy\"}" >> /tmp/pods/filtered.json 23 | 24 | policy="capabilities" 25 | echo "filtering $policy for $cluster" 26 | jq '.items[] | select(.spec.initContainers[].securityContext.capabilities.add)' /tmp/pods/pods-$cluster.json | \ 27 | jq "{namespace: .metadata.namespace, name: .metadata.name, cluster: \"$cluster\", policy: \"$policy\"}" >> /tmp/pods/filtered.json 28 | 29 | done -------------------------------------------------------------------------------- /contrib/analyze-pods.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # This script was used to analyze some existing workloads for point-in-time analysis and verification. 4 | # We're providing it in case it is useful. There may be later usage and documentation at some point in the future. 5 | 6 | mkdir -p /tmp/pods 7 | rm /tmp/pods/filtered.json 8 | 9 | for cluster in $(kubectx) 10 | do 11 | kubectx $cluster 12 | 13 | echo "dumping pod specs for $cluster" 14 | kubectl get po --all-namespaces -o json > /tmp/pods/pods-$cluster.json 15 | 16 | policy="hostnetwork" 17 | echo "filtering $policy for $cluster" 18 | jq '.items[] | select(.spec.hostNetwork==true)' /tmp/pods/pods-$cluster.json | \ 19 | jq "{namespace: .metadata.namespace, name: .metadata.name, cluster: \"$cluster\", policy: \"$policy\"}" >> /tmp/pods/filtered.json 20 | 21 | policy="hostpid" 22 | echo "filtering $policy for $cluster" 23 | jq '.items[] | select(.spec.hostPID==true)' /tmp/pods/pods-$cluster.json | \ 24 | jq "{namespace: .metadata.namespace, name: .metadata.name, cluster: \"$cluster\", policy: \"$policy\"}" >> /tmp/pods/filtered.json 25 | 26 | policy="privileged" 27 | echo "filtering $policy for $cluster" 28 | jq '.items[] | select(.spec.containers[].securityContext.privileged==true)' /tmp/pods/pods-$cluster.json | \ 29 | jq "{namespace: .metadata.namespace, name: .metadata.name, cluster: \"$cluster\", policy: \"$policy\"}" >> /tmp/pods/filtered.json 30 | 31 | policy="hostpath" 32 | echo "filtering $policy for $cluster" 33 | jq '.items[] | select(.spec.volumes[].hostPath)' /tmp/pods/pods-$cluster.json | \ 34 | jq "{namespace: .metadata.namespace, name: .metadata.name, cluster: \"$cluster\", policy: \"$policy\"}" >> /tmp/pods/filtered.json 35 | 36 | policy="dockersock" 37 | echo "filtering $policy for $cluster" 38 | jq '.items[] | select(.spec.containers[].volumeMounts[].mountPath=="/var/run/docker.sock")' /tmp/pods/pods-$cluster.json | \ 39 | jq "{namespace: .metadata.namespace, name: .metadata.name, cluster: \"$cluster\", policy: \"$policy\"}" >> /tmp/pods/filtered.json 40 | 41 | policy="capabilities" 42 | echo "filtering $policy for $cluster" 43 | jq '.items[] | select(.spec.containers[].securityContext.capabilities.add)' /tmp/pods/pods-$cluster.json | \ 44 | jq "{namespace: .metadata.namespace, name: .metadata.name, cluster: \"$cluster\", policy: \"$policy\"}" >> /tmp/pods/filtered.json 45 | 46 | done -------------------------------------------------------------------------------- /examples/exemptions.example.yaml: -------------------------------------------------------------------------------- 1 | ### 2 | # For the Helm deployment, all policy and exemption configuration is contained in deploy/helm/values.yaml. 3 | # This example file is provided for reference. 4 | ### 5 | 6 | --- 7 | # bad-pod needs host network for GCE metadata compromising 8 | - resource_name: bad-pod 9 | namespace: bad-project 10 | exempt_policies: ["pod_no_host_network"] 11 | 12 | # allow everything 13 | # - resource_name: "*" 14 | # namespace: "*" 15 | # username: "*" 16 | # group: "*" 17 | # exempt_policies: ["*"] 18 | -------------------------------------------------------------------------------- /examples/non-compliant-clusterrolebinding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | # This cluster role binding allows anyone in the "manager" group to read secrets in any namespace. 3 | kind: ClusterRoleBinding 4 | metadata: 5 | name: bad-cluster-role-binding 6 | subjects: # You can specify more than one "subject" 7 | - kind: Group 8 | name: system:unauthenticated # Name is case sensitive 9 | apiGroup: rbac.authorization.k8s.io 10 | roleRef: # "roleRef" specifies the binding to a Role / ClusterRole 11 | kind: ClusterRole 12 | name: view 13 | apiGroup: rbac.authorization.k8s.io -------------------------------------------------------------------------------- /examples/non-compliant-daemonset.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apps/v1 3 | kind: DaemonSet 4 | metadata: 5 | name: bad-daemonset 6 | spec: 7 | selector: 8 | matchLabels: 9 | name: bad-daemonset 10 | template: 11 | metadata: 12 | labels: 13 | name: bad-daemonset 14 | spec: 15 | hostNetwork: true 16 | hostPID: true 17 | securityContext: 18 | runAsNonRoot: false 19 | runAsUser: 0 20 | volumes: 21 | - name: dockersock 22 | hostPath: 23 | path: /var/run/docker.sock 24 | - name: hostroot 25 | hostPath: 26 | path: / 27 | containers: 28 | - name: bad-daemonset 29 | image: ubuntu 30 | command: ["sleep", "36000"] 31 | imagePullPolicy: Always 32 | volumeMounts: 33 | - name: dockersock 34 | mountPath: "/var/run/docker.sock" 35 | - name: hostroot 36 | mountPath: "/host" 37 | securityContext: 38 | runAsUser: 0 39 | runAsNonRoot: false 40 | privileged: true 41 | capabilities: 42 | add: ["NET_ADMIN", "SYS_ADMIN"] 43 | -------------------------------------------------------------------------------- /examples/non-compliant-deployment.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | name: bad-deployment 6 | spec: 7 | replicas: 1 8 | selector: 9 | matchLabels: 10 | name: bad-deployment 11 | template: 12 | metadata: 13 | labels: 14 | name: bad-deployment 15 | spec: 16 | hostNetwork: true 17 | hostPID: true 18 | securityContext: 19 | runAsNonRoot: false 20 | runAsUser: 0 21 | volumes: 22 | - name: dockersock 23 | hostPath: 24 | path: /var/run/docker.sock 25 | - name: hostroot 26 | hostPath: 27 | path: / 28 | containers: 29 | - name: bad-deployment 30 | image: ubuntu 31 | command: ["sleep", "36000"] 32 | imagePullPolicy: Always 33 | volumeMounts: 34 | - name: dockersock 35 | mountPath: "/var/run/docker.sock" 36 | - name: hostroot 37 | mountPath: "/host" 38 | securityContext: 39 | runAsUser: 0 40 | runAsNonRoot: false 41 | privileged: true 42 | capabilities: 43 | add: ["NET_ADMIN", "SYS_ADMIN"] 44 | 45 | 46 | -------------------------------------------------------------------------------- /examples/non-compliant-ingress.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: extensions/v1beta1 3 | kind: Ingress 4 | metadata: 5 | name: bad-ingress 6 | annotations: 7 | kubernetes.io/ingress.class: public 8 | spec: 9 | rules: 10 | - host: foo.bar 11 | http: 12 | paths: 13 | - path: / 14 | backend: 15 | serviceName: bad-backend 16 | servicePort: 666 17 | 18 | -------------------------------------------------------------------------------- /examples/non-compliant-persistent-volume.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: PersistentVolume 3 | metadata: 4 | name: bad-pv-volume 5 | spec: 6 | storageClassName: manual 7 | capacity: 8 | storage: 128Mi 9 | accessModes: 10 | - ReadWriteOnce 11 | hostPath: 12 | path: "/path/to/data" -------------------------------------------------------------------------------- /examples/non-compliant-poddisruptionbudget.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | name: test-nginx-pdb 6 | labels: 7 | app: test-nginx-pdb 8 | spec: 9 | replicas: 2 10 | selector: 11 | matchLabels: 12 | app: test-nginx-pdb 13 | template: 14 | metadata: 15 | labels: 16 | app: test-nginx-pdb 17 | spec: 18 | containers: 19 | - name: nginx 20 | image: nginx:1.7.9 21 | ports: 22 | - containerPort: 80 23 | --- 24 | apiVersion: policy/v1beta1 25 | kind: PodDisruptionBudget 26 | metadata: 27 | name: bad-pdb-min 28 | spec: 29 | minAvailable: 2 30 | selector: 31 | matchLabels: 32 | app: test-nginx-pdb 33 | --- 34 | apiVersion: policy/v1beta1 35 | kind: PodDisruptionBudget 36 | metadata: 37 | name: bad-pdb-max 38 | spec: 39 | maxUnavailable: 49% 40 | selector: 41 | matchLabels: 42 | app: test-nginx-pdb 43 | -------------------------------------------------------------------------------- /examples/non-compliant-rolebinding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: RoleBinding 3 | metadata: 4 | name: bad-role-binding 5 | namespace: default 6 | roleRef: 7 | apiGroup: rbac.authorization.k8s.io 8 | kind: Role 9 | name: pod-reader 10 | subjects: 11 | - apiGroup: rbac.authorization.k8s.io 12 | kind: Group 13 | name: "system:anonymous" 14 | --- 15 | apiVersion: rbac.authorization.k8s.io/v1 16 | kind: Role 17 | metadata: 18 | namespace: default 19 | name: pod-reader 20 | rules: 21 | - apiGroups: [""] # "" indicates the core API group 22 | resources: ["pods"] 23 | verbs: ["get", "list"] -------------------------------------------------------------------------------- /examples/non-compliant-service-loadbalancer.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: my-service 5 | spec: 6 | selector: 7 | app: MyApp 8 | ports: 9 | - protocol: TCP 10 | port: 80 11 | targetPort: 9376 12 | type: LoadBalancer 13 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/cruise-automation/k-rail/v3 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/gobwas/glob v0.2.3 7 | github.com/golang/protobuf v1.5.1 // indirect 8 | github.com/gorilla/mux v1.8.0 9 | github.com/hashicorp/go-plugin v1.4.0 10 | github.com/opencontainers/go-digest v1.0.0-rc1 11 | github.com/prometheus/client_golang v1.7.1 12 | github.com/sirupsen/logrus v1.6.0 13 | github.com/slok/go-http-metrics v0.9.0 14 | github.com/stretchr/testify v1.6.1 15 | golang.org/x/net v0.0.0-20201110031124-69a78807bb2b 16 | google.golang.org/grpc v1.35.0 17 | google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0 // indirect 18 | google.golang.org/protobuf v1.26.0 19 | gopkg.in/yaml.v2 v2.3.0 // indirect 20 | istio.io/api v0.0.0-20210817134638-1b724d2c575d 21 | k8s.io/api v0.20.2 22 | k8s.io/apiextensions-apiserver v0.20.2 23 | k8s.io/apimachinery v0.20.2 24 | k8s.io/client-go v0.20.2 //v11.0.0+incompatible 25 | k8s.io/utils v0.0.0-20201110183641-67b214c5f920 26 | sigs.k8s.io/yaml v1.2.0 27 | ) 28 | 29 | replace git.apache.org/thrift.git => github.com/apache/thrift v0.12.0 30 | -------------------------------------------------------------------------------- /images/k-rail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cruise-automation/k-rail/284b16d0a00bafc9532298dffd918ca572662724/images/k-rail.png -------------------------------------------------------------------------------- /plugins/examples/README.md: -------------------------------------------------------------------------------- 1 | Example Plugin 2 | -------------- 3 | 4 | This directory contains an example K-Rail plugin in order to allow custom written K-Rail policies for your organization that are not general purpose enough for open-source usage. 5 | 6 | # K-Rail plugins 7 | K-Rail plugins are based on the [go-plugin Hashicorp](https://github.com/hashicorp/go-plugin/) interface which runs the provided plugin executable over localhost via GRPC and protobufs. This means that theoretically any language that supports GRPC can be written as a K-Rail plugin as long as it conforms to the [KRailPlugin protobuf service](../proto/plugin.proto) as seen below. 8 | 9 | ```protobuf 10 | service KRailPlugin { 11 | rpc PluginName(PluginNameRequest) returns (PluginNameResponse); 12 | rpc PolicyNames(PolicyNamesRequest) returns (PolicyNamesResponse); 13 | rpc ConfigurePlugin(ConfigurePluginRequest) returns (ConfigurePluginResponse); 14 | rpc Validate(ValidateRequest) returns (ValidateResponse); 15 | } 16 | ``` 17 | 18 | `PluginName` returns the name of the plugin as a string which is then used in the `plugin_config` stanza for providing customizable yaml configuration. 19 | 20 | `PolicyNames` returns the names of all the policies implemented by the plugin as an array of strings which is then used to configure them under the `policies` stanza as `enabled` and `report_only` 21 | 22 | `ConfigurePlugin` provides the customizable yaml from under corresponding `plugin_config` and plugin name stanza to initialize the plugin 23 | 24 | `Validate` accepts the policy name with an AdmissionRequest. The resource of interest must be extracted from it. See `resource/pod.go` for an example of extracting PodSpecs from an AdmissionRequest. If mutation on a resource is desired, you can return a slice of JSONPatch operations and `nil` for the violations. 25 | 26 | # About the example 27 | The example plugin is written in Go as a template that can be copied for your own usage with a simple example "luck_threshold" policy included. The "luck_threshold" policy rejects pods from validation based on a configurable threshold. It can be run locally with the included config.yml by using the `make run-plugin` command. Definitely do not use this example policy on a production cluster as it will cause frustration by rejecting 1% of pod deployments by default. 28 | -------------------------------------------------------------------------------- /plugins/examples/config.yml: -------------------------------------------------------------------------------- 1 | cluster_name: default 2 | log_level: info 3 | tls: 4 | cert: "cert.pem" 5 | key: "key.pem" 6 | plugin_config: 7 | example_plugin: 8 | threshold: 0.0 9 | policies: 10 | - name: "luck_threshold" 11 | enabled: True 12 | report_only: False 13 | -------------------------------------------------------------------------------- /plugins/examples/example.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "math/rand" 8 | 9 | "github.com/hashicorp/go-plugin" 10 | admissionv1 "k8s.io/api/admission/v1" 11 | 12 | "github.com/cruise-automation/k-rail/v3/plugins" 13 | "github.com/cruise-automation/k-rail/v3/policies" 14 | "github.com/cruise-automation/k-rail/v3/resource" 15 | ) 16 | 17 | const pluginName = "example_plugin" 18 | 19 | // This implements the plugins.KRailPlugin interface in-order to implement 20 | // the GRPC KRailPlugin service defined in the plugins.proto specification 21 | type ExamplePlugin struct { 22 | Config Config 23 | Policies map[string]Policy 24 | } 25 | 26 | type Config struct { 27 | Threshold float64 28 | } 29 | 30 | func (p ExamplePlugin) PluginName() (string, error) { 31 | return pluginName, nil 32 | } 33 | 34 | func (p ExamplePlugin) RegisterPolicy(policy Policy) { 35 | p.Policies[policy.Name()] = policy 36 | } 37 | 38 | func (p ExamplePlugin) PolicyNames() ([]string, error) { 39 | names := []string{} 40 | for name, _ := range p.Policies { 41 | names = append(names, name) 42 | } 43 | return names, nil 44 | } 45 | 46 | func (p ExamplePlugin) ConfigurePlugin(config map[string]interface{}) error { 47 | if threshold, ok := config["threshold"]; ok { 48 | if threshold64, ok := threshold.(float64); ok { 49 | p.Config.Threshold = threshold64 50 | log.Printf("Configured luck threshold to %.2f\n", threshold64) 51 | } 52 | } 53 | return nil 54 | } 55 | 56 | func (p ExamplePlugin) Validate(policyName string, ar *admissionv1.AdmissionRequest) ([]policies.ResourceViolation, []policies.PatchOperation, error) { 57 | if policy, ok := p.Policies[policyName]; ok { 58 | violations, patchOps := policy.Validate(context.Background(), p.Config, ar) 59 | return violations, patchOps, nil 60 | } 61 | return []policies.ResourceViolation{}, nil, fmt.Errorf("unknown policy %s for plugin %s\n", policyName, pluginName) 62 | } 63 | 64 | // This is mostly a copy of the server.Policies with the custom Config object being the only difference 65 | type Policy interface { 66 | Name() string 67 | Validate(ctx context.Context, 68 | config Config, 69 | ar *admissionv1.AdmissionRequest, 70 | ) ([]policies.ResourceViolation, []policies.PatchOperation) 71 | } 72 | 73 | type ThresholdPolicy struct{} 74 | 75 | func (t ThresholdPolicy) Name() string { 76 | return "luck_threshold" 77 | } 78 | 79 | func (t ThresholdPolicy) Validate(ctx context.Context, 80 | config Config, 81 | ar *admissionv1.AdmissionRequest, 82 | ) ([]policies.ResourceViolation, []policies.PatchOperation) { 83 | 84 | resourceViolations := []policies.ResourceViolation{} 85 | podResource := resource.GetPodResource(ctx, ar) 86 | if podResource == nil { 87 | return resourceViolations, nil 88 | } 89 | 90 | if rand.Float64() > config.Threshold { 91 | violationText := fmt.Sprintf("This Pod was unlucky and didn't clear the random %.2f%% threshold, rejecting", config.Threshold) 92 | resourceViolations = append(resourceViolations, policies.ResourceViolation{ 93 | Namespace: ar.Namespace, 94 | ResourceName: podResource.ResourceName, 95 | ResourceKind: podResource.ResourceKind, 96 | Violation: violationText, 97 | Policy: t.Name(), 98 | }) 99 | 100 | } 101 | return resourceViolations, nil 102 | } 103 | 104 | func main() { 105 | // Default the luck threshold to 99% (it's very lucky) 106 | examplePlugin := &ExamplePlugin{Config: Config{Threshold: 0.99}, Policies: map[string]Policy{}} 107 | examplePlugin.RegisterPolicy(ThresholdPolicy{}) 108 | 109 | plugin.Serve(&plugin.ServeConfig{ 110 | HandshakeConfig: plugins.Handshake, 111 | Plugins: map[string]plugin.Plugin{ 112 | plugins.GRPCPluginName: &plugins.KRailGRPCPlugin{Impl: examplePlugin}, 113 | }, 114 | 115 | // A non-nil value here enables gRPC serving for this plugin... 116 | GRPCServer: plugin.DefaultGRPCServer, 117 | }) 118 | } 119 | -------------------------------------------------------------------------------- /plugins/grpc.go: -------------------------------------------------------------------------------- 1 | package plugins 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | 7 | "golang.org/x/net/context" 8 | structpb "google.golang.org/protobuf/types/known/structpb" 9 | admissionv1 "k8s.io/api/admission/v1" 10 | 11 | "github.com/cruise-automation/k-rail/v3/plugins/proto" 12 | "github.com/cruise-automation/k-rail/v3/policies" 13 | ) 14 | 15 | // GRPCClient is an implementation of KRailPlugin that talks over RPC. 16 | type GRPCClient struct{ client proto.KRailPluginClient } 17 | 18 | func (m *GRPCClient) PluginName() (string, error) { 19 | resp, err := m.client.PluginName(context.Background(), &proto.PluginNameRequest{}) 20 | if err != nil { 21 | return "", err 22 | } 23 | return resp.PluginName, nil 24 | } 25 | 26 | func (m *GRPCClient) PolicyNames() ([]string, error) { 27 | resp, err := m.client.PolicyNames(context.Background(), &proto.PolicyNamesRequest{}) 28 | if err != nil { 29 | return []string{}, err 30 | } 31 | return resp.PolicyNames, nil 32 | } 33 | 34 | func (m *GRPCClient) ConfigurePlugin(config map[string]interface{}) error { 35 | configStruct, err := structpb.NewStruct(config) 36 | if err != nil { 37 | return err 38 | } 39 | _, err = m.client.ConfigurePlugin(context.Background(), &proto.ConfigurePluginRequest{ 40 | PluginConfig: configStruct, 41 | }) 42 | return err 43 | } 44 | 45 | func (m *GRPCClient) Validate(policyName string, ar *admissionv1.AdmissionRequest) ([]policies.ResourceViolation, []policies.PatchOperation, error) { 46 | resourceViolations := []policies.ResourceViolation{} 47 | arJson, err := json.Marshal(ar) 48 | if err != nil { 49 | return resourceViolations, nil, err 50 | } 51 | resp, err := m.client.Validate(context.Background(), &proto.ValidateRequest{ 52 | PolicyName: policyName, 53 | AdmissionRequest: arJson, 54 | }) 55 | if err != nil { 56 | return resourceViolations, nil, err 57 | } 58 | for _, violation := range resp.ResourceViolations { 59 | resourceViolations = append(resourceViolations, policies.ResourceViolation{ 60 | ResourceName: violation.ResourceName, 61 | ResourceKind: violation.ResourceKind, 62 | Namespace: violation.Namespace, 63 | Violation: violation.Violation, 64 | Policy: violation.Policy, 65 | Error: errors.New(violation.Error), 66 | }) 67 | } 68 | patchOperations := []policies.PatchOperation{} 69 | for _, patchOp := range resp.PatchOperations { 70 | patchOperations = append(patchOperations, policies.PatchOperation{ 71 | Op: patchOp.Op, Path: patchOp.Path, 72 | Value: patchOp.Value.AsInterface(), 73 | }) 74 | } 75 | return resourceViolations, patchOperations, err 76 | } 77 | 78 | // Here is the gRPC server that GRPCClient talks to. 79 | type GRPCServer struct { 80 | // This is the real implementation 81 | Impl KRailPlugin 82 | } 83 | 84 | func (m *GRPCServer) PluginName(ctx context.Context, in *proto.PluginNameRequest) (*proto.PluginNameResponse, error) { 85 | pluginName, err := m.Impl.PluginName() 86 | return &proto.PluginNameResponse{PluginName: pluginName}, err 87 | } 88 | 89 | func (m *GRPCServer) PolicyNames(ctx context.Context, in *proto.PolicyNamesRequest) (*proto.PolicyNamesResponse, error) { 90 | policyNames, err := m.Impl.PolicyNames() 91 | return &proto.PolicyNamesResponse{PolicyNames: policyNames}, err 92 | } 93 | 94 | func (m *GRPCServer) ConfigurePlugin(ctx context.Context, in *proto.ConfigurePluginRequest) (*proto.ConfigurePluginResponse, error) { 95 | return &proto.ConfigurePluginResponse{}, m.Impl.ConfigurePlugin(in.PluginConfig.AsMap()) 96 | } 97 | 98 | func (m *GRPCServer) Validate(ctx context.Context, in *proto.ValidateRequest) (*proto.ValidateResponse, error) { 99 | var ar admissionv1.AdmissionRequest 100 | err := json.Unmarshal(in.AdmissionRequest, &ar) 101 | if err != nil { 102 | return nil, err 103 | } 104 | resourceViolations, patchOperations, err := m.Impl.Validate(in.PolicyName, &ar) 105 | if err != nil { 106 | return nil, err 107 | } 108 | 109 | violations := []*proto.ResourceViolation{} 110 | for _, violation := range resourceViolations { 111 | violations = append(violations, &proto.ResourceViolation{ 112 | ResourceName: violation.ResourceName, 113 | ResourceKind: violation.ResourceKind, 114 | Namespace: violation.Namespace, 115 | Violation: violation.Violation, 116 | Policy: violation.Policy, 117 | Error: violation.Error.Error(), 118 | }) 119 | } 120 | patchOps := []*proto.PatchOperation{} 121 | for _, patchOp := range patchOperations { 122 | valueStruct, err := structpb.NewValue(patchOp.Value) 123 | if err != nil { 124 | return nil, err 125 | } 126 | patchOps = append(patchOps, &proto.PatchOperation{ 127 | Op: patchOp.Op, 128 | Path: patchOp.Path, 129 | Value: valueStruct, 130 | }) 131 | } 132 | return &proto.ValidateResponse{ 133 | ResourceViolations: violations, 134 | PatchOperations: patchOps, 135 | }, err 136 | } 137 | -------------------------------------------------------------------------------- /plugins/plugins.go: -------------------------------------------------------------------------------- 1 | package plugins 2 | 3 | import ( 4 | "context" 5 | "os/exec" 6 | "path/filepath" 7 | 8 | "github.com/hashicorp/go-plugin" 9 | log "github.com/sirupsen/logrus" 10 | "google.golang.org/grpc" 11 | admissionv1 "k8s.io/api/admission/v1" 12 | 13 | "github.com/cruise-automation/k-rail/v3/plugins/proto" 14 | "github.com/cruise-automation/k-rail/v3/policies" 15 | ) 16 | 17 | type Plugin struct { 18 | name string 19 | policyNames []string 20 | client plugin.Client 21 | kRailPlugin KRailPlugin 22 | } 23 | 24 | func (p *Plugin) Name() string { 25 | return p.name 26 | } 27 | 28 | func (p *Plugin) PolicyNames() []string { 29 | return p.policyNames 30 | } 31 | 32 | func (p *Plugin) Configure(config map[string]interface{}) error { 33 | return p.kRailPlugin.ConfigurePlugin(config) 34 | } 35 | 36 | func (p *Plugin) Kill() { 37 | p.client.Kill() 38 | } 39 | 40 | func (p *Plugin) Validate(policyName string, ar *admissionv1.AdmissionRequest) ([]policies.ResourceViolation, []policies.PatchOperation, error) { 41 | return p.kRailPlugin.Validate(policyName, ar) 42 | } 43 | 44 | // PluginPolicy implements the server.Policy interface 45 | type PluginPolicy struct { 46 | name string 47 | plugin Plugin 48 | } 49 | 50 | func NewPluginPolicy(name string, plugin Plugin) PluginPolicy { 51 | return PluginPolicy{name: name, plugin: plugin} 52 | } 53 | 54 | func (p PluginPolicy) Name() string { 55 | return p.name 56 | } 57 | 58 | func (p PluginPolicy) Validate(ctx context.Context, 59 | config policies.Config, 60 | ar *admissionv1.AdmissionRequest, 61 | ) ([]policies.ResourceViolation, []policies.PatchOperation) { 62 | 63 | violations, patchOps, err := p.plugin.Validate(p.name, ar) 64 | 65 | if err != nil { 66 | log.WithError(err).Errorf("error running Validate on Plugin %s Policy %s\n", p.plugin.name, p.name) 67 | return []policies.ResourceViolation{}, nil 68 | } 69 | return violations, patchOps 70 | } 71 | 72 | // KRailPlugin is the interface that we're exposing as a plugin. 73 | type KRailPlugin interface { 74 | PluginName() (string, error) 75 | PolicyNames() ([]string, error) 76 | ConfigurePlugin(config map[string]interface{}) error 77 | Validate(policyName string, ar *admissionv1.AdmissionRequest) ([]policies.ResourceViolation, []policies.PatchOperation, error) 78 | } 79 | 80 | // This is the implementation of plugin.GRPCPlugin so we can serve/consume this. 81 | type KRailGRPCPlugin struct { 82 | // GRPCPlugin must still implement the Plugin interface 83 | plugin.Plugin 84 | // Concrete implementation, written in Go. This is only used for plugins 85 | // that are written in Go. 86 | Impl KRailPlugin 87 | } 88 | 89 | func (p *KRailGRPCPlugin) GRPCServer(broker *plugin.GRPCBroker, s *grpc.Server) error { 90 | proto.RegisterKRailPluginServer(s, &GRPCServer{Impl: p.Impl}) 91 | return nil 92 | } 93 | 94 | func (p *KRailGRPCPlugin) GRPCClient(ctx context.Context, broker *plugin.GRPCBroker, c *grpc.ClientConn) (interface{}, error) { 95 | return &GRPCClient{client: proto.NewKRailPluginClient(c)}, nil 96 | } 97 | 98 | func PluginsFromDirectory(directory string) ([]Plugin, error) { 99 | binaries, err := filepath.Glob(directory) 100 | if err != nil { 101 | return []Plugin{}, err 102 | } 103 | 104 | pluginClients := []Plugin{} 105 | for _, binary := range binaries { 106 | pluginClient, err := LaunchPluginProcess(binary) 107 | if err != nil { 108 | return pluginClients, err 109 | 110 | } 111 | pluginClients = append(pluginClients, *pluginClient) 112 | } 113 | return pluginClients, nil 114 | } 115 | 116 | var Handshake = plugin.HandshakeConfig{ 117 | ProtocolVersion: 1, 118 | MagicCookieKey: "K_RAIL_PLUGIN", 119 | MagicCookieValue: "TRUE", 120 | } 121 | 122 | const GRPCPluginName = "K_RAIL_GRPC" 123 | 124 | var pluginMap = map[string]plugin.Plugin{ 125 | GRPCPluginName: &KRailGRPCPlugin{}, 126 | } 127 | 128 | func LaunchPluginProcess(binaryPath string) (*Plugin, error) { 129 | client := plugin.NewClient(&plugin.ClientConfig{ 130 | HandshakeConfig: Handshake, 131 | Plugins: pluginMap, 132 | Cmd: exec.Command(binaryPath), 133 | AllowedProtocols: []plugin.Protocol{plugin.ProtocolGRPC}, 134 | }) 135 | 136 | // Connect via RPC 137 | rpcClient, err := client.Client() 138 | if err != nil { 139 | return nil, err 140 | } 141 | 142 | // Request the plugin 143 | raw, err := rpcClient.Dispense(GRPCPluginName) 144 | if err != nil { 145 | return nil, err 146 | } 147 | 148 | krailPlugin := raw.(KRailPlugin) 149 | 150 | pluginName, err := krailPlugin.PluginName() 151 | if err != nil { 152 | return nil, err 153 | } 154 | 155 | policyNames, err := krailPlugin.PolicyNames() 156 | if err != nil { 157 | return nil, err 158 | } 159 | 160 | return &Plugin{ 161 | name: pluginName, 162 | policyNames: policyNames, 163 | client: *client, 164 | kRailPlugin: krailPlugin, 165 | }, nil 166 | } 167 | -------------------------------------------------------------------------------- /plugins/proto/plugin.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package proto; 3 | option go_package = "./;proto"; 4 | import "google/protobuf/struct.proto"; 5 | 6 | message PluginNameRequest {} 7 | 8 | message PluginNameResponse { 9 | string plugin_name = 1; 10 | } 11 | 12 | message PolicyNamesRequest {} 13 | 14 | message PolicyNamesResponse { 15 | repeated string policy_names = 1; 16 | } 17 | 18 | message ConfigurePluginRequest { 19 | google.protobuf.Struct plugin_config = 1; 20 | } 21 | 22 | message ConfigurePluginResponse {} 23 | 24 | message ValidateRequest { 25 | string policy_name = 1; 26 | bytes admission_request = 2; 27 | } 28 | 29 | message ValidateResponse { 30 | repeated ResourceViolation resource_violations = 1; 31 | repeated PatchOperation patch_operations = 2; 32 | } 33 | 34 | message ResourceViolation { 35 | string resource_name = 1; 36 | string resource_kind = 2; 37 | string namespace = 3; 38 | string violation = 4; 39 | string policy = 5; 40 | string error = 6; 41 | } 42 | 43 | message PatchOperation { 44 | string op = 1; 45 | string path = 2; 46 | google.protobuf.Value value = 3; 47 | } 48 | 49 | service KRailPlugin { 50 | rpc PluginName(PluginNameRequest) returns (PluginNameResponse); 51 | rpc PolicyNames(PolicyNamesRequest) returns (PolicyNamesResponse); 52 | rpc ConfigurePlugin(ConfigurePluginRequest) returns (ConfigurePluginResponse); 53 | rpc Validate(ValidateRequest) returns (ValidateResponse); 54 | } 55 | -------------------------------------------------------------------------------- /policies/clusterrolebinding/no_anonymous_cluster_role_binding.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Cruise LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // https://www.apache.org/licenses/LICENSE-2.0 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | package clusterrolebinding 14 | 15 | import ( 16 | "context" 17 | "strings" 18 | 19 | admissionv1 "k8s.io/api/admission/v1" 20 | 21 | "github.com/cruise-automation/k-rail/v3/policies" 22 | "github.com/cruise-automation/k-rail/v3/resource" 23 | ) 24 | 25 | type PolicyNoAnonymousClusterRoleBinding struct{} 26 | 27 | func (p PolicyNoAnonymousClusterRoleBinding) Name() string { 28 | return "cluster_role_binding_no_anonymous_subject" 29 | } 30 | 31 | func (p PolicyNoAnonymousClusterRoleBinding) Validate(ctx context.Context, config policies.Config, ar *admissionv1.AdmissionRequest) ([]policies.ResourceViolation, []policies.PatchOperation) { 32 | 33 | resourceViolations := []policies.ResourceViolation{} 34 | crbResource := resource.GetClusterRoleBindingResource(ctx, ar) 35 | if crbResource == nil { 36 | return resourceViolations, nil 37 | } 38 | 39 | violationText := "No Anonymous Cluster Role Binding: Granting permissions to anonymous or unauthenticated subject is forbidden" 40 | for _, subject := range crbResource.ClusterRoleBinding.Subjects { 41 | if (strings.ToLower(subject.Name) == "system:anonymous") || (strings.ToLower(subject.Name) == "system:unauthenticated") { 42 | resourceViolations = append(resourceViolations, policies.ResourceViolation{ 43 | Namespace: ar.Namespace, 44 | ResourceName: crbResource.ResourceName, 45 | ResourceKind: crbResource.ResourceKind, 46 | Violation: violationText, 47 | Policy: p.Name(), 48 | }) 49 | } 50 | } 51 | 52 | return resourceViolations, nil 53 | } 54 | -------------------------------------------------------------------------------- /policies/config.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Cruise LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // https://www.apache.org/licenses/LICENSE-2.0 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | package policies 14 | 15 | import ( 16 | "encoding/json" 17 | "errors" 18 | "fmt" 19 | 20 | apiresource "k8s.io/apimachinery/pkg/api/resource" 21 | ) 22 | 23 | // Config contains configuration for Policies 24 | type Config struct { 25 | // PolicyRequireIngressExemptionClasses contains the Ingress classes that an exemption is required for 26 | // to use. Typically this would include your public ingress classes. 27 | PolicyRequireIngressExemptionClasses []string `json:"policy_require_ingress_exemption_classes"` 28 | // PolicyRequireServiceLoadBalancerAnnotations contains the Service LB types annotation that are allowed with this policy. 29 | PolicyRequireServiceLoadBalancerAnnotations []*AnnotationConfig `json:"policy_require_service_loadbalancer_annotations"` 30 | // PolicyRequireVirtualServiceGateways contains the Gateways that are allowed with this policy. 31 | PolicyRequireVirtualServiceGateways *VirtualServiceGatewaysConfig `json:"policy_require_virtualservice_gateways"` 32 | // PolicyTrustedRepositoryRegexes contains regexes that match image repositories that you want to allow. 33 | PolicyTrustedRepositoryRegexes []string `json:"policy_trusted_repository_regexes"` 34 | // PolicyDefaultSeccompPolicy contains the seccomp policy that you want to be applied on Pods by default. 35 | // Defaults to 'runtime/default' 36 | PolicyDefaultSeccompPolicy string `json:"policy_default_seccomp_policy"` 37 | // PolicyImagePullPolicy contains the images that needs to enforce to a specific ImagePullPolicy 38 | PolicyImagePullPolicy map[string][]string `json:"mutate_image_pull_policy"` 39 | MutateEmptyDirSizeLimit MutateEmptyDirSizeLimit `json:"mutate_empty_dir_size_limit"` 40 | } 41 | 42 | // AnnotationConfig defines a single annotation config 43 | type AnnotationConfig struct { 44 | Annotation string `json:"annotation"` 45 | Annotations []string `json:"annotations"` 46 | AllowedValues []string `json:"allowed_values"` 47 | AllowMissing bool `json:"allow_missing"` 48 | } 49 | 50 | type VirtualServiceGatewaysConfig struct { 51 | AllowedGateways []string `json:"allowed_gateways"` 52 | AllowEmptyGateways bool `json:"allow_empty_gateways"` 53 | } 54 | 55 | type MutateEmptyDirSizeLimit struct { 56 | MaximumSizeLimit apiresource.Quantity `json:"maximum_size_limit"` 57 | DefaultSizeLimit apiresource.Quantity `json:"default_size_limit"` 58 | } 59 | 60 | func (m *MutateEmptyDirSizeLimit) UnmarshalJSON(value []byte) error { 61 | var v map[string]json.RawMessage 62 | if err := json.Unmarshal(value, &v); err != nil { 63 | return err 64 | } 65 | 66 | if max, ok := v["maximum_size_limit"]; ok { 67 | if err := m.MaximumSizeLimit.UnmarshalJSON(max); err != nil { 68 | return fmt.Errorf("maximum_size_limit failed: %s", err) 69 | } 70 | } 71 | if def, ok := v["default_size_limit"]; ok { 72 | if err := m.DefaultSizeLimit.UnmarshalJSON(def); err != nil { 73 | return fmt.Errorf("default_size_limit failed: %s", err) 74 | } 75 | } 76 | if m.DefaultSizeLimit.IsZero() { 77 | return errors.New("default size must not be empty") 78 | } 79 | if m.MaximumSizeLimit.IsZero() { 80 | return errors.New("max size must not be empty") 81 | } 82 | if m.DefaultSizeLimit.Cmp(m.MaximumSizeLimit) > 0 { 83 | return errors.New("default size must not be greater than max size") 84 | } 85 | return nil 86 | } 87 | -------------------------------------------------------------------------------- /policies/config_test.go: -------------------------------------------------------------------------------- 1 | package policies 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | apiresource "k8s.io/apimachinery/pkg/api/resource" 8 | "sigs.k8s.io/yaml" 9 | ) 10 | 11 | func TestMutateEmptyDirSizeLimit(t *testing.T) { 12 | specs := map[string]struct { 13 | src string 14 | exp *MutateEmptyDirSizeLimit 15 | expErr bool 16 | }{ 17 | 18 | "all good": { 19 | src: ` 20 | mutate_empty_dir_size_limit: 21 | maximum_size_limit: "1Gi" 22 | default_size_limit: "512Mi" 23 | `, 24 | exp: &MutateEmptyDirSizeLimit{ 25 | MaximumSizeLimit: apiresource.MustParse("1Gi"), 26 | DefaultSizeLimit: apiresource.MustParse("512Mi"), 27 | }, 28 | }, 29 | "default > max": { 30 | src: ` 31 | mutate_empty_dir_size_limit: 32 | maximum_size_limit: "1Gi" 33 | default_size_limit: "2Gi" 34 | `, 35 | expErr: true, 36 | }, 37 | "default not set": { 38 | src: ` 39 | mutate_empty_dir_size_limit: 40 | maximum_size_limit: "1Gi" 41 | `, 42 | expErr: true, 43 | }, 44 | "max not set": { 45 | src: ` 46 | mutate_empty_dir_size_limit: 47 | default_size_limit: "2Gi" 48 | `, 49 | expErr: true, 50 | }, 51 | "unsupported type": { 52 | src: ` 53 | mutate_empty_dir_size_limit: 54 | default_size_limit: "2ALX" 55 | maximum_size_limit: "2ALX" 56 | `, 57 | expErr: true, 58 | }, 59 | } 60 | for msg, spec := range specs { 61 | t.Run(msg, func(t *testing.T) { 62 | var cfg Config 63 | switch err := yaml.Unmarshal([]byte(spec.src), &cfg); { 64 | case spec.expErr && err != nil: 65 | return 66 | case spec.expErr: 67 | t.Fatal("expected error") 68 | case !spec.expErr && err != nil: 69 | t.Fatalf("unexpected error: %+v", err) 70 | } 71 | if exp, got := *spec.exp, cfg.MutateEmptyDirSizeLimit; !reflect.DeepEqual(exp, got) { 72 | t.Errorf("expected %v but got %v", exp, got) 73 | } 74 | }) 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /policies/customresourcedefinition/crd_protect.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Cruise LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // https://www.apache.org/licenses/LICENSE-2.0 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | package customresourcedefinition 14 | 15 | import ( 16 | "context" 17 | 18 | admissionv1 "k8s.io/api/admission/v1" 19 | apiextensionv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" 20 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 21 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 22 | "k8s.io/apimachinery/pkg/runtime" 23 | "k8s.io/apimachinery/pkg/runtime/schema" 24 | "k8s.io/apimachinery/pkg/runtime/serializer" 25 | "k8s.io/client-go/dynamic" 26 | "k8s.io/client-go/rest" 27 | 28 | "github.com/cruise-automation/k-rail/v3/policies" 29 | ) 30 | 31 | // NewPolicyCRDProtect new 32 | func NewPolicyCRDProtect() (PolicyCRDProtect, error) { 33 | p := PolicyCRDProtect{} 34 | 35 | restConfig, err := rest.InClusterConfig() 36 | if err != nil { 37 | return p, err 38 | } 39 | config := dynamic.ConfigFor(restConfig) 40 | dynamicClient, err := dynamic.NewForConfig(config) 41 | if err != nil { 42 | return p, err 43 | } 44 | p.client = dynamicClient 45 | 46 | return p, nil 47 | } 48 | 49 | // PolicyCRDProtect type 50 | type PolicyCRDProtect struct { 51 | client dynamic.Interface 52 | } 53 | 54 | // Name name 55 | func (p PolicyCRDProtect) Name() string { 56 | return "crd_protect" 57 | } 58 | 59 | // Validate CRD Resources 60 | func (p PolicyCRDProtect) Validate(ctx context.Context, config policies.Config, ar *admissionv1.AdmissionRequest) ([]policies.ResourceViolation, []policies.PatchOperation) { 61 | 62 | resourceViolations := []policies.ResourceViolation{} 63 | 64 | // check if the target resource is a CRD 65 | if ar.Kind.Kind != "CustomResourceDefinition" && ar.Kind.Group != "apiextensions.k8s.io" { 66 | return resourceViolations, nil 67 | } 68 | 69 | // check if this is a delete operation 70 | if ar.Operation != "DELETE" { 71 | return resourceViolations, nil 72 | } 73 | 74 | // check if protect annotation is set on the CRD 75 | crd := &apiextensionv1beta1.CustomResourceDefinition{} 76 | 77 | scheme := runtime.NewScheme() 78 | codecFactory := serializer.NewCodecFactory(scheme) 79 | deserializer := codecFactory.UniversalDeserializer() 80 | if _, _, err := deserializer.Decode(ar.OldObject.Raw, nil, crd); err != nil { 81 | return resourceViolations, nil 82 | } 83 | 84 | if v, ok := crd.ObjectMeta.Annotations["k-rail.crd.protect"]; ok { 85 | if v != "enabled" { 86 | return resourceViolations, nil 87 | } 88 | } else { 89 | return resourceViolations, nil 90 | } 91 | 92 | // check if any CRs exist for CRD 93 | for _, v := range crd.Spec.Versions { 94 | var cr *unstructured.UnstructuredList 95 | var err error 96 | crgvr := schema.GroupVersionResource{ 97 | Group: crd.Spec.Group, 98 | Version: v.Name, 99 | Resource: crd.Spec.Names.Plural, 100 | } 101 | 102 | if crd.Spec.Scope == "Cluster" { 103 | cr, err = p.client.Resource(crgvr).List(ctx, metav1.ListOptions{}) 104 | } else { 105 | cr, err = p.client.Resource(crgvr).Namespace("").List(ctx, metav1.ListOptions{}) 106 | } 107 | if err != nil { 108 | return resourceViolations, nil 109 | } 110 | if len(cr.Items) > 0 { 111 | resourceViolations = append( 112 | resourceViolations, policies.ResourceViolation{ 113 | Namespace: ar.Namespace, 114 | ResourceName: crd.Name, 115 | ResourceKind: crd.Spec.Names.Kind, 116 | Violation: "Can not delete custom resource definition (CRD) while custom resources (CRs) exist", 117 | Policy: p.Name(), 118 | }, 119 | ) 120 | return resourceViolations, nil 121 | } 122 | } 123 | 124 | return resourceViolations, nil 125 | } 126 | -------------------------------------------------------------------------------- /policies/exemption.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Cruise LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // https://www.apache.org/licenses/LICENSE-2.0 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | package policies 14 | 15 | import ( 16 | "io/ioutil" 17 | "path/filepath" 18 | "strings" 19 | 20 | "github.com/gobwas/glob" 21 | log "github.com/sirupsen/logrus" 22 | authenticationv1 "k8s.io/api/authentication/v1" 23 | "sigs.k8s.io/yaml" 24 | ) 25 | 26 | // RawExemption is the configuration for a policy exemption 27 | type RawExemption struct { 28 | ClusterName string `json:"cluster_name"` 29 | ResourceName string `json:"resource_name"` 30 | Namespace string `json:"namespace"` 31 | Username string `json:"username"` 32 | Group string `json:"group"` 33 | ExemptPolicies []string `json:"exempt_policies"` 34 | } 35 | 36 | // CompiledExemption is the compiled configuration for a policy exemption 37 | type CompiledExemption struct { 38 | ClusterName glob.Glob 39 | ResourceName glob.Glob 40 | Namespace glob.Glob 41 | Username glob.Glob 42 | Group glob.Glob 43 | ExemptPolicies []glob.Glob 44 | } 45 | 46 | // Compile returns a CompiledExemption 47 | func (r *RawExemption) Compile() CompiledExemption { 48 | // if not specified, assume it's the field matches all 49 | 50 | // ensure that ResourceName has a trailing glob so it can match the IDs added by certain resource types 51 | // ie, Deployment pod name test-pod, ReplicaSet name test-pod-sdf932, PodName test-pod-sdf932-ew92 52 | if !strings.HasSuffix(r.ResourceName, "*") { 53 | r.ResourceName = r.ResourceName + "*" 54 | } 55 | 56 | if r.ClusterName == "" { 57 | r.ClusterName = "*" 58 | } 59 | if r.Namespace == "" { 60 | r.Namespace = "*" 61 | } 62 | if r.Username == "" { 63 | r.Username = "*" 64 | } 65 | if r.Group == "" { 66 | r.Group = "*" 67 | } 68 | if len(r.ExemptPolicies) == 0 { 69 | r.ExemptPolicies = []string{"*"} 70 | } 71 | 72 | // compile the RawExemption 73 | var policies []glob.Glob 74 | for _, p := range r.ExemptPolicies { 75 | policies = append(policies, glob.MustCompile(p)) 76 | } 77 | return CompiledExemption{ 78 | ClusterName: glob.MustCompile(r.ClusterName), 79 | ResourceName: glob.MustCompile(r.ResourceName), 80 | Namespace: glob.MustCompile(r.Namespace), 81 | Username: glob.MustCompile(r.Username), 82 | Group: glob.MustCompile(r.Group), 83 | ExemptPolicies: policies, 84 | } 85 | } 86 | 87 | // ExemptionsFromYAML returns compiled exemptions from YAML input 88 | func ExemptionsFromYAML(exemptions []byte) ([]CompiledExemption, error) { 89 | var rawExemptions []RawExemption 90 | err := yaml.Unmarshal(exemptions, &rawExemptions) 91 | if err != nil { 92 | return []CompiledExemption{}, err 93 | } 94 | var c []CompiledExemption 95 | for _, e := range rawExemptions { 96 | log.WithFields(log.Fields{"exemption": e}).Info("loaded exemption") 97 | c = append(c, e.Compile()) 98 | } 99 | return c, nil 100 | } 101 | 102 | // ExemptionsFromDirectory returns compiled exemptions a given directory 103 | func ExemptionsFromDirectory(directory string) ([]CompiledExemption, error) { 104 | files, err := filepath.Glob(directory) 105 | if err != nil { 106 | return []CompiledExemption{}, err 107 | } 108 | 109 | var c []CompiledExemption 110 | for _, f := range files { 111 | contents, err := ioutil.ReadFile(f) 112 | if err != nil { 113 | return []CompiledExemption{}, err 114 | } 115 | e, err := ExemptionsFromYAML(contents) 116 | if err != nil { 117 | return []CompiledExemption{}, err 118 | } 119 | c = append(c, e...) 120 | } 121 | return c, nil 122 | } 123 | 124 | // IsExempt returns whether a resource is exempt from a given policy 125 | func IsExempt(clusterName, resourceName, namespace string, userInfo authenticationv1.UserInfo, policyName string, exemptions []CompiledExemption) bool { 126 | for _, e := range exemptions { 127 | if e.ClusterName.Match(clusterName) && 128 | e.Namespace.Match(namespace) && 129 | e.ResourceName.Match(resourceName) && 130 | e.Username.Match(userInfo.Username) { 131 | for _, p := range e.ExemptPolicies { 132 | if p.Match(policyName) { 133 | for _, g := range userInfo.Groups { 134 | if e.Group.Match(g) { 135 | return true 136 | } 137 | } 138 | } 139 | } 140 | } 141 | } 142 | 143 | return false 144 | } 145 | -------------------------------------------------------------------------------- /policies/exemption_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Cruise LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // https://www.apache.org/licenses/LICENSE-2.0 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | package policies 14 | 15 | import ( 16 | "testing" 17 | 18 | authenticationv1 "k8s.io/api/authentication/v1" 19 | ) 20 | 21 | func TestIsExempt(t *testing.T) { 22 | type args struct { 23 | resourceName string 24 | namespace string 25 | userInfo authenticationv1.UserInfo 26 | policyName string 27 | exemption RawExemption 28 | } 29 | tests := []struct { 30 | name string 31 | args args 32 | want bool 33 | }{ 34 | { 35 | name: "exact match", 36 | want: true, 37 | args: args{ 38 | resourceName: "test-pod", 39 | namespace: "test-namespace", 40 | userInfo: authenticationv1.UserInfo{ 41 | Username: "test-user", 42 | Groups: []string{"group-a", "group-b"}, 43 | }, 44 | policyName: "test-policy", 45 | exemption: RawExemption{ 46 | ResourceName: "test-pod", 47 | Namespace: "test-namespace", 48 | Username: "test-user", 49 | Group: "group-a", 50 | ExemptPolicies: []string{"test-policy"}, 51 | }, 52 | }, 53 | }, 54 | { 55 | name: "fail policy", 56 | want: false, 57 | args: args{ 58 | resourceName: "test-pod", 59 | namespace: "test-namespace", 60 | userInfo: authenticationv1.UserInfo{ 61 | Username: "test-user", 62 | Groups: []string{"group-a", "group-b"}, 63 | }, 64 | policyName: "test-policy", 65 | exemption: RawExemption{ 66 | ResourceName: "test-pod", 67 | Namespace: "test-namespace", 68 | Username: "test-user", 69 | Group: "group-a", 70 | ExemptPolicies: []string{"different-policy"}, 71 | }, 72 | }, 73 | }, 74 | { 75 | name: "all globs", 76 | want: true, 77 | args: args{ 78 | resourceName: "test-pod", 79 | namespace: "test-namespace", 80 | userInfo: authenticationv1.UserInfo{ 81 | Username: "test-user", 82 | Groups: []string{"group-a", "group-b"}, 83 | }, 84 | policyName: "test-policy", 85 | exemption: RawExemption{ 86 | ResourceName: "test-*", 87 | Namespace: "test-*", 88 | Username: "test-*", 89 | Group: "group-*", 90 | ExemptPolicies: []string{"test-*"}, 91 | }, 92 | }, 93 | }, 94 | { 95 | name: "empty field is assumed passing during validation", 96 | want: true, 97 | args: args{ 98 | resourceName: "test-pod", 99 | namespace: "test-namespace", 100 | userInfo: authenticationv1.UserInfo{ 101 | Username: "test-user", 102 | Groups: []string{"group-a", "group-b"}, 103 | }, 104 | policyName: "test-policy", 105 | exemption: RawExemption{}, 106 | }, 107 | }, 108 | { 109 | name: "fail deepest condition", 110 | want: false, 111 | args: args{ 112 | resourceName: "test-pod", 113 | namespace: "test-namespace", 114 | userInfo: authenticationv1.UserInfo{ 115 | Username: "test-user", 116 | Groups: []string{"group-a", "group-b"}, 117 | }, 118 | policyName: "test-policy", 119 | exemption: RawExemption{ 120 | Group: "fail-group", 121 | }, 122 | }, 123 | }, 124 | { 125 | name: "implied resource glob", 126 | want: true, 127 | args: args{ 128 | resourceName: "test-pod-dsfd32-sduf8ds", 129 | namespace: "test-namespace", 130 | userInfo: authenticationv1.UserInfo{ 131 | Username: "test-user", 132 | Groups: []string{"group-a", "group-b"}, 133 | }, 134 | policyName: "test-policy", 135 | exemption: RawExemption{ 136 | ResourceName: "test-pod", 137 | }, 138 | }, 139 | }, 140 | } 141 | for _, tt := range tests { 142 | t.Run(tt.name, func(t *testing.T) { 143 | if got := IsExempt( 144 | "fake-cluster", 145 | tt.args.resourceName, 146 | tt.args.namespace, 147 | tt.args.userInfo, 148 | tt.args.policyName, 149 | []CompiledExemption{tt.args.exemption.Compile()}, 150 | ); got != tt.want { 151 | t.Errorf("IsExempt() = %v, want %v", got, tt.want) 152 | } 153 | }) 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /policies/ingress/disallow_nginx_snippet.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Cruise LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // https://www.apache.org/licenses/LICENSE-2.0 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | package ingress 14 | 15 | import ( 16 | "context" 17 | "fmt" 18 | "regexp" 19 | 20 | admissionv1 "k8s.io/api/admission/v1" 21 | 22 | "github.com/cruise-automation/k-rail/v3/policies" 23 | "github.com/cruise-automation/k-rail/v3/resource" 24 | ) 25 | 26 | var nginxSnippetAnnotationRegex = regexp.MustCompile("^nginx.ingress.kubernetes.io/.*-snippet$") 27 | 28 | type PolicyDisallowNGINXSnippet struct{} 29 | 30 | func (p PolicyDisallowNGINXSnippet) Name() string { 31 | return "ingress_disallow_nginx_snippet" 32 | } 33 | 34 | func (p PolicyDisallowNGINXSnippet) Validate(ctx context.Context, config policies.Config, ar *admissionv1.AdmissionRequest) ([]policies.ResourceViolation, []policies.PatchOperation) { 35 | 36 | resourceViolations := []policies.ResourceViolation{} 37 | 38 | ingressResource := resource.GetIngressResource(ctx, ar) 39 | if ingressResource == nil { 40 | return resourceViolations, nil 41 | } 42 | 43 | for key := range ingressResource.GetAnnotations() { 44 | if nginxSnippetAnnotationRegex.MatchString(key) { 45 | resourceViolations = append(resourceViolations, policies.ResourceViolation{ 46 | Namespace: ar.Namespace, 47 | ResourceName: ingressResource.ResourceName, 48 | ResourceKind: ingressResource.ResourceKind, 49 | Violation: fmt.Sprintf("NGINX Snippets are not allowed, found %q", key), 50 | Policy: p.Name(), 51 | }) 52 | } 53 | } 54 | return resourceViolations, nil 55 | } 56 | -------------------------------------------------------------------------------- /policies/ingress/disallow_nginx_snippet_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Cruise LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // https://www.apache.org/licenses/LICENSE-2.0 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | package ingress 14 | 15 | import ( 16 | "context" 17 | "encoding/json" 18 | "reflect" 19 | "testing" 20 | 21 | admissionv1 "k8s.io/api/admission/v1" 22 | extensionsv1beta1 "k8s.io/api/extensions/v1beta1" 23 | networkingv1 "k8s.io/api/networking/v1" 24 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 25 | "k8s.io/apimachinery/pkg/runtime" 26 | "k8s.io/apimachinery/pkg/runtime/schema" 27 | 28 | "github.com/cruise-automation/k-rail/v3/policies" 29 | ) 30 | 31 | func TestPolicyDisallowNGINXSnippet(t *testing.T) { 32 | ctx := context.Background() 33 | 34 | tests := []struct { 35 | name string 36 | ingress interface { 37 | GetObjectKind() schema.ObjectKind 38 | } 39 | violations int 40 | }{ 41 | { 42 | name: "deny 1", 43 | violations: 1, 44 | ingress: &extensionsv1beta1.Ingress{ 45 | TypeMeta: metav1.TypeMeta{ 46 | APIVersion: "extensions/v1beta1", 47 | }, 48 | ObjectMeta: metav1.ObjectMeta{ 49 | Annotations: map[string]string{ 50 | "nginx.ingress.kubernetes.io/server-snippet": "i'm malicious", 51 | }, 52 | }, 53 | }, 54 | }, 55 | { 56 | name: "deny 2", 57 | violations: 2, 58 | ingress: &extensionsv1beta1.Ingress{ 59 | TypeMeta: metav1.TypeMeta{ 60 | APIVersion: "networking.k8s.io/v1beta1", 61 | }, 62 | ObjectMeta: metav1.ObjectMeta{ 63 | Annotations: map[string]string{ 64 | "nginx.ingress.kubernetes.io/server-snippet": "i'm malicious", 65 | "nginx.ingress.kubernetes.io/auth-snippet": "me too", 66 | }, 67 | }, 68 | }, 69 | }, 70 | { 71 | name: "deny 3", 72 | violations: 2, 73 | ingress: &networkingv1.Ingress{ 74 | TypeMeta: metav1.TypeMeta{ 75 | APIVersion: "networking.k8s.io/v1", 76 | }, 77 | ObjectMeta: metav1.ObjectMeta{ 78 | Annotations: map[string]string{ 79 | "nginx.ingress.kubernetes.io/server-snippet": "i'm malicious", 80 | "nginx.ingress.kubernetes.io/auth-snippet": "me too", 81 | }, 82 | }, 83 | }, 84 | }, 85 | { 86 | name: "allow", 87 | violations: 0, 88 | ingress: &extensionsv1beta1.Ingress{ 89 | ObjectMeta: metav1.ObjectMeta{ 90 | Annotations: map[string]string{ 91 | "foo": "bar", 92 | }, 93 | }, 94 | }, 95 | }, 96 | } 97 | for _, tt := range tests { 98 | t.Run(tt.name, func(t *testing.T) { 99 | raw, _ := json.Marshal(tt.ingress) 100 | ar := &admissionv1.AdmissionRequest{ 101 | Namespace: "namespace", 102 | Name: "name", 103 | Object: runtime.RawExtension{Raw: raw}, 104 | Resource: metav1.GroupVersionResource{ 105 | Group: tt.ingress.GetObjectKind().GroupVersionKind().Group, 106 | Version: tt.ingress.GetObjectKind().GroupVersionKind().Version, 107 | Resource: "ingresses", 108 | }, 109 | } 110 | 111 | v := PolicyDisallowNGINXSnippet{} 112 | if got, _ := v.Validate(ctx, policies.Config{}, ar); !reflect.DeepEqual(len(got), tt.violations) { 113 | t.Errorf("PolicyDisallowNGINXSnippet() %s got %v want %v violations", tt.name, len(got), tt.violations) 114 | } 115 | }) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /policies/ingress/require_ingress_exemption.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Cruise LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // https://www.apache.org/licenses/LICENSE-2.0 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | package ingress 14 | 15 | import ( 16 | "context" 17 | 18 | "github.com/cruise-automation/k-rail/v3/policies" 19 | "github.com/cruise-automation/k-rail/v3/resource" 20 | admissionv1 "k8s.io/api/admission/v1" 21 | ) 22 | 23 | type PolicyRequireIngressExemption struct{} 24 | 25 | func (p PolicyRequireIngressExemption) Name() string { 26 | return "ingress_require_ingress_exemption" 27 | } 28 | 29 | func (p PolicyRequireIngressExemption) Validate(ctx context.Context, config policies.Config, ar *admissionv1.AdmissionRequest) ([]policies.ResourceViolation, []policies.PatchOperation) { 30 | 31 | resourceViolations := []policies.ResourceViolation{} 32 | 33 | ingressResource := resource.GetIngressResource(ctx, ar) 34 | if ingressResource == nil { 35 | return resourceViolations, nil 36 | } 37 | 38 | violationText := "Require Ingress Exemption: Using certain Ingress classes requires an exemption" 39 | 40 | for _, ingressClass := range config.PolicyRequireIngressExemptionClasses { 41 | for annotation, value := range ingressResource.GetAnnotations() { 42 | if annotation == "kubernetes.io/ingress.class" { 43 | if value == ingressClass { 44 | resourceViolations = append(resourceViolations, policies.ResourceViolation{ 45 | Namespace: ar.Namespace, 46 | ResourceName: ingressResource.ResourceName, 47 | ResourceKind: ingressResource.ResourceKind, 48 | Violation: violationText, 49 | Policy: p.Name(), 50 | }) 51 | } 52 | } 53 | } 54 | } 55 | return resourceViolations, nil 56 | } 57 | -------------------------------------------------------------------------------- /policies/ingress/require_ingress_exemption_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Cruise LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // https://www.apache.org/licenses/LICENSE-2.0 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | package ingress 14 | 15 | import ( 16 | "context" 17 | "encoding/json" 18 | "reflect" 19 | "testing" 20 | 21 | "github.com/cruise-automation/k-rail/v3/policies" 22 | admissionv1 "k8s.io/api/admission/v1" 23 | extensionsv1beta1 "k8s.io/api/extensions/v1beta1" 24 | networkingv1beta1 "k8s.io/api/networking/v1beta1" 25 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 26 | "k8s.io/apimachinery/pkg/runtime" 27 | ) 28 | 29 | func TestPolicyRequireIngressExemption(t *testing.T) { 30 | ctx := context.Background() 31 | 32 | tests := []struct { 33 | name string 34 | ingressExt *extensionsv1beta1.Ingress 35 | ingressNet *networkingv1beta1.Ingress 36 | violations int 37 | }{ 38 | { 39 | name: "deny ext ingress", 40 | violations: 1, 41 | ingressExt: &extensionsv1beta1.Ingress{ 42 | ObjectMeta: metav1.ObjectMeta{ 43 | Annotations: map[string]string{ 44 | "kubernetes.io/ingress.class": "public", 45 | }, 46 | }, 47 | }, 48 | }, 49 | { 50 | name: "deny net ingress", 51 | violations: 1, 52 | ingressNet: &networkingv1beta1.Ingress{ 53 | ObjectMeta: metav1.ObjectMeta{ 54 | Annotations: map[string]string{ 55 | "kubernetes.io/ingress.class": "public", 56 | }, 57 | }, 58 | }, 59 | }, 60 | { 61 | name: "allow ext ingress", 62 | violations: 0, 63 | ingressExt: &extensionsv1beta1.Ingress{ 64 | ObjectMeta: metav1.ObjectMeta{ 65 | Annotations: map[string]string{ 66 | "kubernetes.io/ingress.class": "private", 67 | }, 68 | }, 69 | }, 70 | }, 71 | { 72 | name: "allow net ingress", 73 | violations: 0, 74 | ingressNet: &networkingv1beta1.Ingress{ 75 | ObjectMeta: metav1.ObjectMeta{ 76 | Annotations: map[string]string{ 77 | "kubernetes.io/ingress.class": "private", 78 | }, 79 | }, 80 | }, 81 | }, 82 | } 83 | for _, tt := range tests { 84 | t.Run(tt.name, func(t *testing.T) { 85 | var ar = &admissionv1.AdmissionRequest{} 86 | 87 | if tt.ingressExt != nil { 88 | raw, _ := json.Marshal(tt.ingressExt) 89 | ar = &admissionv1.AdmissionRequest{ 90 | Namespace: "namespace", 91 | Name: "name", 92 | Object: runtime.RawExtension{Raw: raw}, 93 | Resource: metav1.GroupVersionResource{Group: "extensions", Version: "v1beta1", Resource: "ingresses"}, 94 | } 95 | } 96 | 97 | if tt.ingressNet != nil { 98 | raw, _ := json.Marshal(tt.ingressNet) 99 | ar = &admissionv1.AdmissionRequest{ 100 | Namespace: "namespace", 101 | Name: "name", 102 | Object: runtime.RawExtension{Raw: raw}, 103 | Resource: metav1.GroupVersionResource{Group: "networking.k8s.io", Version: "v1beta1", Resource: "ingresses"}, 104 | } 105 | } 106 | 107 | v := PolicyRequireIngressExemption{} 108 | if got, _ := v.Validate(ctx, policies.Config{PolicyRequireIngressExemptionClasses: []string{"public"}}, ar); !reflect.DeepEqual(len(got), tt.violations) { 109 | t.Errorf("PolicyRequireIngressExemption() %s got %v want %v violations", tt.name, len(got), tt.violations) 110 | } 111 | }) 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /policies/ingress/unique_ingress_host.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Cruise LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // https://www.apache.org/licenses/LICENSE-2.0 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | package ingress 14 | 15 | import ( 16 | "context" 17 | 18 | "github.com/cruise-automation/k-rail/v3/policies" 19 | "github.com/cruise-automation/k-rail/v3/resource" 20 | log "github.com/sirupsen/logrus" 21 | admissionv1 "k8s.io/api/admission/v1" 22 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 23 | "k8s.io/client-go/kubernetes" 24 | "k8s.io/client-go/rest" 25 | ) 26 | 27 | func NewPolicyRequireUniqueHost() (PolicyRequireUniqueHost, error) { 28 | p := PolicyRequireUniqueHost{} 29 | 30 | config, err := rest.InClusterConfig() 31 | if err != nil { 32 | return p, err 33 | } 34 | 35 | clientset, err := kubernetes.NewForConfig(config) 36 | if err != nil { 37 | return p, err 38 | } 39 | p.client = clientset 40 | return p, err 41 | } 42 | 43 | type PolicyRequireUniqueHost struct { 44 | client *kubernetes.Clientset 45 | } 46 | 47 | func (p PolicyRequireUniqueHost) Name() string { 48 | return "ingress_unique_ingress_host" 49 | } 50 | 51 | func (p PolicyRequireUniqueHost) CheckIngressNamespaces(ctx context.Context, host string) (map[string]struct{}, error) { 52 | ingressNamespacesMap := make(map[string]struct{}) 53 | ingresses, err := p.client.ExtensionsV1beta1().Ingresses("").List(ctx, metav1.ListOptions{}) 54 | if err != nil { 55 | return ingressNamespacesMap, err 56 | } 57 | for _, ingress := range ingresses.Items { 58 | rules := ingress.Spec.Rules 59 | for _, rule := range rules { 60 | if rule.Host == host { 61 | ingressNamespace := ingress.ObjectMeta.Namespace 62 | ingressNamespacesMap[ingressNamespace] = struct{}{} 63 | } 64 | } 65 | } 66 | return ingressNamespacesMap, nil 67 | } 68 | 69 | func Find(slice []string, val string) bool { 70 | for _, item := range slice { 71 | if item == val { 72 | return true 73 | } 74 | } 75 | return false 76 | } 77 | 78 | func (p PolicyRequireUniqueHost) Validate(ctx context.Context, config policies.Config, ar *admissionv1.AdmissionRequest) ([]policies.ResourceViolation, []policies.PatchOperation) { 79 | 80 | resourceViolations := []policies.ResourceViolation{} 81 | 82 | ingressResource := resource.GetIngressResource(ctx, ar) 83 | if ingressResource == nil { 84 | return resourceViolations, nil 85 | } 86 | 87 | violationText := "Requires Unique Ingress Host: Ingress Host should not point to multiple namespaces. Host already in:" 88 | 89 | for _, host := range ingressResource.GetHosts() { 90 | ingressNamespacesMap, err := p.CheckIngressNamespaces(ctx, host) 91 | if err != nil { 92 | log.Error(err) 93 | return nil, nil 94 | } 95 | foundNamespace := false 96 | _, ok := ingressNamespacesMap[ar.Namespace] 97 | if ok { 98 | foundNamespace = true 99 | } 100 | if (len(ingressNamespacesMap) == 0) || (len(ingressNamespacesMap) == 1 && foundNamespace) { 101 | return resourceViolations, nil 102 | } else { 103 | namespacesStr := "" 104 | for k := range ingressNamespacesMap { 105 | namespacesStr = namespacesStr + " " + k 106 | } 107 | resourceViolations = append(resourceViolations, policies.ResourceViolation{ 108 | Namespace: ar.Namespace, 109 | ResourceName: ingressResource.ResourceName, 110 | ResourceKind: ingressResource.ResourceKind, 111 | Violation: violationText + ": " + namespacesStr, 112 | Policy: p.Name(), 113 | }) 114 | } 115 | } 116 | return resourceViolations, nil 117 | } 118 | -------------------------------------------------------------------------------- /policies/patch.go: -------------------------------------------------------------------------------- 1 | package policies 2 | 3 | // PatchOperation is used for specifying mutating patches on resources. 4 | // It follows the JSONPatch format (http://jsonpatch.com/) 5 | // This is the format that MutatingWebhookConfigurations require. 6 | type PatchOperation struct { 7 | Op string `json:"op"` 8 | Path string `json:"path"` 9 | Value interface{} `json:"value,omitempty"` 10 | } 11 | -------------------------------------------------------------------------------- /policies/persistentvolume/no_persistent_volume_host.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Cruise LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // https://www.apache.org/licenses/LICENSE-2.0 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | package persistentvolume 14 | 15 | import ( 16 | "context" 17 | 18 | admissionv1 "k8s.io/api/admission/v1" 19 | 20 | "github.com/cruise-automation/k-rail/v3/policies" 21 | "github.com/cruise-automation/k-rail/v3/resource" 22 | ) 23 | 24 | type PolicyNoPersistentVolumeHost struct{} 25 | 26 | func (p PolicyNoPersistentVolumeHost) Name() string { 27 | return "persistent_volume_no_host_path" 28 | } 29 | 30 | func (p PolicyNoPersistentVolumeHost) Validate(ctx context.Context, config policies.Config, ar *admissionv1.AdmissionRequest) ([]policies.ResourceViolation, []policies.PatchOperation) { 31 | 32 | resourceViolations := []policies.ResourceViolation{} 33 | 34 | pvResource := resource.GetPersistentVolumeResource(ctx, ar) 35 | if pvResource == nil { 36 | return resourceViolations, nil 37 | } 38 | 39 | violationText := "No Persistent Volume Host Path: Using the host path is forbidden" 40 | 41 | if pvResource.PersistentVolume.Spec.PersistentVolumeSource.HostPath != nil { 42 | resourceViolations = append(resourceViolations, policies.ResourceViolation{ 43 | Namespace: ar.Namespace, 44 | ResourceName: pvResource.ResourceName, 45 | ResourceKind: pvResource.ResourceKind, 46 | Violation: violationText, 47 | Policy: p.Name(), 48 | }) 49 | } 50 | 51 | return resourceViolations, nil 52 | } 53 | -------------------------------------------------------------------------------- /policies/pod/bind_mounts.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Cruise LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // https://www.apache.org/licenses/LICENSE-2.0 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | package pod 14 | 15 | import ( 16 | "context" 17 | 18 | "github.com/cruise-automation/k-rail/v3/policies" 19 | "github.com/cruise-automation/k-rail/v3/resource" 20 | admissionv1 "k8s.io/api/admission/v1" 21 | ) 22 | 23 | type PolicyBindMounts struct{} 24 | 25 | func (p PolicyBindMounts) Name() string { 26 | return "pod_no_bind_mounts" 27 | } 28 | 29 | func (p PolicyBindMounts) Validate(ctx context.Context, config policies.Config, ar *admissionv1.AdmissionRequest) ([]policies.ResourceViolation, []policies.PatchOperation) { 30 | 31 | resourceViolations := []policies.ResourceViolation{} 32 | 33 | podResource := resource.GetPodResource(ctx, ar) 34 | if podResource == nil { 35 | return resourceViolations, nil 36 | } 37 | 38 | violationText := "Host Bind Mounts: host bind mounts are forbidden" 39 | 40 | for _, volume := range podResource.PodSpec.Volumes { 41 | if volume.HostPath != nil { 42 | resourceViolations = append(resourceViolations, policies.ResourceViolation{ 43 | Namespace: ar.Namespace, 44 | ResourceName: podResource.ResourceName, 45 | ResourceKind: podResource.ResourceKind, 46 | Violation: violationText, 47 | Policy: p.Name(), 48 | }) 49 | } 50 | } 51 | 52 | return resourceViolations, nil 53 | } 54 | -------------------------------------------------------------------------------- /policies/pod/bind_mounts_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Cruise LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // https://www.apache.org/licenses/LICENSE-2.0 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | package pod 14 | 15 | import ( 16 | "context" 17 | "encoding/json" 18 | "reflect" 19 | "testing" 20 | 21 | "github.com/cruise-automation/k-rail/v3/policies" 22 | admissionv1 "k8s.io/api/admission/v1" 23 | corev1 "k8s.io/api/core/v1" 24 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 25 | "k8s.io/apimachinery/pkg/runtime" 26 | ) 27 | 28 | func TestPolicyBindMounts(t *testing.T) { 29 | ctx := context.Background() 30 | 31 | tests := []struct { 32 | name string 33 | podSpec corev1.PodSpec 34 | violations int 35 | }{ 36 | { 37 | name: "deny", 38 | violations: 1, 39 | podSpec: corev1.PodSpec{ 40 | Volumes: []corev1.Volume{ 41 | { 42 | VolumeSource: corev1.VolumeSource{ 43 | HostPath: &corev1.HostPathVolumeSource{ 44 | Path: "/host-path", 45 | }, 46 | }, 47 | }, 48 | }, 49 | }, 50 | }, 51 | { 52 | name: "allow", 53 | violations: 0, 54 | podSpec: corev1.PodSpec{ 55 | Volumes: []corev1.Volume{ 56 | { 57 | VolumeSource: corev1.VolumeSource{ 58 | Secret: &corev1.SecretVolumeSource{ 59 | SecretName: "super secret", 60 | }, 61 | }, 62 | }, 63 | }, 64 | }, 65 | }, 66 | } 67 | for _, tt := range tests { 68 | t.Run(tt.name, func(t *testing.T) { 69 | 70 | raw, _ := json.Marshal(corev1.Pod{Spec: tt.podSpec}) 71 | ar := &admissionv1.AdmissionRequest{ 72 | Namespace: "namespace", 73 | Name: "name", 74 | Object: runtime.RawExtension{Raw: raw}, 75 | Resource: metav1.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"}, 76 | } 77 | 78 | v := PolicyBindMounts{} 79 | 80 | if got, _ := v.Validate(ctx, policies.Config{}, ar); !reflect.DeepEqual(len(got), tt.violations) { 81 | t.Errorf("PolicyBindMounts() = %v, got %v want %v violations", tt.name, len(got), tt.violations) 82 | } 83 | }) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /policies/pod/deny_unconfined_apparmor.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Cruise LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // https://www.apache.org/licenses/LICENSE-2.0 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | package pod 14 | 15 | import ( 16 | "context" 17 | "strings" 18 | 19 | "github.com/cruise-automation/k-rail/v3/policies" 20 | "github.com/cruise-automation/k-rail/v3/resource" 21 | admissionv1 "k8s.io/api/admission/v1" 22 | ) 23 | 24 | type PolicyDenyUnconfinedApparmorPolicy struct{} 25 | 26 | func (p PolicyDenyUnconfinedApparmorPolicy) Name() string { 27 | return "pod_deny_unconfined_apparmor_policy" 28 | } 29 | 30 | func (p PolicyDenyUnconfinedApparmorPolicy) Validate(ctx context.Context, config policies.Config, ar *admissionv1.AdmissionRequest) ([]policies.ResourceViolation, []policies.PatchOperation) { 31 | resourceViolations := []policies.ResourceViolation{} 32 | 33 | podResource := resource.GetPodResource(ctx, ar) 34 | if podResource == nil { 35 | return nil, nil 36 | } 37 | 38 | violationText := "Unconfined AppArmor pod: Setting AppArmor annonation on Pod to unconfined is forbidden without an exception" 39 | 40 | if podResource.ResourceKind == "Pod" { 41 | for name, value := range podResource.PodAnnotations { 42 | if strings.HasPrefix(name, "container.apparmor.security.beta.kubernetes.io") { 43 | if value == "unconfined" { 44 | resourceViolations = append(resourceViolations, policies.ResourceViolation{ 45 | Namespace: ar.Namespace, 46 | ResourceName: podResource.ResourceName, 47 | ResourceKind: podResource.ResourceKind, 48 | Violation: violationText, 49 | Policy: p.Name(), 50 | Error: nil, 51 | }) 52 | } 53 | } 54 | } 55 | } 56 | 57 | return resourceViolations, nil 58 | } 59 | -------------------------------------------------------------------------------- /policies/pod/deny_unconfined_apparmor_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Cruise LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // https://www.apache.org/licenses/LICENSE-2.0 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | package pod 14 | 15 | import ( 16 | "context" 17 | "encoding/json" 18 | "testing" 19 | 20 | "github.com/cruise-automation/k-rail/v3/policies" 21 | admissionv1 "k8s.io/api/admission/v1" 22 | corev1 "k8s.io/api/core/v1" 23 | v1 "k8s.io/api/core/v1" 24 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 25 | "k8s.io/apimachinery/pkg/runtime" 26 | ) 27 | 28 | func TestPolicyDenyUnconfinedApparmorPolicy(t *testing.T) { 29 | type args struct { 30 | ctx context.Context 31 | config policies.Config 32 | ar *admissionv1.AdmissionRequest 33 | } 34 | tests := []struct { 35 | name string 36 | podSpec v1.PodSpec 37 | annotations map[string]string 38 | violations int 39 | }{ 40 | { 41 | name: "violation", 42 | podSpec: v1.PodSpec{}, 43 | annotations: map[string]string{ 44 | "container.apparmor.security.beta.kubernetes.io/app": "unconfined", 45 | }, 46 | violations: 1, 47 | }, 48 | { 49 | name: "no violation", 50 | podSpec: v1.PodSpec{}, 51 | annotations: map[string]string{}, 52 | violations: 0, 53 | }, 54 | { 55 | name: "no violation, using other than unconfined", 56 | podSpec: v1.PodSpec{}, 57 | annotations: map[string]string{ 58 | "container.apparmor.security.beta.kubernetes.io/app": "runtime/default", 59 | }, 60 | violations: 0, 61 | }, 62 | } 63 | for _, tt := range tests { 64 | t.Run(tt.name, func(t *testing.T) { 65 | p := PolicyDenyUnconfinedApparmorPolicy{} 66 | raw, _ := json.Marshal(corev1.Pod{Spec: tt.podSpec, ObjectMeta: metav1.ObjectMeta{Annotations: tt.annotations}}) 67 | ar := &admissionv1.AdmissionRequest{ 68 | Namespace: "namespace", 69 | Name: "name", 70 | Object: runtime.RawExtension{Raw: raw}, 71 | Resource: metav1.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"}, 72 | } 73 | got, _ := p.Validate(context.Background(), policies.Config{}, ar) 74 | if len(got) != tt.violations { 75 | t.Errorf("PolicyDenyUnconfinedApparmorPolicy.Validate() got = %v, want %v", len(got), tt.violations) 76 | } 77 | }) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /policies/pod/docker_sock.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Cruise LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // https://www.apache.org/licenses/LICENSE-2.0 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | package pod 14 | 15 | import ( 16 | "context" 17 | 18 | "github.com/cruise-automation/k-rail/v3/policies" 19 | "github.com/cruise-automation/k-rail/v3/resource" 20 | admissionv1 "k8s.io/api/admission/v1" 21 | ) 22 | 23 | type PolicyDockerSock struct{} 24 | 25 | func (p PolicyDockerSock) Name() string { 26 | return "pod_no_docker_sock" 27 | } 28 | 29 | func (p PolicyDockerSock) Validate(ctx context.Context, config policies.Config, ar *admissionv1.AdmissionRequest) ([]policies.ResourceViolation, []policies.PatchOperation) { 30 | 31 | resourceViolations := []policies.ResourceViolation{} 32 | 33 | podResource := resource.GetPodResource(ctx, ar) 34 | if podResource == nil { 35 | return resourceViolations, nil 36 | } 37 | 38 | violationText := "Docker Sock Mount: mounting the Docker socket is forbidden" 39 | 40 | for _, volume := range podResource.PodSpec.Volumes { 41 | if volume.HostPath != nil { 42 | if volume.HostPath.Path == "/var/run/docker.sock" { 43 | resourceViolations = append(resourceViolations, policies.ResourceViolation{ 44 | Namespace: ar.Namespace, 45 | ResourceName: podResource.ResourceName, 46 | ResourceKind: podResource.ResourceKind, 47 | Violation: violationText, 48 | Policy: p.Name(), 49 | }) 50 | } 51 | } 52 | } 53 | 54 | return resourceViolations, nil 55 | } 56 | -------------------------------------------------------------------------------- /policies/pod/docker_sock_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Cruise LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // https://www.apache.org/licenses/LICENSE-2.0 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | package pod 14 | 15 | import ( 16 | "context" 17 | "encoding/json" 18 | "reflect" 19 | "testing" 20 | 21 | "github.com/cruise-automation/k-rail/v3/policies" 22 | admissionv1 "k8s.io/api/admission/v1" 23 | corev1 "k8s.io/api/core/v1" 24 | v1 "k8s.io/api/core/v1" 25 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 26 | "k8s.io/apimachinery/pkg/runtime" 27 | ) 28 | 29 | func TestPolicyDockerSock(t *testing.T) { 30 | ctx := context.Background() 31 | 32 | tests := []struct { 33 | name string 34 | podSpec v1.PodSpec 35 | violations int 36 | }{ 37 | { 38 | name: "deny", 39 | violations: 1, 40 | podSpec: v1.PodSpec{ 41 | Volumes: []v1.Volume{ 42 | { 43 | VolumeSource: v1.VolumeSource{ 44 | HostPath: &v1.HostPathVolumeSource{ 45 | Path: "/var/run/docker.sock", 46 | }, 47 | }, 48 | }, 49 | }, 50 | }, 51 | }, 52 | { 53 | name: "allow", 54 | violations: 0, 55 | podSpec: v1.PodSpec{ 56 | Volumes: []v1.Volume{ 57 | { 58 | VolumeSource: v1.VolumeSource{ 59 | HostPath: &v1.HostPathVolumeSource{ 60 | Path: "/other/path", 61 | }, 62 | }, 63 | }, 64 | }, 65 | }, 66 | }, 67 | } 68 | for _, tt := range tests { 69 | t.Run(tt.name, func(t *testing.T) { 70 | 71 | raw, _ := json.Marshal(corev1.Pod{Spec: tt.podSpec}) 72 | ar := &admissionv1.AdmissionRequest{ 73 | Namespace: "namespace", 74 | Name: "name", 75 | Object: runtime.RawExtension{Raw: raw}, 76 | Resource: metav1.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"}, 77 | } 78 | 79 | v := PolicyDockerSock{} 80 | if got, _ := v.Validate(ctx, policies.Config{}, ar); !reflect.DeepEqual(len(got), tt.violations) { 81 | t.Errorf("PolicyDockerSock() %s got %v want %v violations", tt.name, len(got), tt.violations) 82 | } 83 | }) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /policies/pod/empty_dir_size_limit.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Cruise LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // https://www.apache.org/licenses/LICENSE-2.0 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | package pod 14 | 15 | import ( 16 | "context" 17 | "fmt" 18 | 19 | "github.com/cruise-automation/k-rail/v3/policies" 20 | "github.com/cruise-automation/k-rail/v3/resource" 21 | admissionv1 "k8s.io/api/admission/v1" 22 | ) 23 | 24 | type PolicyEmptyDirSizeLimit struct { 25 | } 26 | 27 | func (p PolicyEmptyDirSizeLimit) Name() string { 28 | return "pod_empty_dir_size_limit" 29 | } 30 | 31 | func (p PolicyEmptyDirSizeLimit) Validate(ctx context.Context, config policies.Config, ar *admissionv1.AdmissionRequest) ([]policies.ResourceViolation, []policies.PatchOperation) { 32 | var resourceViolations []policies.ResourceViolation 33 | 34 | podResource := resource.GetPodResource(ctx, ar) 35 | if podResource == nil { 36 | return resourceViolations, nil 37 | } 38 | 39 | cfg := config.MutateEmptyDirSizeLimit 40 | var patches []policies.PatchOperation 41 | 42 | violationText := "Empty dir size limit: size limit exceeds the max value" 43 | 44 | for i, volume := range podResource.PodSpec.Volumes { 45 | if volume.EmptyDir == nil { 46 | continue 47 | } 48 | if volume.EmptyDir.SizeLimit == nil || volume.EmptyDir.SizeLimit.IsZero() { 49 | patches = append(patches, policies.PatchOperation{ 50 | Op: "replace", 51 | Path: fmt.Sprintf(volumePatchPath(podResource.ResourceKind)+"/%d/emptyDir/sizeLimit", i), 52 | Value: cfg.DefaultSizeLimit.String(), 53 | }) 54 | continue 55 | } 56 | 57 | if volume.EmptyDir.SizeLimit.Cmp(cfg.MaximumSizeLimit) > 0 { 58 | resourceViolations = append(resourceViolations, policies.ResourceViolation{ 59 | Namespace: ar.Namespace, 60 | ResourceName: podResource.ResourceName, 61 | ResourceKind: podResource.ResourceKind, 62 | Violation: violationText, 63 | Policy: p.Name(), 64 | }) 65 | } 66 | } 67 | return resourceViolations, patches 68 | } 69 | 70 | const templateVolumePath = "/spec/template/spec/volumes" 71 | 72 | func volumePatchPath(podKind string) string { 73 | nonTemplateKinds := map[string]string{ 74 | "Pod": "/spec/volumes", 75 | "CronJob": "/spec/jobTemplate/spec/template/spec/volumes", 76 | } 77 | if pathPath, ok := nonTemplateKinds[podKind]; ok { 78 | return pathPath 79 | } 80 | return templateVolumePath 81 | } 82 | -------------------------------------------------------------------------------- /policies/pod/immutable_image_digest.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Cruise LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // https://www.apache.org/licenses/LICENSE-2.0 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | package pod 14 | 15 | import ( 16 | "context" 17 | "strings" 18 | 19 | admissionv1 "k8s.io/api/admission/v1" 20 | corev1 "k8s.io/api/core/v1" 21 | 22 | "github.com/cruise-automation/k-rail/v3/policies" 23 | "github.com/cruise-automation/k-rail/v3/resource" 24 | 25 | digest "github.com/opencontainers/go-digest" 26 | ) 27 | 28 | type PolicyImageImmutableReference struct{} 29 | 30 | func (p PolicyImageImmutableReference) Name() string { 31 | return "pod_immutable_reference" 32 | } 33 | 34 | func (p PolicyImageImmutableReference) Validate(ctx context.Context, config policies.Config, ar *admissionv1.AdmissionRequest) ([]policies.ResourceViolation, []policies.PatchOperation) { 35 | 36 | resourceViolations := []policies.ResourceViolation{} 37 | 38 | podResource := resource.GetPodResource(ctx, ar) 39 | if podResource == nil { 40 | return resourceViolations, nil 41 | } 42 | 43 | violationText := "Immutable Image Reference: image tag must include its sha256 digest" 44 | 45 | validateContainer := func(container corev1.Container) { 46 | // validate that the image name ends with a digest 47 | refSplit := strings.Split(container.Image, "@") 48 | if len(refSplit) == 2 { 49 | d, _ := digest.Parse(refSplit[len(refSplit)-1]) 50 | err := d.Validate() 51 | if err != nil { 52 | resourceViolations = append(resourceViolations, policies.ResourceViolation{ 53 | Namespace: ar.Namespace, 54 | ResourceName: podResource.ResourceName, 55 | ResourceKind: podResource.ResourceKind, 56 | Violation: violationText, 57 | Policy: p.Name(), 58 | Error: err, 59 | }) 60 | } 61 | } else { 62 | resourceViolations = append(resourceViolations, policies.ResourceViolation{ 63 | Namespace: ar.Namespace, 64 | ResourceName: podResource.ResourceName, 65 | ResourceKind: podResource.ResourceKind, 66 | Violation: violationText, 67 | Policy: p.Name(), 68 | Error: nil, 69 | }) 70 | } 71 | } 72 | 73 | for _, container := range podResource.PodSpec.Containers { 74 | validateContainer(container) 75 | } 76 | 77 | for _, container := range podResource.PodSpec.InitContainers { 78 | validateContainer(container) 79 | } 80 | 81 | return resourceViolations, nil 82 | } 83 | -------------------------------------------------------------------------------- /policies/pod/immutable_image_digest_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Cruise LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // https://www.apache.org/licenses/LICENSE-2.0 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | package pod 14 | 15 | import ( 16 | "context" 17 | "encoding/json" 18 | "reflect" 19 | "testing" 20 | 21 | "github.com/cruise-automation/k-rail/v3/policies" 22 | admissionv1 "k8s.io/api/admission/v1" 23 | corev1 "k8s.io/api/core/v1" 24 | v1 "k8s.io/api/core/v1" 25 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 26 | "k8s.io/apimachinery/pkg/runtime" 27 | ) 28 | 29 | func TestPolicyImageImmutableReference(t *testing.T) { 30 | ctx := context.Background() 31 | 32 | tests := []struct { 33 | name string 34 | podSpec v1.PodSpec 35 | violations int 36 | }{ 37 | { 38 | name: "allow", 39 | violations: 0, 40 | podSpec: v1.PodSpec{ 41 | Containers: []v1.Container{ 42 | { 43 | Image: "alpine@sha256:dad671370a148e9d9573e3e10a9f8cc26ce937bea78f3da80b570c2442364406", 44 | }, 45 | }, 46 | }, 47 | }, 48 | { 49 | name: "deny", 50 | violations: 1, 51 | podSpec: v1.PodSpec{ 52 | Containers: []v1.Container{ 53 | { 54 | Image: "alpine:latest", 55 | }, 56 | }, 57 | }, 58 | }, 59 | } 60 | for _, tt := range tests { 61 | t.Run(tt.name, func(t *testing.T) { 62 | 63 | raw, _ := json.Marshal(corev1.Pod{Spec: tt.podSpec}) 64 | ar := &admissionv1.AdmissionRequest{ 65 | Namespace: "namespace", 66 | Name: "name", 67 | Object: runtime.RawExtension{Raw: raw}, 68 | Resource: metav1.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"}, 69 | } 70 | 71 | v := PolicyImageImmutableReference{} 72 | if got, _ := v.Validate(ctx, policies.Config{}, ar); !reflect.DeepEqual(len(got), tt.violations) { 73 | t.Errorf("PolicyImageImmutableReference() = %v, got %v want %v violations", tt.podSpec.Containers[0].Image, len(got), tt.violations) 74 | } 75 | }) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /policies/pod/mutate_default_seccomp_policy.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Cruise LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // https://www.apache.org/licenses/LICENSE-2.0 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | package pod 14 | 15 | import ( 16 | "context" 17 | 18 | "github.com/cruise-automation/k-rail/v3/policies" 19 | "github.com/cruise-automation/k-rail/v3/resource" 20 | admissionv1 "k8s.io/api/admission/v1" 21 | ) 22 | 23 | type PolicyDefaultSeccompPolicy struct{} 24 | 25 | func (p PolicyDefaultSeccompPolicy) Name() string { 26 | return "pod_default_seccomp_policy" 27 | } 28 | 29 | func (p PolicyDefaultSeccompPolicy) Validate(ctx context.Context, config policies.Config, ar *admissionv1.AdmissionRequest) ([]policies.ResourceViolation, []policies.PatchOperation) { 30 | 31 | podResource := resource.GetPodResource(ctx, ar) 32 | if podResource == nil { 33 | return nil, nil 34 | } 35 | 36 | var patches []policies.PatchOperation 37 | 38 | seccompPolicy := config.PolicyDefaultSeccompPolicy 39 | if len(seccompPolicy) == 0 { 40 | seccompPolicy = "runtime/default" 41 | } 42 | 43 | if podResource.ResourceKind == "Pod" { 44 | apply := true 45 | for name := range podResource.PodAnnotations { 46 | if name == "seccomp.security.alpha.kubernetes.io/pod" { 47 | apply = false 48 | } 49 | } 50 | 51 | if apply { 52 | if podResource.PodAnnotations == nil { 53 | patches = append(patches, policies.PatchOperation{ 54 | Op: "add", 55 | Path: "/metadata/annotations", 56 | Value: map[string]string{ 57 | "seccomp.security.alpha.kubernetes.io/pod": seccompPolicy, 58 | }, 59 | }) 60 | } else { 61 | patches = append(patches, policies.PatchOperation{ 62 | Op: "replace", 63 | // escape `/` in a path with ~1 in JSONPatch format 🤮 64 | Path: "/metadata/annotations/seccomp.security.alpha.kubernetes.io~1pod", 65 | Value: seccompPolicy, 66 | }) 67 | } 68 | } 69 | } 70 | 71 | return nil, patches 72 | } 73 | -------------------------------------------------------------------------------- /policies/pod/mutate_default_seccomp_policy_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Cruise LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // https://www.apache.org/licenses/LICENSE-2.0 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | package pod 14 | 15 | import ( 16 | "context" 17 | "encoding/json" 18 | "reflect" 19 | "testing" 20 | 21 | "github.com/cruise-automation/k-rail/v3/policies" 22 | admissionv1 "k8s.io/api/admission/v1" 23 | corev1 "k8s.io/api/core/v1" 24 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 25 | "k8s.io/apimachinery/pkg/runtime" 26 | ) 27 | 28 | func TestPolicyDefaultSeccompPolicy(t *testing.T) { 29 | ctx := context.Background() 30 | 31 | tests := []struct { 32 | name string 33 | podSpec corev1.PodSpec 34 | annotations map[string]string 35 | expectedPatches map[string]*policies.PatchOperation 36 | }{ 37 | { 38 | name: "with annotation", 39 | podSpec: corev1.PodSpec{}, 40 | annotations: map[string]string{ 41 | "cluster-autoscaler.kubernetes.io/safe-to-evict": "true", 42 | }, 43 | expectedPatches: map[string]*policies.PatchOperation{ 44 | "/metadata/annotations/seccomp.security.alpha.kubernetes.io~1pod": &policies.PatchOperation{ 45 | Op: "replace", 46 | Path: "/metadata/annotations/seccomp.security.alpha.kubernetes.io~1pod", 47 | Value: "runtime/default", 48 | }, 49 | }, 50 | }, 51 | { 52 | name: "without annotation", 53 | podSpec: corev1.PodSpec{}, 54 | annotations: nil, 55 | expectedPatches: map[string]*policies.PatchOperation{ 56 | "/metadata/annotations": &policies.PatchOperation{ 57 | Op: "add", 58 | Path: "/metadata/annotations", 59 | Value: map[string]string{ 60 | "seccomp.security.alpha.kubernetes.io/pod": "runtime/default", 61 | }, 62 | }, 63 | }, 64 | }, 65 | } 66 | for _, tt := range tests { 67 | t.Run(tt.name, func(t *testing.T) { 68 | 69 | raw, _ := json.Marshal(corev1.Pod{Spec: tt.podSpec, ObjectMeta: metav1.ObjectMeta{Annotations: tt.annotations}}) 70 | ar := &admissionv1.AdmissionRequest{ 71 | Namespace: "namespace", 72 | Name: "name", 73 | Object: runtime.RawExtension{Raw: raw}, 74 | Resource: metav1.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"}, 75 | } 76 | 77 | v := PolicyDefaultSeccompPolicy{} 78 | conf := policies.Config{} 79 | _, patches := v.Validate(ctx, conf, ar) 80 | if len(tt.expectedPatches) != len(patches) { 81 | t.Fatalf("PolicyDefaultSeccompPolicy failed, expected number of Patches:%d, returned number of Patches: %d", len(tt.expectedPatches), len(patches)) 82 | } 83 | for _, patch := range patches { 84 | p, ok := tt.expectedPatches[patch.Path] 85 | if !ok { 86 | t.Fatalf("PolicyDefaultSeccompPolicy return unwanted patch: %v", patch) 87 | } 88 | if !reflect.DeepEqual(p.Value, patch.Value) || p.Op != patch.Op { 89 | t.Fatalf("PolicyDefaultSeccompPolicy expectedPatch: %v, returned patch: %v", p, patch) 90 | } 91 | } 92 | }) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /policies/pod/mutate_image_pull_policy.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Cruise LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // https://www.apache.org/licenses/LICENSE-2.0 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | package pod 14 | 15 | import ( 16 | "context" 17 | "fmt" 18 | "regexp" 19 | 20 | "github.com/cruise-automation/k-rail/v3/policies" 21 | "github.com/cruise-automation/k-rail/v3/resource" 22 | admissionv1 "k8s.io/api/admission/v1" 23 | corev1 "k8s.io/api/core/v1" 24 | ) 25 | 26 | // PolicyImagePullPolicy is to enforce the imagePullPolicy 27 | type PolicyImagePullPolicy struct{} 28 | 29 | // Name is to return the name of the policy 30 | func (p PolicyImagePullPolicy) Name() string { 31 | return "pod_image_pull_policy" 32 | } 33 | 34 | // Validate is to enforce the imagePullPolicy 35 | func (p PolicyImagePullPolicy) Validate(ctx context.Context, config policies.Config, ar *admissionv1.AdmissionRequest) ([]policies.ResourceViolation, []policies.PatchOperation) { 36 | 37 | podResource := resource.GetPodResource(ctx, ar) 38 | if podResource == nil { 39 | return nil, nil 40 | } 41 | 42 | var patches []policies.PatchOperation 43 | 44 | // if there is nothing configured, directly pass the Validate 45 | if len(config.PolicyImagePullPolicy) == 0 { 46 | return nil, nil 47 | } 48 | 49 | if podResource.ResourceKind == "Pod" { 50 | for index, container := range podResource.PodSpec.InitContainers { 51 | patch := checkImagePullPolicy(&container, fmt.Sprintf("/spec/initContainers/%d/imagePullPolicy", index), config.PolicyImagePullPolicy) 52 | if patch != nil { 53 | patches = append(patches, *patch) 54 | } 55 | } 56 | for index, container := range podResource.PodSpec.Containers { 57 | patch := checkImagePullPolicy(&container, fmt.Sprintf("/spec/containers/%d/imagePullPolicy", index), config.PolicyImagePullPolicy) 58 | if patch != nil { 59 | patches = append(patches, *patch) 60 | } 61 | } 62 | } 63 | 64 | return nil, patches 65 | } 66 | 67 | func checkImagePullPolicy(c *corev1.Container, path string, imagePullPolicyMap map[string][]string) *policies.PatchOperation { 68 | // loop through pullPolicy enforcement configured 69 | for enforcedPullPolicy, imageRegexes := range imagePullPolicyMap { 70 | for _, pattern := range imageRegexes { 71 | // check if image matches the regex 72 | matched, _ := regexp.MatchString(pattern, c.Image) 73 | if !matched { 74 | continue 75 | } 76 | if enforcedPullPolicy != string(c.ImagePullPolicy) { 77 | return &policies.PatchOperation{ 78 | Op: "replace", 79 | Path: path, 80 | Value: enforcedPullPolicy, 81 | } 82 | } else { 83 | return nil 84 | } 85 | } 86 | } 87 | return nil 88 | } 89 | -------------------------------------------------------------------------------- /policies/pod/mutate_safe_to_evict.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Cruise LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // https://www.apache.org/licenses/LICENSE-2.0 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | package pod 14 | 15 | import ( 16 | "context" 17 | 18 | "github.com/cruise-automation/k-rail/v3/policies" 19 | "github.com/cruise-automation/k-rail/v3/resource" 20 | admissionv1 "k8s.io/api/admission/v1" 21 | ) 22 | 23 | type PolicyMutateSafeToEvict struct{} 24 | 25 | func (p PolicyMutateSafeToEvict) Name() string { 26 | return "pod_mutate_safe_to_evict" 27 | } 28 | 29 | func (p PolicyMutateSafeToEvict) Validate(ctx context.Context, config policies.Config, ar *admissionv1.AdmissionRequest) ([]policies.ResourceViolation, []policies.PatchOperation) { 30 | 31 | podResource := resource.GetPodResource(ctx, ar) 32 | if podResource == nil { 33 | return nil, nil 34 | } 35 | 36 | var patches []policies.PatchOperation 37 | 38 | if podResource.ResourceKind == "Pod" { 39 | for _, volume := range podResource.PodSpec.Volumes { 40 | if volume.HostPath != nil || volume.EmptyDir != nil { 41 | apply := true 42 | for name := range podResource.PodAnnotations { 43 | if name == "cluster-autoscaler.kubernetes.io/safe-to-evict" { 44 | apply = false 45 | } 46 | } 47 | 48 | if apply { 49 | if podResource.PodAnnotations == nil { 50 | patches = append(patches, policies.PatchOperation{ 51 | Op: "add", 52 | Path: "/metadata/annotations", 53 | Value: map[string]string{ 54 | "cluster-autoscaler.kubernetes.io/safe-to-evict": "true", 55 | }, 56 | }) 57 | } else { 58 | patches = append(patches, policies.PatchOperation{ 59 | Op: "replace", 60 | Path: "/metadata/annotations/cluster-autoscaler.kubernetes.io~1safe-to-evict", 61 | Value: "true", 62 | }) 63 | } 64 | } 65 | } 66 | } 67 | } 68 | 69 | return nil, patches 70 | } 71 | -------------------------------------------------------------------------------- /policies/pod/no_exec.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Cruise LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // https://www.apache.org/licenses/LICENSE-2.0 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | package pod 14 | 15 | import ( 16 | "context" 17 | 18 | "github.com/cruise-automation/k-rail/v3/policies" 19 | "github.com/cruise-automation/k-rail/v3/resource" 20 | admissionv1 "k8s.io/api/admission/v1" 21 | ) 22 | 23 | type PolicyNoExec struct{} 24 | 25 | func (p PolicyNoExec) Name() string { 26 | return "pod_no_exec" 27 | } 28 | 29 | func (p PolicyNoExec) Validate(ctx context.Context, config policies.Config, ar *admissionv1.AdmissionRequest) ([]policies.ResourceViolation, []policies.PatchOperation) { 30 | 31 | resourceViolations := []policies.ResourceViolation{} 32 | 33 | podExecResource := resource.GetPodExecResource(ctx, ar) 34 | if podExecResource == nil { 35 | return resourceViolations, nil 36 | } 37 | 38 | violationText := "No pod exec: Execing into a Pod is forbidden without an exemption" 39 | 40 | resourceViolations = append(resourceViolations, policies.ResourceViolation{ 41 | Namespace: ar.Namespace, 42 | ResourceName: podExecResource.ResourceName, 43 | ResourceKind: podExecResource.ResourceKind, 44 | Violation: violationText, 45 | Policy: p.Name(), 46 | }) 47 | 48 | return resourceViolations, nil 49 | } 50 | -------------------------------------------------------------------------------- /policies/pod/no_host_network.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Cruise LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // https://www.apache.org/licenses/LICENSE-2.0 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | package pod 14 | 15 | import ( 16 | "context" 17 | 18 | "github.com/cruise-automation/k-rail/v3/policies" 19 | "github.com/cruise-automation/k-rail/v3/resource" 20 | admissionv1 "k8s.io/api/admission/v1" 21 | ) 22 | 23 | type PolicyNoHostNetwork struct{} 24 | 25 | func (p PolicyNoHostNetwork) Name() string { 26 | return "pod_no_host_network" 27 | } 28 | 29 | func (p PolicyNoHostNetwork) Validate(ctx context.Context, config policies.Config, ar *admissionv1.AdmissionRequest) ([]policies.ResourceViolation, []policies.PatchOperation) { 30 | 31 | resourceViolations := []policies.ResourceViolation{} 32 | 33 | podResource := resource.GetPodResource(ctx, ar) 34 | if podResource == nil { 35 | return resourceViolations, nil 36 | } 37 | 38 | violationText := "No Host Network: Using the host network is forbidden" 39 | 40 | if podResource.PodSpec.HostNetwork { 41 | resourceViolations = append(resourceViolations, policies.ResourceViolation{ 42 | Namespace: ar.Namespace, 43 | ResourceName: podResource.ResourceName, 44 | ResourceKind: podResource.ResourceKind, 45 | Violation: violationText, 46 | Policy: p.Name(), 47 | }) 48 | } 49 | 50 | return resourceViolations, nil 51 | } 52 | -------------------------------------------------------------------------------- /policies/pod/no_host_pid.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Cruise LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // https://www.apache.org/licenses/LICENSE-2.0 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | package pod 14 | 15 | import ( 16 | "context" 17 | 18 | "github.com/cruise-automation/k-rail/v3/policies" 19 | "github.com/cruise-automation/k-rail/v3/resource" 20 | admissionv1 "k8s.io/api/admission/v1" 21 | ) 22 | 23 | type PolicyNoHostPID struct{} 24 | 25 | func (p PolicyNoHostPID) Name() string { 26 | return "pod_no_host_pid" 27 | } 28 | 29 | func (p PolicyNoHostPID) Validate(ctx context.Context, config policies.Config, ar *admissionv1.AdmissionRequest) ([]policies.ResourceViolation, []policies.PatchOperation) { 30 | 31 | resourceViolations := []policies.ResourceViolation{} 32 | 33 | podResource := resource.GetPodResource(ctx, ar) 34 | if podResource == nil { 35 | return resourceViolations, nil 36 | } 37 | 38 | violationText := "No Host PID: Using the host PID namespace is forbidden" 39 | 40 | if podResource.PodSpec.HostPID { 41 | resourceViolations = append(resourceViolations, policies.ResourceViolation{ 42 | Namespace: ar.Namespace, 43 | ResourceName: podResource.ResourceName, 44 | ResourceKind: podResource.ResourceKind, 45 | Violation: violationText, 46 | Policy: p.Name(), 47 | }) 48 | } 49 | 50 | return resourceViolations, nil 51 | } 52 | -------------------------------------------------------------------------------- /policies/pod/no_new_capabilities.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Cruise LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // https://www.apache.org/licenses/LICENSE-2.0 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | package pod 14 | 15 | import ( 16 | "context" 17 | 18 | admissionv1 "k8s.io/api/admission/v1" 19 | corev1 "k8s.io/api/core/v1" 20 | 21 | "github.com/cruise-automation/k-rail/v3/policies" 22 | "github.com/cruise-automation/k-rail/v3/resource" 23 | ) 24 | 25 | type PolicyNoNewCapabilities struct{} 26 | 27 | func (p PolicyNoNewCapabilities) Name() string { 28 | return "pod_no_new_capabilities" 29 | } 30 | 31 | func (p PolicyNoNewCapabilities) Validate(ctx context.Context, config policies.Config, ar *admissionv1.AdmissionRequest) ([]policies.ResourceViolation, []policies.PatchOperation) { 32 | 33 | resourceViolations := []policies.ResourceViolation{} 34 | 35 | podResource := resource.GetPodResource(ctx, ar) 36 | if podResource == nil { 37 | return resourceViolations, nil 38 | } 39 | 40 | violationText := "No New Capabilities: Adding additional capabilities is forbidden" 41 | 42 | validateContainer := func(container corev1.Container) { 43 | if container.SecurityContext != nil && container.SecurityContext.Capabilities != nil { 44 | if len(container.SecurityContext.Capabilities.Add) > 0 { 45 | resourceViolations = append(resourceViolations, policies.ResourceViolation{ 46 | Namespace: ar.Namespace, 47 | ResourceName: podResource.ResourceName, 48 | ResourceKind: podResource.ResourceKind, 49 | Violation: violationText, 50 | Policy: p.Name(), 51 | }) 52 | } 53 | } 54 | } 55 | 56 | for _, container := range podResource.PodSpec.Containers { 57 | validateContainer(container) 58 | } 59 | 60 | for _, container := range podResource.PodSpec.InitContainers { 61 | validateContainer(container) 62 | } 63 | 64 | return resourceViolations, nil 65 | } 66 | -------------------------------------------------------------------------------- /policies/pod/no_privileged_container.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Cruise LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // https://www.apache.org/licenses/LICENSE-2.0 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | package pod 14 | 15 | import ( 16 | "context" 17 | 18 | admissionv1 "k8s.io/api/admission/v1" 19 | corev1 "k8s.io/api/core/v1" 20 | 21 | "github.com/cruise-automation/k-rail/v3/policies" 22 | "github.com/cruise-automation/k-rail/v3/resource" 23 | ) 24 | 25 | type PolicyNoPrivilegedContainer struct{} 26 | 27 | func (p PolicyNoPrivilegedContainer) Name() string { 28 | return "pod_no_privileged_container" 29 | } 30 | 31 | func (p PolicyNoPrivilegedContainer) Validate(ctx context.Context, config policies.Config, ar *admissionv1.AdmissionRequest) ([]policies.ResourceViolation, []policies.PatchOperation) { 32 | 33 | resourceViolations := []policies.ResourceViolation{} 34 | 35 | podResource := resource.GetPodResource(ctx, ar) 36 | if podResource == nil { 37 | return resourceViolations, nil 38 | } 39 | 40 | violationText := "No Privileged Container: Using privileged containers is forbidden" 41 | 42 | validateContainer := func(container corev1.Container) { 43 | if container.SecurityContext != nil && container.SecurityContext.Privileged != nil { 44 | if *container.SecurityContext.Privileged { 45 | resourceViolations = append(resourceViolations, policies.ResourceViolation{ 46 | Namespace: ar.Namespace, 47 | ResourceName: podResource.ResourceName, 48 | ResourceKind: podResource.ResourceKind, 49 | Violation: violationText, 50 | Policy: p.Name(), 51 | }) 52 | } 53 | } 54 | } 55 | 56 | for _, container := range podResource.PodSpec.Containers { 57 | validateContainer(container) 58 | } 59 | 60 | for _, container := range podResource.PodSpec.InitContainers { 61 | validateContainer(container) 62 | } 63 | 64 | return resourceViolations, nil 65 | } 66 | -------------------------------------------------------------------------------- /policies/pod/no_root.go: -------------------------------------------------------------------------------- 1 | package pod 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/cruise-automation/k-rail/v3/policies" 7 | "github.com/cruise-automation/k-rail/v3/resource" 8 | admissionv1 "k8s.io/api/admission/v1" 9 | corev1 "k8s.io/api/core/v1" 10 | ) 11 | 12 | type PolicyNoRootUser struct{} 13 | 14 | func (p PolicyNoRootUser) Name() string { 15 | return "pod_no_root_user" 16 | } 17 | 18 | func (p PolicyNoRootUser) Validate(ctx context.Context, _ policies.Config, ar *admissionv1.AdmissionRequest) ([]policies.ResourceViolation, []policies.PatchOperation) { 19 | resourceViolations := []policies.ResourceViolation{} 20 | podResource := resource.GetPodResource(ctx, ar) 21 | if podResource == nil { 22 | return resourceViolations, nil 23 | } 24 | violationText := "No Root user: Running as the root user is forbidden" 25 | 26 | // check the containers first 27 | validateSecurityContext := func(container corev1.Container) { 28 | if container.SecurityContext != nil && container.SecurityContext.RunAsNonRoot != nil && *container.SecurityContext.RunAsNonRoot { 29 | return 30 | } else if container.SecurityContext != nil && container.SecurityContext.RunAsUser != nil && *container.SecurityContext.RunAsUser > 0 { 31 | return 32 | } 33 | resourceViolations = append(resourceViolations, policies.ResourceViolation{ 34 | Namespace: ar.Namespace, 35 | ResourceName: podResource.ResourceName, 36 | ResourceKind: podResource.ResourceKind, 37 | Violation: fmt.Sprintf("No Root user: Container %s can run as the root user which is forbidden", container.Name), 38 | Policy: p.Name(), 39 | }) 40 | } 41 | for _, container := range podResource.PodSpec.Containers { 42 | validateSecurityContext(container) 43 | } 44 | for _, container := range podResource.PodSpec.InitContainers { 45 | validateSecurityContext(container) 46 | } 47 | 48 | // if all the containers have the appropriate securityContext 49 | // and the podSecurityContext is unset, we can skip checking it 50 | if len(resourceViolations) == 0 && podResource.PodSpec.SecurityContext == nil { 51 | return resourceViolations, nil 52 | } 53 | 54 | if podResource.PodSpec.SecurityContext != nil && podResource.PodSpec.SecurityContext.RunAsNonRoot != nil && *podResource.PodSpec.SecurityContext.RunAsNonRoot { 55 | return resourceViolations, nil 56 | } else if podResource.PodSpec.SecurityContext != nil && podResource.PodSpec.SecurityContext.RunAsUser != nil && *podResource.PodSpec.SecurityContext.RunAsUser > 0 { 57 | return resourceViolations, nil 58 | } 59 | 60 | resourceViolations = append(resourceViolations, policies.ResourceViolation{ 61 | Namespace: ar.Namespace, 62 | ResourceName: podResource.ResourceName, 63 | ResourceKind: podResource.ResourceKind, 64 | Violation: violationText, 65 | Policy: p.Name(), 66 | }) 67 | return resourceViolations, nil 68 | } 69 | -------------------------------------------------------------------------------- /policies/pod/no_root_test.go: -------------------------------------------------------------------------------- 1 | package pod 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "github.com/cruise-automation/k-rail/v3/policies" 7 | admissionv1 "k8s.io/api/admission/v1" 8 | corev1 "k8s.io/api/core/v1" 9 | v1 "k8s.io/api/core/v1" 10 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 11 | "k8s.io/apimachinery/pkg/runtime" 12 | "k8s.io/utils/pointer" 13 | "reflect" 14 | "testing" 15 | ) 16 | 17 | func TestPolicyNoRoot(t *testing.T) { 18 | ctx := context.Background() 19 | 20 | tests := []struct { 21 | name string 22 | podSpec v1.PodSpec 23 | violations int 24 | }{ 25 | { 26 | name: "empty securityContext", 27 | violations: 3, 28 | podSpec: v1.PodSpec{ 29 | SecurityContext: &v1.PodSecurityContext{}, 30 | Containers: []v1.Container{ 31 | { 32 | SecurityContext: &v1.SecurityContext{}, 33 | }, 34 | }, 35 | InitContainers: []v1.Container{ 36 | { 37 | SecurityContext: &v1.SecurityContext{}, 38 | }, 39 | }, 40 | }, 41 | }, 42 | { 43 | name: "runAsRoot", 44 | violations: 1, 45 | podSpec: v1.PodSpec{ 46 | SecurityContext: &v1.PodSecurityContext{ 47 | RunAsUser: pointer.Int64Ptr(0), 48 | }, 49 | }, 50 | }, 51 | { 52 | name: "runAsUser", 53 | violations: 0, 54 | podSpec: v1.PodSpec{ 55 | SecurityContext: &v1.PodSecurityContext{ 56 | RunAsUser: pointer.Int64Ptr(1001), 57 | }, 58 | }, 59 | }, 60 | { 61 | name: "runAsNonRoot", 62 | violations: 0, 63 | podSpec: v1.PodSpec{ 64 | SecurityContext: &v1.PodSecurityContext{ 65 | RunAsNonRoot: pointer.BoolPtr(true), 66 | }, 67 | }, 68 | }, 69 | { 70 | name: "runAsUser container", 71 | violations: 0, 72 | podSpec: v1.PodSpec{ 73 | SecurityContext: &v1.PodSecurityContext{ 74 | RunAsNonRoot: pointer.BoolPtr(true), 75 | }, 76 | Containers: []v1.Container{ 77 | { 78 | SecurityContext: &v1.SecurityContext{ 79 | RunAsUser: pointer.Int64Ptr(1001), 80 | }, 81 | }, 82 | }, 83 | }, 84 | }, 85 | { 86 | name: "no pod context, but containers are set", 87 | violations: 0, 88 | podSpec: v1.PodSpec{ 89 | SecurityContext: nil, 90 | Containers: []v1.Container{ 91 | { 92 | SecurityContext: &v1.SecurityContext{ 93 | RunAsNonRoot: pointer.BoolPtr(true), 94 | }, 95 | }, 96 | }, 97 | }, 98 | }, 99 | } 100 | for _, tt := range tests { 101 | t.Run(tt.name, func(t *testing.T) { 102 | raw, _ := json.Marshal(corev1.Pod{Spec: tt.podSpec}) 103 | ar := &admissionv1.AdmissionRequest{ 104 | Namespace: "namespace", 105 | Name: "name", 106 | Object: runtime.RawExtension{Raw: raw}, 107 | Resource: metav1.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"}, 108 | } 109 | v := PolicyNoRootUser{} 110 | if got, _ := v.Validate(ctx, policies.Config{}, ar); !reflect.DeepEqual(len(got), tt.violations) { 111 | t.Errorf("PolicyNoRootUser() %s got %v want %v violations", tt.name, len(got), tt.violations) 112 | } 113 | }) 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /policies/pod/no_shareprocessnamespace.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Cruise LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // https://www.apache.org/licenses/LICENSE-2.0 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | package pod 14 | 15 | import ( 16 | "context" 17 | 18 | "github.com/cruise-automation/k-rail/v3/policies" 19 | "github.com/cruise-automation/k-rail/v3/resource" 20 | admissionv1 "k8s.io/api/admission/v1" 21 | ) 22 | 23 | type PolicyNoShareProcessNamespace struct{} 24 | 25 | func (p PolicyNoShareProcessNamespace) Name() string { 26 | return "pod_no_shareprocessnamespace" 27 | } 28 | 29 | func (p PolicyNoShareProcessNamespace) Validate(ctx context.Context, config policies.Config, ar *admissionv1.AdmissionRequest) ([]policies.ResourceViolation, []policies.PatchOperation) { 30 | 31 | resourceViolations := []policies.ResourceViolation{} 32 | 33 | podResource := resource.GetPodResource(ctx, ar) 34 | if podResource == nil { 35 | return resourceViolations, nil 36 | } 37 | 38 | violationText := "No ShareProcessNamespace: sharing the process namespace among containers in a Pod is forbidden." 39 | 40 | if podResource.PodSpec.ShareProcessNamespace != nil && *podResource.PodSpec.ShareProcessNamespace { 41 | resourceViolations = append(resourceViolations, policies.ResourceViolation{ 42 | Namespace: ar.Namespace, 43 | ResourceName: podResource.ResourceName, 44 | ResourceKind: podResource.ResourceKind, 45 | Violation: violationText, 46 | Policy: p.Name(), 47 | }) 48 | } 49 | 50 | return resourceViolations, nil 51 | } 52 | -------------------------------------------------------------------------------- /policies/pod/no_shareprocessnamespace_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Cruise LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // https://www.apache.org/licenses/LICENSE-2.0 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | package pod 14 | 15 | import ( 16 | "context" 17 | "encoding/json" 18 | "testing" 19 | 20 | "github.com/cruise-automation/k-rail/v3/policies" 21 | admissionv1 "k8s.io/api/admission/v1" 22 | corev1 "k8s.io/api/core/v1" 23 | v1 "k8s.io/api/core/v1" 24 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 25 | "k8s.io/apimachinery/pkg/runtime" 26 | "k8s.io/utils/pointer" 27 | ) 28 | 29 | func TestPolicyNoShareProcessNamespace_Validate(t *testing.T) { 30 | type args struct { 31 | ctx context.Context 32 | config policies.Config 33 | ar *admissionv1.AdmissionRequest 34 | } 35 | tests := []struct { 36 | name string 37 | podSpec v1.PodSpec 38 | violations int 39 | }{ 40 | { 41 | name: "nil, no violation", 42 | podSpec: v1.PodSpec{ 43 | ShareProcessNamespace: nil, 44 | }, 45 | violations: 0, 46 | }, 47 | { 48 | name: "false, no violation", 49 | podSpec: v1.PodSpec{ 50 | ShareProcessNamespace: pointer.BoolPtr(false), 51 | }, 52 | violations: 0, 53 | }, 54 | { 55 | name: "true, violation", 56 | podSpec: v1.PodSpec{ 57 | ShareProcessNamespace: pointer.BoolPtr(true), 58 | }, 59 | violations: 1, 60 | }, 61 | } 62 | for _, tt := range tests { 63 | t.Run(tt.name, func(t *testing.T) { 64 | p := PolicyNoShareProcessNamespace{} 65 | raw, _ := json.Marshal(corev1.Pod{Spec: tt.podSpec}) 66 | ar := &admissionv1.AdmissionRequest{ 67 | Namespace: "namespace", 68 | Name: "name", 69 | Object: runtime.RawExtension{Raw: raw}, 70 | Resource: metav1.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"}, 71 | } 72 | got, _ := p.Validate(context.Background(), policies.Config{}, ar) 73 | if len(got) != tt.violations { 74 | t.Errorf("PolicyNoShareProcessNamespace.Validate() got = %v, want %v", len(got), tt.violations) 75 | } 76 | }) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /policies/pod/no_tiller.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Cruise LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // https://www.apache.org/licenses/LICENSE-2.0 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | package pod 14 | 15 | import ( 16 | "context" 17 | "strings" 18 | 19 | admissionv1 "k8s.io/api/admission/v1" 20 | corev1 "k8s.io/api/core/v1" 21 | 22 | "github.com/cruise-automation/k-rail/v3/policies" 23 | "github.com/cruise-automation/k-rail/v3/resource" 24 | ) 25 | 26 | type PolicyNoTiller struct{} 27 | 28 | func (p PolicyNoTiller) Name() string { 29 | return "pod_no_tiller" 30 | } 31 | 32 | func (p PolicyNoTiller) Validate(ctx context.Context, config policies.Config, ar *admissionv1.AdmissionRequest) ([]policies.ResourceViolation, []policies.PatchOperation) { 33 | 34 | resourceViolations := []policies.ResourceViolation{} 35 | 36 | podResource := resource.GetPodResource(ctx, ar) 37 | if podResource == nil { 38 | return resourceViolations, nil 39 | } 40 | 41 | violationText := "No Tiller: Helm Tiller is forbidden from running" 42 | 43 | validateContainer := func(container corev1.Container) { 44 | // we could also check for port 44134/tcp 45 | if strings.Contains(container.Image, "/tiller") { 46 | resourceViolations = append(resourceViolations, policies.ResourceViolation{ 47 | Namespace: ar.Namespace, 48 | ResourceName: podResource.ResourceName, 49 | ResourceKind: podResource.ResourceKind, 50 | Violation: violationText, 51 | Policy: p.Name(), 52 | }) 53 | } 54 | } 55 | 56 | for _, container := range podResource.PodSpec.Containers { 57 | validateContainer(container) 58 | } 59 | 60 | for _, container := range podResource.PodSpec.InitContainers { 61 | validateContainer(container) 62 | } 63 | 64 | return resourceViolations, nil 65 | } 66 | -------------------------------------------------------------------------------- /policies/pod/no_tiller_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Cruise LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // https://www.apache.org/licenses/LICENSE-2.0 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | package pod 14 | 15 | import ( 16 | "context" 17 | "encoding/json" 18 | "reflect" 19 | "testing" 20 | 21 | "github.com/cruise-automation/k-rail/v3/policies" 22 | admissionv1 "k8s.io/api/admission/v1" 23 | corev1 "k8s.io/api/core/v1" 24 | v1 "k8s.io/api/core/v1" 25 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 26 | "k8s.io/apimachinery/pkg/runtime" 27 | ) 28 | 29 | func TestPolicyNoTiller(t *testing.T) { 30 | ctx := context.Background() 31 | 32 | tests := []struct { 33 | name string 34 | podSpec v1.PodSpec 35 | violations int 36 | }{ 37 | { 38 | name: "allow", 39 | violations: 0, 40 | podSpec: v1.PodSpec{ 41 | Containers: []v1.Container{ 42 | { 43 | Image: "alpine@sha256:dad671370a148e9d9573e3e10a9f8cc26ce937bea78f3da80b570c2442364406", 44 | }, 45 | }, 46 | }, 47 | }, 48 | { 49 | name: "deny", 50 | violations: 1, 51 | podSpec: v1.PodSpec{ 52 | Containers: []v1.Container{ 53 | { 54 | Image: "repo/tiller", 55 | }, 56 | }, 57 | }, 58 | }, 59 | } 60 | for _, tt := range tests { 61 | t.Run(tt.name, func(t *testing.T) { 62 | 63 | raw, _ := json.Marshal(corev1.Pod{Spec: tt.podSpec}) 64 | ar := &admissionv1.AdmissionRequest{ 65 | Namespace: "namespace", 66 | Name: "name", 67 | Object: runtime.RawExtension{Raw: raw}, 68 | Resource: metav1.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"}, 69 | } 70 | 71 | v := PolicyNoTiller{} 72 | if got, _ := v.Validate(ctx, policies.Config{}, ar); !reflect.DeepEqual(len(got), tt.violations) { 73 | t.Errorf("PolicyNoTiller() %s got %v want %v violations", tt.name, len(got), tt.violations) 74 | } 75 | }) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /policies/pod/safe_to_evict.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Cruise LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // https://www.apache.org/licenses/LICENSE-2.0 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | package pod 14 | 15 | import ( 16 | "context" 17 | 18 | "github.com/cruise-automation/k-rail/v3/policies" 19 | "github.com/cruise-automation/k-rail/v3/resource" 20 | admissionv1 "k8s.io/api/admission/v1" 21 | ) 22 | 23 | type PolicySafeToEvict struct{} 24 | 25 | func (p PolicySafeToEvict) Name() string { 26 | return "pod_safe_to_evict" 27 | } 28 | 29 | func (p PolicySafeToEvict) Validate(ctx context.Context, config policies.Config, ar *admissionv1.AdmissionRequest) ([]policies.ResourceViolation, []policies.PatchOperation) { 30 | 31 | resourceViolations := []policies.ResourceViolation{} 32 | 33 | podResource := resource.GetPodResource(ctx, ar) 34 | if podResource == nil { 35 | return resourceViolations, nil 36 | } 37 | 38 | violationText := "Safe to evict: annotation is required for Pods that use emptyDir or hostPath mounts to enable cluster autoscaling" 39 | 40 | for _, volume := range podResource.PodSpec.Volumes { 41 | if volume.HostPath != nil || volume.EmptyDir != nil { 42 | found := false 43 | for name, value := range podResource.PodAnnotations { 44 | if name == "cluster-autoscaler.kubernetes.io/safe-to-evict" && value == "true" { 45 | found = true 46 | } 47 | } 48 | 49 | if !found { 50 | resourceViolations = append(resourceViolations, policies.ResourceViolation{ 51 | Namespace: ar.Namespace, 52 | ResourceName: podResource.ResourceName, 53 | ResourceKind: podResource.ResourceKind, 54 | Violation: violationText, 55 | Policy: p.Name(), 56 | }) 57 | } 58 | } 59 | } 60 | 61 | return resourceViolations, nil 62 | } 63 | -------------------------------------------------------------------------------- /policies/pod/safe_to_evict_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Cruise LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // https://www.apache.org/licenses/LICENSE-2.0 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | package pod 14 | 15 | import ( 16 | "context" 17 | "encoding/json" 18 | "reflect" 19 | "testing" 20 | 21 | "github.com/cruise-automation/k-rail/v3/policies" 22 | admissionv1 "k8s.io/api/admission/v1" 23 | corev1 "k8s.io/api/core/v1" 24 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 25 | "k8s.io/apimachinery/pkg/runtime" 26 | ) 27 | 28 | func TestPolicySafeToEvict(t *testing.T) { 29 | ctx := context.Background() 30 | 31 | tests := []struct { 32 | name string 33 | podSpec corev1.PodSpec 34 | annotations map[string]string 35 | violations int 36 | }{ 37 | { 38 | name: "allow with annotation and volume", 39 | violations: 0, 40 | podSpec: corev1.PodSpec{ 41 | Volumes: []corev1.Volume{ 42 | { 43 | VolumeSource: corev1.VolumeSource{ 44 | HostPath: &corev1.HostPathVolumeSource{ 45 | Path: "/host-path", 46 | }, 47 | }, 48 | }, 49 | }, 50 | }, 51 | annotations: map[string]string{ 52 | "cluster-autoscaler.kubernetes.io/safe-to-evict": "true", 53 | }, 54 | }, 55 | { 56 | name: "disallow with annotation and volume", 57 | violations: 1, 58 | podSpec: corev1.PodSpec{ 59 | Volumes: []corev1.Volume{ 60 | { 61 | VolumeSource: corev1.VolumeSource{ 62 | HostPath: &corev1.HostPathVolumeSource{ 63 | Path: "/host-path", 64 | }, 65 | }, 66 | }, 67 | }, 68 | }, 69 | annotations: map[string]string{ 70 | "cluster-autoscaler.kubernetes.io/safe-to-evict": "false", 71 | }, 72 | }, 73 | { 74 | name: "disallow with volume and without annotation", 75 | violations: 1, 76 | podSpec: corev1.PodSpec{ 77 | Volumes: []corev1.Volume{ 78 | { 79 | VolumeSource: corev1.VolumeSource{ 80 | HostPath: &corev1.HostPathVolumeSource{ 81 | Path: "/host-path", 82 | }, 83 | }, 84 | }, 85 | }, 86 | }, 87 | annotations: map[string]string{}, 88 | }, 89 | { 90 | name: "allow with annotation and no volume", 91 | violations: 0, 92 | podSpec: corev1.PodSpec{ 93 | Containers: []corev1.Container{ 94 | { 95 | Image: "gcr.io/some-gcr-repo/security/k-rail", 96 | }, 97 | }, 98 | }, 99 | annotations: map[string]string{ 100 | "cluster-autoscaler.kubernetes.io/safe-to-evict": "false", 101 | }, 102 | }, 103 | } 104 | for _, tt := range tests { 105 | t.Run(tt.name, func(t *testing.T) { 106 | 107 | raw, _ := json.Marshal(corev1.Pod{Spec: tt.podSpec, ObjectMeta: metav1.ObjectMeta{Annotations: tt.annotations}}) 108 | ar := &admissionv1.AdmissionRequest{ 109 | Namespace: "namespace", 110 | Name: "name", 111 | Object: runtime.RawExtension{Raw: raw}, 112 | Resource: metav1.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"}, 113 | } 114 | 115 | v := PolicySafeToEvict{} 116 | conf := policies.Config{} 117 | if got, _ := v.Validate(ctx, conf, ar); !reflect.DeepEqual(len(got), tt.violations) { 118 | t.Errorf("PolicySafeToEvict() %s got %v want %v violations", tt.name, len(got), tt.violations) 119 | } 120 | }) 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /policies/pod/trusted_repository.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Cruise LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // https://www.apache.org/licenses/LICENSE-2.0 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | package pod 14 | 15 | import ( 16 | "context" 17 | "regexp" 18 | 19 | admissionv1 "k8s.io/api/admission/v1" 20 | corev1 "k8s.io/api/core/v1" 21 | 22 | "github.com/cruise-automation/k-rail/v3/policies" 23 | "github.com/cruise-automation/k-rail/v3/resource" 24 | ) 25 | 26 | type PolicyTrustedRepository struct{} 27 | 28 | func (p PolicyTrustedRepository) Name() string { 29 | return "pod_trusted_repository" 30 | } 31 | 32 | func (p PolicyTrustedRepository) Validate(ctx context.Context, config policies.Config, ar *admissionv1.AdmissionRequest) ([]policies.ResourceViolation, []policies.PatchOperation) { 33 | 34 | resourceViolations := []policies.ResourceViolation{} 35 | 36 | podResource := resource.GetPodResource(ctx, ar) 37 | if podResource == nil { 38 | return resourceViolations, nil 39 | } 40 | 41 | validateContainer := func(container corev1.Container) { 42 | matches := 0 43 | for _, pattern := range config.PolicyTrustedRepositoryRegexes { 44 | if matched, _ := regexp.MatchString(pattern, container.Image); matched { 45 | matches++ 46 | } 47 | } 48 | 49 | if matches == 0 { 50 | violationText := "Trusted Image Repository: image must be sourced from a trusted repository. Untrusted Images: " + container.Image 51 | resourceViolations = append(resourceViolations, policies.ResourceViolation{ 52 | Namespace: ar.Namespace, 53 | ResourceName: podResource.ResourceName, 54 | ResourceKind: podResource.ResourceKind, 55 | Violation: violationText, 56 | Policy: p.Name(), 57 | }) 58 | } 59 | } 60 | 61 | for _, container := range podResource.PodSpec.Containers { 62 | validateContainer(container) 63 | } 64 | 65 | for _, container := range podResource.PodSpec.InitContainers { 66 | validateContainer(container) 67 | } 68 | 69 | return resourceViolations, nil 70 | } 71 | -------------------------------------------------------------------------------- /policies/pod/trusted_repository_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Cruise LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // https://www.apache.org/licenses/LICENSE-2.0 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | package pod 14 | 15 | import ( 16 | "context" 17 | "encoding/json" 18 | "reflect" 19 | "testing" 20 | 21 | "github.com/cruise-automation/k-rail/v3/policies" 22 | admissionv1 "k8s.io/api/admission/v1" 23 | corev1 "k8s.io/api/core/v1" 24 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 25 | "k8s.io/apimachinery/pkg/runtime" 26 | ) 27 | 28 | func TestPolicyTrustedRepository(t *testing.T) { 29 | ctx := context.Background() 30 | 31 | tests := []struct { 32 | name string 33 | podSpec corev1.PodSpec 34 | violations int 35 | }{ 36 | { 37 | name: "allow", 38 | violations: 0, 39 | podSpec: corev1.PodSpec{ 40 | Containers: []corev1.Container{ 41 | { 42 | Image: "alpine@sha256:dad671370a148e9d9573e3e10a9f8cc26ce937bea78f3da80b570c2442364406", 43 | }, 44 | }, 45 | }, 46 | }, 47 | { 48 | name: "deny", 49 | violations: 1, 50 | podSpec: corev1.PodSpec{ 51 | Containers: []corev1.Container{ 52 | { 53 | Image: "dustindecker/alpine:latest", 54 | }, 55 | }, 56 | }, 57 | }, 58 | { 59 | name: "deny", 60 | violations: 1, 61 | podSpec: corev1.PodSpec{ 62 | Containers: []corev1.Container{ 63 | { 64 | Image: "gcr.io/other-gcr-repo/security/k-rail", 65 | }, 66 | }, 67 | }, 68 | }, 69 | { 70 | name: "allow", 71 | violations: 0, 72 | podSpec: corev1.PodSpec{ 73 | Containers: []corev1.Container{ 74 | { 75 | Image: "gcr.io/some-gcr-repo/security/k-rail", 76 | }, 77 | }, 78 | }, 79 | }, 80 | } 81 | for _, tt := range tests { 82 | t.Run(tt.name, func(t *testing.T) { 83 | 84 | raw, _ := json.Marshal(corev1.Pod{Spec: tt.podSpec}) 85 | ar := &admissionv1.AdmissionRequest{ 86 | Namespace: "namespace", 87 | Name: "name", 88 | Object: runtime.RawExtension{Raw: raw}, 89 | Resource: metav1.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"}, 90 | } 91 | 92 | v := PolicyTrustedRepository{} 93 | conf := policies.Config{ 94 | PolicyTrustedRepositoryRegexes: []string{ 95 | "^gcr.io/some-gcr-repo/.*", 96 | "^k8s.gcr.io/.*", // official k8s GCR repo 97 | "^[A-Za-z0-9:@]+$", // official docker hub images 98 | }, 99 | } 100 | if got, _ := v.Validate(ctx, conf, ar); !reflect.DeepEqual(len(got), tt.violations) { 101 | t.Errorf("PolicyTrustedRepository() %s got %v want %v violations", tt.name, len(got), tt.violations) 102 | } 103 | }) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /policies/rolebinding/no_anonymous_role_binding.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Cruise LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // https://www.apache.org/licenses/LICENSE-2.0 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | package rolebinding 14 | 15 | import ( 16 | "context" 17 | "strings" 18 | 19 | admissionv1 "k8s.io/api/admission/v1" 20 | 21 | "github.com/cruise-automation/k-rail/v3/policies" 22 | "github.com/cruise-automation/k-rail/v3/resource" 23 | ) 24 | 25 | type PolicyNoAnonymousRoleBinding struct{} 26 | 27 | func (p PolicyNoAnonymousRoleBinding) Name() string { 28 | return "role_binding_no_anonymous_subject" 29 | } 30 | 31 | func (p PolicyNoAnonymousRoleBinding) Validate(ctx context.Context, config policies.Config, ar *admissionv1.AdmissionRequest) ([]policies.ResourceViolation, []policies.PatchOperation) { 32 | 33 | resourceViolations := []policies.ResourceViolation{} 34 | rbResource := resource.GetRoleBindingResource(ctx, ar) 35 | if rbResource == nil { 36 | return resourceViolations, nil 37 | } 38 | 39 | violationText := "No Anonymous Role Binding: Granting permissions to anonymous or unauthenticated subject is forbidden" 40 | for _, subject := range rbResource.RoleBinding.Subjects { 41 | if (strings.ToLower(subject.Name) == "system:anonymous") || (strings.ToLower(subject.Name) == "system:unauthenticated") { 42 | resourceViolations = append(resourceViolations, policies.ResourceViolation{ 43 | Namespace: ar.Namespace, 44 | ResourceName: rbResource.ResourceName, 45 | ResourceKind: rbResource.ResourceKind, 46 | Violation: violationText, 47 | Policy: p.Name(), 48 | }) 49 | } 50 | } 51 | 52 | return resourceViolations, nil 53 | } 54 | -------------------------------------------------------------------------------- /policies/service/require_service_loadbalancer_exemption.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Cruise LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // https://www.apache.org/licenses/LICENSE-2.0 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License 12 | 13 | package service 14 | 15 | import ( 16 | "context" 17 | "fmt" 18 | "strings" 19 | 20 | admissionv1 "k8s.io/api/admission/v1" 21 | corev1 "k8s.io/api/core/v1" 22 | 23 | "github.com/cruise-automation/k-rail/v3/policies" 24 | "github.com/cruise-automation/k-rail/v3/resource" 25 | ) 26 | 27 | type PolicyRequireServiceLoadbalancerExemption struct{} 28 | 29 | func (p PolicyRequireServiceLoadbalancerExemption) Name() string { 30 | return "service_require_loadbalancer_exemption" 31 | } 32 | 33 | func (p PolicyRequireServiceLoadbalancerExemption) Validate(ctx context.Context, config policies.Config, ar *admissionv1.AdmissionRequest) ([]policies.ResourceViolation, []policies.PatchOperation) { 34 | 35 | resourceViolations := []policies.ResourceViolation{} 36 | 37 | serviceResource := resource.GetServiceResource(ctx, ar) 38 | if serviceResource == nil { 39 | return resourceViolations, nil 40 | } 41 | 42 | // Only Servcices of type LoadBalancers are policed. 43 | if serviceResource.Service.Spec.Type != corev1.ServiceTypeLoadBalancer { 44 | return resourceViolations, nil 45 | } 46 | 47 | annotations := serviceResource.Service.ObjectMeta.GetAnnotations() 48 | 49 | // Each annotation entry in the config is tested sequentially 50 | for _, annotationConfig := range config.PolicyRequireServiceLoadBalancerAnnotations { 51 | annotationsToCheck := annotationConfig.Annotations 52 | if annotationConfig.Annotation != "" { 53 | annotationsToCheck = append(annotationsToCheck, annotationConfig.Annotation) 54 | } 55 | 56 | atLeastOneAnnotationExists := false 57 | 58 | for _, annotation := range annotationsToCheck { 59 | value, exists := annotations[annotation] 60 | atLeastOneAnnotationExists = atLeastOneAnnotationExists || exists 61 | // The annotation is only tested against possible values if it exists 62 | if exists { 63 | valueAllowed := false 64 | for _, allowedValue := range annotationConfig.AllowedValues { 65 | if value == allowedValue { 66 | valueAllowed = true 67 | break 68 | } 69 | } 70 | 71 | if !valueAllowed { 72 | resourceViolations = append(resourceViolations, policies.ResourceViolation{ 73 | Namespace: ar.Namespace, 74 | ResourceName: serviceResource.ResourceName, 75 | ResourceKind: serviceResource.ResourceKind, 76 | Violation: fmt.Sprintf("Require Service LoadBalancer annotations: Annotation %s value %s is not allowed", annotationConfig.Annotation, value), 77 | Policy: p.Name(), 78 | }) 79 | } 80 | } 81 | } 82 | 83 | // If the annotation(s) is not present, the policy config defines if this is acceptable. 84 | if !atLeastOneAnnotationExists && !annotationConfig.AllowMissing { 85 | requiredAnnotations := strings.Join(annotationsToCheck, " or ") 86 | resourceViolations = append(resourceViolations, policies.ResourceViolation{ 87 | Namespace: ar.Namespace, 88 | ResourceName: serviceResource.ResourceName, 89 | ResourceKind: serviceResource.ResourceKind, 90 | Violation: fmt.Sprintf("Require Service LoadBalancer annotations: Annotation %s cannot be empty", requiredAnnotations), 91 | Policy: p.Name(), 92 | }) 93 | } 94 | } 95 | 96 | return resourceViolations, nil 97 | } 98 | -------------------------------------------------------------------------------- /policies/service/service_no_externalip.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Cruise LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // https://www.apache.org/licenses/LICENSE-2.0 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License 12 | 13 | package service 14 | 15 | import ( 16 | "context" 17 | 18 | admissionv1 "k8s.io/api/admission/v1" 19 | 20 | "github.com/cruise-automation/k-rail/v3/policies" 21 | "github.com/cruise-automation/k-rail/v3/resource" 22 | ) 23 | 24 | type PolicyServiceNoExternalIP struct{} 25 | 26 | func (p PolicyServiceNoExternalIP) Name() string { 27 | return "service_no_external_ip" 28 | } 29 | 30 | func (p PolicyServiceNoExternalIP) Validate(ctx context.Context, config policies.Config, ar *admissionv1.AdmissionRequest) ([]policies.ResourceViolation, []policies.PatchOperation) { 31 | 32 | resourceViolations := []policies.ResourceViolation{} 33 | 34 | serviceResource := resource.GetServiceResource(ctx, ar) 35 | if serviceResource == nil { 36 | return resourceViolations, nil 37 | } 38 | 39 | if len(serviceResource.Service.Spec.ExternalIPs) > 0 { 40 | resourceViolations = append(resourceViolations, policies.ResourceViolation{ 41 | Namespace: ar.Namespace, 42 | ResourceName: serviceResource.ResourceName, 43 | ResourceKind: serviceResource.ResourceKind, 44 | Violation: "Services cannot have External IPs provided due to CVE-2020-8554", 45 | Policy: p.Name(), 46 | }) 47 | } 48 | 49 | return resourceViolations, nil 50 | } 51 | -------------------------------------------------------------------------------- /policies/violation.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Cruise LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // https://www.apache.org/licenses/LICENSE-2.0 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | package policies 14 | 15 | import "fmt" 16 | 17 | // ResourceViolation contains information needed to report and track violations, as 18 | // well as checking for exemptions 19 | type ResourceViolation struct { 20 | ResourceName string 21 | ResourceKind string 22 | Namespace string 23 | Violation string 24 | Policy string 25 | Error error 26 | } 27 | 28 | func (r ResourceViolation) HumanString() string { 29 | return fmt.Sprintf("%s %s had violation: %s", r.ResourceKind, r.ResourceName, r.Violation) 30 | } 31 | -------------------------------------------------------------------------------- /policies/virtualservice/require_virtualservice_gateway_exemption.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Cruise LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // https://www.apache.org/licenses/LICENSE-2.0 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License 12 | 13 | package virtualservice 14 | 15 | import ( 16 | "context" 17 | "fmt" 18 | "strings" 19 | 20 | admissionv1 "k8s.io/api/admission/v1" 21 | 22 | "github.com/cruise-automation/k-rail/v3/policies" 23 | "github.com/cruise-automation/k-rail/v3/resource" 24 | ) 25 | 26 | type PolicyRequireVirtualServiceGatewayExemption struct{} 27 | 28 | func (p PolicyRequireVirtualServiceGatewayExemption) Name() string { 29 | return "service_require_virtualservice_gateway_exemption" 30 | } 31 | 32 | var allowedGatewaysSet = map[string]bool{} 33 | 34 | func (p PolicyRequireVirtualServiceGatewayExemption) Validate(ctx context.Context, config policies.Config, ar *admissionv1.AdmissionRequest) ([]policies.ResourceViolation, []policies.PatchOperation) { 35 | 36 | resourceViolations := []policies.ResourceViolation{} 37 | 38 | virtualServiceResource := resource.GetVirtualServiceResource(ctx, ar) 39 | if virtualServiceResource == nil { 40 | return resourceViolations, nil 41 | } 42 | 43 | if config.PolicyRequireVirtualServiceGateways == nil || len(config.PolicyRequireVirtualServiceGateways.AllowedGateways) == 0 { 44 | return resourceViolations, nil 45 | } 46 | 47 | // Memoize the allowedGatewaysSet 48 | if len(allowedGatewaysSet) == 0 { 49 | for _, gateway := range config.PolicyRequireVirtualServiceGateways.AllowedGateways { 50 | allowedGatewaysSet[gateway] = true 51 | } 52 | } 53 | 54 | gateways := virtualServiceResource.VirtualService.Spec.GetGateways() 55 | if len(gateways) == 0 && !config.PolicyRequireVirtualServiceGateways.AllowEmptyGateways { 56 | resourceViolations = append(resourceViolations, policies.ResourceViolation{ 57 | Namespace: ar.Namespace, 58 | ResourceName: virtualServiceResource.ResourceName, 59 | ResourceKind: virtualServiceResource.ResourceKind, 60 | Violation: fmt.Sprintf( 61 | "VirtualService Gateway not specified: Only the following gateways are allowed %s without an exemption", 62 | strings.Join(config.PolicyRequireVirtualServiceGateways.AllowedGateways, ", ")), 63 | Policy: p.Name(), 64 | }) 65 | 66 | } 67 | 68 | for _, gateway := range gateways { 69 | if _, exists := allowedGatewaysSet[gateway]; !exists { 70 | resourceViolations = append(resourceViolations, policies.ResourceViolation{ 71 | Namespace: ar.Namespace, 72 | ResourceName: virtualServiceResource.ResourceName, 73 | ResourceKind: virtualServiceResource.ResourceKind, 74 | Violation: fmt.Sprintf( 75 | "Require VirtualService Gateway: Only the following gateways are allowed %s. Gateway value %s is not allowed without an exemption", 76 | strings.Join(config.PolicyRequireVirtualServiceGateways.AllowedGateways, ", "), 77 | gateway), 78 | Policy: p.Name(), 79 | }) 80 | } 81 | 82 | } 83 | 84 | return resourceViolations, nil 85 | } 86 | -------------------------------------------------------------------------------- /resource/cluster_role_binding.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Cruise LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // https://www.apache.org/licenses/LICENSE-2.0 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | package resource 14 | 15 | import ( 16 | "context" 17 | 18 | admissionv1 "k8s.io/api/admission/v1" 19 | rbacv1 "k8s.io/api/rbac/v1" 20 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 21 | ) 22 | 23 | // ClusterRoleBindingResource contains the information needed for processing by a Policy 24 | type ClusterRoleBindingResource struct { 25 | ClusterRoleBinding rbacv1.ClusterRoleBinding 26 | ResourceName string 27 | ResourceKind string 28 | } 29 | 30 | // GetClusterRoleBindingResource extracts a ClusterRoleBindingResource from an AdmissionRequest 31 | func GetClusterRoleBindingResource(ctx context.Context, ar *admissionv1.AdmissionRequest) *ClusterRoleBindingResource { 32 | c := GetResourceCache(ctx) 33 | return c.getOrSet(cacheKeyClusterRoleBinding, func() interface{} { 34 | return decodeClusterRoleBindingResource(ar) 35 | }).(*ClusterRoleBindingResource) 36 | } 37 | 38 | func decodeClusterRoleBindingResource(ar *admissionv1.AdmissionRequest) *ClusterRoleBindingResource { 39 | switch ar.Kind { 40 | case metav1.GroupVersionKind{Group: "rbac.authorization.k8s.io", Version: "v1", Kind: "ClusterRoleBinding"}: 41 | crb := rbacv1.ClusterRoleBinding{} 42 | if err := decodeObject(ar.Object.Raw, &crb); err != nil { 43 | return nil 44 | } 45 | return &ClusterRoleBindingResource{ 46 | ClusterRoleBinding: crb, 47 | ResourceName: GetResourceName(crb.ObjectMeta), 48 | ResourceKind: "ClusterRoleBinding", 49 | } 50 | default: 51 | return nil 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /resource/context.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Cruise LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // https://www.apache.org/licenses/LICENSE-2.0 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | package resource 14 | 15 | import ( 16 | "context" 17 | "sync" 18 | ) 19 | 20 | type contextKey int // local to the resource module 21 | 22 | const ( 23 | ctxKeyCache contextKey = iota 24 | ) 25 | const ( 26 | cacheKeyPod = iota 27 | cacheKeyPodExec 28 | cacheKeyIngress 29 | cacheKeyService 30 | cacheKeyPersistentVolume 31 | cacheKeyClusterRoleBinding 32 | cacheKeyRoleBinding 33 | cacheKeyPodDisruptionBudget 34 | cacheKeyVirtualService 35 | ) 36 | 37 | type cache struct { 38 | l sync.Mutex 39 | m map[int]interface{} 40 | } 41 | 42 | // getOrSet returns the cached value for the given key. When none exists the passed function is called once to create 43 | // the initial value. When cache is nil no caching happens and the create function is always called. 44 | // Calls are executed thread safe. 45 | func (c *cache) getOrSet(cacheKey int, f func() interface{}) interface{} { 46 | if c == nil { 47 | return f() 48 | } 49 | c.l.Lock() 50 | defer c.l.Unlock() 51 | if p, ok := c.m[cacheKey]; ok { 52 | return p 53 | } 54 | v := f() 55 | c.m[cacheKey] = v 56 | return v 57 | } 58 | 59 | // WithResourceCache adds a resource cache to the context returned. 60 | func WithResourceCache(ctx context.Context) context.Context { 61 | c := &cache{m: make(map[int]interface{}, 1)} 62 | return context.WithValue(ctx, ctxKeyCache, c) 63 | } 64 | 65 | // GetResourceCache returns the cache from the context. Result will return nil when none exists. 66 | func GetResourceCache(ctx context.Context) *cache { 67 | c := ctx.Value(ctxKeyCache) 68 | if c == nil { 69 | return nil 70 | } 71 | return c.(*cache) 72 | } 73 | -------------------------------------------------------------------------------- /resource/context_test.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "context" 5 | "reflect" 6 | "sync" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | func TestResourceCacheContext(t *testing.T) { 12 | const testRunCount = 2 13 | const testCacheKey = 999 14 | specs := map[string]struct { 15 | srcCtx func() context.Context 16 | srcKey int 17 | srcFactoryFunc *creatorMock 18 | expResp interface{} 19 | expCalls int 20 | }{ 21 | "with cache in ctx ": { 22 | srcCtx: func() context.Context { 23 | return WithResourceCache(context.TODO()) 24 | }, 25 | srcKey: testCacheKey, 26 | srcFactoryFunc: &creatorMock{respValue: "myValue"}, 27 | expCalls: 1, 28 | expResp: "myValue", 29 | }, 30 | "with cache filled": { 31 | srcCtx: func() context.Context { 32 | ctx := WithResourceCache(context.TODO()) 33 | GetResourceCache(ctx).getOrSet(testCacheKey, func() interface{} { 34 | return "myValue" 35 | }) 36 | return ctx 37 | }, 38 | srcKey: testCacheKey, 39 | srcFactoryFunc: &creatorMock{respValue: "otherValue"}, 40 | expCalls: 0, 41 | expResp: "myValue", 42 | }, 43 | "with empty ctx": { 44 | srcCtx: context.TODO, 45 | srcKey: testCacheKey, 46 | srcFactoryFunc: &creatorMock{respValue: "foo"}, 47 | expCalls: testRunCount, 48 | expResp: "foo", 49 | }, 50 | } 51 | for msg, spec := range specs { 52 | t.Run(msg, func(t *testing.T) { 53 | ctx := spec.srcCtx() 54 | mock := spec.srcFactoryFunc 55 | for i := 0; i < testRunCount; i++ { 56 | resp := GetResourceCache(ctx).getOrSet(spec.srcKey, mock.CountCall) 57 | if exp, got := spec.expResp, resp; !reflect.DeepEqual(exp, got) { 58 | t.Errorf("expected %v but got %v", exp, got) 59 | } 60 | } 61 | if exp, got := spec.expCalls, mock.called; exp != got { 62 | t.Errorf("expected %d but got %d", exp, got) 63 | } 64 | }) 65 | } 66 | } 67 | 68 | func TestCacheWithConcurrentAccess(t *testing.T) { 69 | const testCacheKey = 999 70 | const actorCount = 10 71 | 72 | var awaitStart sync.WaitGroup 73 | awaitStart.Add(actorCount) 74 | var awaitCompleted sync.WaitGroup 75 | awaitCompleted.Add(actorCount) 76 | 77 | actors := make([]*creatorMock, actorCount) 78 | rsp := make(chan interface{}, actorCount) 79 | 80 | c := &cache{m: make(map[int]interface{}, 1)} 81 | for i := 0; i < actorCount; i++ { 82 | actors[i] = &creatorMock{respValue: i} 83 | go func(i int) { 84 | awaitStart.Done() 85 | awaitStart.Wait() // wait for all actors to start sync 86 | rsp <- c.getOrSet(testCacheKey, actors[i].CountCall) 87 | awaitCompleted.Done() 88 | }(i) 89 | } 90 | awaitCompleted.Wait() 91 | 92 | // then only 1 create function should be called 93 | var active *creatorMock 94 | var expResult int 95 | for i, a := range actors { 96 | if a.called != 0 { 97 | if active != nil { 98 | t.Fatal("more than 1 create function called") 99 | } 100 | active = a 101 | expResult = i 102 | } 103 | } 104 | // and all should see the same result 105 | for i := 0; i < actorCount; i++ { 106 | select { 107 | case r := <-rsp: 108 | if exp, got := expResult, r; exp != got { 109 | t.Errorf("expected %v but got %v", exp, got) 110 | } 111 | case <-time.After(time.Millisecond): 112 | t.Fatal("test timeout") 113 | } 114 | } 115 | } 116 | 117 | type creatorMock struct { 118 | l sync.Mutex 119 | called int 120 | respValue interface{} 121 | } 122 | 123 | func (m *creatorMock) CountCall() interface{} { 124 | m.l.Lock() 125 | m.called++ 126 | m.l.Unlock() 127 | return m.respValue 128 | } 129 | -------------------------------------------------------------------------------- /resource/decoder.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Cruise LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // https://www.apache.org/licenses/LICENSE-2.0 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | package resource 14 | 15 | import ( 16 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 17 | "k8s.io/apimachinery/pkg/runtime" 18 | "k8s.io/apimachinery/pkg/runtime/serializer" 19 | ) 20 | 21 | var ( 22 | runtimeScheme = runtime.NewScheme() 23 | codecs = serializer.NewCodecFactory(runtimeScheme) 24 | ) 25 | 26 | type object interface { 27 | metav1.Object 28 | runtime.Object 29 | } 30 | 31 | func decodeObject(raw []byte, object object) error { 32 | deserializer := codecs.UniversalDeserializer() 33 | if _, _, err := deserializer.Decode(raw, nil, object); err != nil { 34 | return err 35 | } 36 | return nil 37 | } 38 | 39 | // GetResourceName attempts to get the best name for a resource 40 | func GetResourceName(meta metav1.ObjectMeta) (name string) { 41 | // Attempt to get the owner controller's resource name. 42 | // This name is the high level resource that the user is working with. 43 | for _, owner := range meta.OwnerReferences { 44 | if owner.Controller != nil && *owner.Controller == true { 45 | if len(owner.Name) > 0 { 46 | name = owner.Name 47 | return 48 | } 49 | } 50 | } 51 | 52 | // Attempt to get the object's name 53 | if len(meta.Name) > 0 { 54 | name = meta.Name 55 | return 56 | } 57 | 58 | // Attempt to get the name label 59 | if val, ok := meta.Labels["name"]; ok { 60 | name = val 61 | return 62 | } 63 | 64 | return 65 | } 66 | -------------------------------------------------------------------------------- /resource/ingress.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Cruise LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // https://www.apache.org/licenses/LICENSE-2.0 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | package resource 14 | 15 | import ( 16 | "context" 17 | 18 | admissionv1 "k8s.io/api/admission/v1" 19 | extensionsv1beta1 "k8s.io/api/extensions/v1beta1" 20 | networkingv1 "k8s.io/api/networking/v1" 21 | networkingv1beta1 "k8s.io/api/networking/v1beta1" 22 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 23 | ) 24 | 25 | // IngressResource contains the information needed for processing by a Policy 26 | type IngressResource struct { 27 | IngressExt extensionsv1beta1.Ingress 28 | IngressNetV1Beta1 networkingv1beta1.Ingress 29 | IngressNetV1 networkingv1.Ingress 30 | ResourceName string 31 | ResourceKind string 32 | } 33 | 34 | // GetIngressResource extracts and IngressResource from an AdmissionRequest 35 | func GetIngressResource(ctx context.Context, ar *admissionv1.AdmissionRequest) *IngressResource { 36 | c := GetResourceCache(ctx) 37 | return c.getOrSet(cacheKeyIngress, func() interface{} { 38 | return decodeIngressResource(ar) 39 | }).(*IngressResource) 40 | } 41 | 42 | func decodeIngressResource(ar *admissionv1.AdmissionRequest) *IngressResource { 43 | switch ar.Resource { 44 | case metav1.GroupVersionResource{Group: "extensions", Version: "v1beta1", Resource: "ingresses"}: 45 | ing := extensionsv1beta1.Ingress{} 46 | if err := decodeObject(ar.Object.Raw, &ing); err != nil { 47 | return nil 48 | } 49 | return &IngressResource{ 50 | IngressExt: ing, 51 | ResourceName: GetResourceName(ing.ObjectMeta), 52 | ResourceKind: "Ingress", 53 | } 54 | case metav1.GroupVersionResource{Group: "networking.k8s.io", Version: "v1beta1", Resource: "ingresses"}: 55 | ing := networkingv1beta1.Ingress{} 56 | if err := decodeObject(ar.Object.Raw, &ing); err != nil { 57 | return nil 58 | } 59 | return &IngressResource{ 60 | IngressNetV1Beta1: ing, 61 | ResourceName: GetResourceName(ing.ObjectMeta), 62 | ResourceKind: "Ingress", 63 | } 64 | case metav1.GroupVersionResource{Group: "networking.k8s.io", Version: "v1", Resource: "ingresses"}: 65 | ing := networkingv1.Ingress{} 66 | if err := decodeObject(ar.Object.Raw, &ing); err != nil { 67 | return nil 68 | } 69 | return &IngressResource{ 70 | IngressNetV1: ing, 71 | ResourceName: GetResourceName(ing.ObjectMeta), 72 | ResourceKind: "Ingress", 73 | } 74 | default: 75 | return nil 76 | } 77 | } 78 | 79 | // GetAnnotations returns ingress annotations, across all available ingress versions 80 | func (ir IngressResource) GetAnnotations() map[string]string { 81 | if ir.IngressExt.Annotations != nil { 82 | return ir.IngressExt.Annotations 83 | } else if ir.IngressNetV1Beta1.Annotations != nil { 84 | return ir.IngressNetV1Beta1.Annotations 85 | } else if ir.IngressNetV1.Annotations != nil { 86 | return ir.IngressNetV1.Annotations 87 | } 88 | return nil 89 | } 90 | 91 | // GetHosts returns list of all hosts in ingress spec, across all available ingress versions 92 | func (ir IngressResource) GetHosts() []string { 93 | hosts := []string{} 94 | for _, rule := range ir.IngressExt.Spec.Rules { 95 | hosts = append(hosts, rule.Host) 96 | } 97 | for _, rule := range ir.IngressNetV1Beta1.Spec.Rules { 98 | hosts = append(hosts, rule.Host) 99 | } 100 | for _, rule := range ir.IngressNetV1.Spec.Rules { 101 | hosts = append(hosts, rule.Host) 102 | } 103 | return hosts 104 | } 105 | -------------------------------------------------------------------------------- /resource/ingress_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Cruise LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // https://www.apache.org/licenses/LICENSE-2.0 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | package resource 14 | 15 | import ( 16 | "context" 17 | "encoding/json" 18 | "testing" 19 | 20 | "github.com/stretchr/testify/assert" 21 | admissionv1 "k8s.io/api/admission/v1" 22 | extensionsv1beta1 "k8s.io/api/extensions/v1beta1" 23 | networkingv1 "k8s.io/api/networking/v1" 24 | networkingv1beta1 "k8s.io/api/networking/v1beta1" 25 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 26 | "k8s.io/apimachinery/pkg/runtime" 27 | "k8s.io/apimachinery/pkg/runtime/schema" 28 | ) 29 | 30 | func TestGetAnnotations(t *testing.T) { 31 | for _, tt := range []struct { 32 | name string 33 | ingress interface { 34 | GetObjectKind() schema.ObjectKind 35 | } 36 | wantAnnotations map[string]string 37 | }{ 38 | { 39 | ingress: &extensionsv1beta1.Ingress{ 40 | TypeMeta: metav1.TypeMeta{ 41 | APIVersion: "extensions/v1beta1", 42 | }, 43 | ObjectMeta: metav1.ObjectMeta{ 44 | Annotations: map[string]string{ 45 | "foo": "bar", 46 | }, 47 | }, 48 | }, 49 | wantAnnotations: map[string]string{ 50 | "foo": "bar", 51 | }, 52 | }, 53 | { 54 | ingress: &networkingv1beta1.Ingress{ 55 | TypeMeta: metav1.TypeMeta{ 56 | APIVersion: "networking.k8s.io/v1beta1", 57 | }, 58 | ObjectMeta: metav1.ObjectMeta{ 59 | Annotations: map[string]string{ 60 | "foo": "bar", 61 | }, 62 | }, 63 | }, 64 | wantAnnotations: map[string]string{ 65 | "foo": "bar", 66 | }, 67 | }, 68 | { 69 | ingress: &networkingv1.Ingress{ 70 | TypeMeta: metav1.TypeMeta{ 71 | APIVersion: "networking.k8s.io/v1", 72 | }, 73 | ObjectMeta: metav1.ObjectMeta{ 74 | Annotations: map[string]string{ 75 | "foo": "bar", 76 | }, 77 | }, 78 | }, 79 | wantAnnotations: map[string]string{ 80 | "foo": "bar", 81 | }, 82 | }, 83 | } { 84 | tt := tt 85 | t.Run(tt.name, func(t *testing.T) { 86 | raw, _ := json.Marshal(tt.ingress) 87 | ar := &admissionv1.AdmissionRequest{ 88 | Namespace: "namespace", 89 | Name: "name", 90 | Object: runtime.RawExtension{Raw: raw}, 91 | Resource: metav1.GroupVersionResource{ 92 | Group: tt.ingress.GetObjectKind().GroupVersionKind().Group, 93 | Version: tt.ingress.GetObjectKind().GroupVersionKind().Version, 94 | Resource: "ingresses", 95 | }, 96 | } 97 | ingressResource := decodeIngressResource(ar) 98 | assert.Equal(t, tt.wantAnnotations, ingressResource.GetAnnotations()) 99 | }) 100 | } 101 | } 102 | 103 | func TestGetHosts(t *testing.T) { 104 | for _, tt := range []struct { 105 | name string 106 | ingress interface { 107 | GetObjectKind() schema.ObjectKind 108 | } 109 | wantHosts []string 110 | }{ 111 | { 112 | ingress: &extensionsv1beta1.Ingress{ 113 | TypeMeta: metav1.TypeMeta{ 114 | APIVersion: "extensions/v1beta1", 115 | }, 116 | Spec: extensionsv1beta1.IngressSpec{ 117 | Rules: []extensionsv1beta1.IngressRule{ 118 | {Host: "foo"}, 119 | {Host: "bar"}, 120 | }, 121 | }, 122 | }, 123 | wantHosts: []string{"foo", "bar"}, 124 | }, 125 | { 126 | ingress: &networkingv1beta1.Ingress{ 127 | TypeMeta: metav1.TypeMeta{ 128 | APIVersion: "networking.k8s.io/v1beta1", 129 | }, 130 | Spec: networkingv1beta1.IngressSpec{ 131 | Rules: []networkingv1beta1.IngressRule{ 132 | {Host: "foo"}, 133 | {Host: "bar"}, 134 | }, 135 | }, 136 | }, 137 | wantHosts: []string{"foo", "bar"}, 138 | }, 139 | { 140 | ingress: &networkingv1.Ingress{ 141 | TypeMeta: metav1.TypeMeta{ 142 | APIVersion: "networking.k8s.io/v1", 143 | }, 144 | Spec: networkingv1.IngressSpec{ 145 | Rules: []networkingv1.IngressRule{ 146 | {Host: "foo"}, 147 | {Host: "bar"}, 148 | }, 149 | }, 150 | }, 151 | wantHosts: []string{"foo", "bar"}, 152 | }, 153 | } { 154 | tt := tt 155 | t.Run(tt.name, func(t *testing.T) { 156 | raw, _ := json.Marshal(tt.ingress) 157 | ar := &admissionv1.AdmissionRequest{ 158 | Namespace: "namespace", 159 | Name: "name", 160 | Object: runtime.RawExtension{Raw: raw}, 161 | Resource: metav1.GroupVersionResource{ 162 | Group: tt.ingress.GetObjectKind().GroupVersionKind().Group, 163 | Version: tt.ingress.GetObjectKind().GroupVersionKind().Version, 164 | Resource: "ingresses", 165 | }, 166 | } 167 | ingressResource := GetIngressResource(context.Background(), ar) 168 | assert.Equal(t, tt.wantHosts, ingressResource.GetHosts()) 169 | }) 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /resource/persistent_volume.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Cruise LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // https://www.apache.org/licenses/LICENSE-2.0 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | package resource 14 | 15 | import ( 16 | "context" 17 | 18 | admissionv1 "k8s.io/api/admission/v1" 19 | corev1 "k8s.io/api/core/v1" 20 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 21 | ) 22 | 23 | // PersistentVolumeResource contains the information needed for processing by a Policy 24 | type PersistentVolumeResource struct { 25 | PersistentVolume corev1.PersistentVolume 26 | ResourceName string 27 | ResourceKind string 28 | } 29 | 30 | // GetPersistentVolumeResource extracts and PersistentVolumeResource from an AdmissionRequest 31 | func GetPersistentVolumeResource(ctx context.Context, ar *admissionv1.AdmissionRequest) *PersistentVolumeResource { 32 | c := GetResourceCache(ctx) 33 | return c.getOrSet(cacheKeyPersistentVolume, func() interface{} { 34 | return decodePersistentVolumeResource(ar) 35 | }).(*PersistentVolumeResource) 36 | } 37 | 38 | func decodePersistentVolumeResource(ar *admissionv1.AdmissionRequest) *PersistentVolumeResource { 39 | switch ar.Kind { 40 | case metav1.GroupVersionKind{Group: "", Version: "v1", Kind: "PersistentVolume"}: 41 | pv := corev1.PersistentVolume{} 42 | if err := decodeObject(ar.Object.Raw, &pv); err != nil { 43 | return nil 44 | } 45 | return &PersistentVolumeResource{ 46 | PersistentVolume: pv, 47 | ResourceName: GetResourceName(pv.ObjectMeta), 48 | ResourceKind: "PersistentVolume", 49 | } 50 | default: 51 | return nil 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /resource/pod_disruption_budget.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Cruise LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // https://www.apache.org/licenses/LICENSE-2.0 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | package resource 14 | 15 | import ( 16 | "context" 17 | 18 | admissionv1 "k8s.io/api/admission/v1" 19 | policyv1beta1 "k8s.io/api/policy/v1beta1" 20 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 21 | ) 22 | 23 | // PodDisruptionBudgetResource contains the information needed for processing by a Policy 24 | type PodDisruptionBudgetResource struct { 25 | PodDisruptionBudget policyv1beta1.PodDisruptionBudget 26 | ResourceName string 27 | ResourceKind string 28 | } 29 | 30 | // GetPodDisruptionBudgetResource extracts an PodDisruptionBudgetResource from an AdmissionRequest 31 | func GetPodDisruptionBudgetResource(ctx context.Context, ar *admissionv1.AdmissionRequest) *PodDisruptionBudgetResource { 32 | c := GetResourceCache(ctx) 33 | return c.getOrSet(cacheKeyPodDisruptionBudget, func() interface{} { 34 | return decodePodDisruptionBudgetResource(ar) 35 | }).(*PodDisruptionBudgetResource) 36 | } 37 | 38 | func decodePodDisruptionBudgetResource(ar *admissionv1.AdmissionRequest) *PodDisruptionBudgetResource { 39 | switch ar.Resource { 40 | case metav1.GroupVersionResource{Group: "policy", Version: "v1beta1", Resource: "poddisruptionbudgets"}: 41 | pdb := policyv1beta1.PodDisruptionBudget{} 42 | if err := decodeObject(ar.Object.Raw, &pdb); err != nil { 43 | return nil 44 | } 45 | return &PodDisruptionBudgetResource{ 46 | PodDisruptionBudget: pdb, 47 | ResourceName: GetResourceName(pdb.ObjectMeta), 48 | ResourceKind: "PodDisruptionBudget", 49 | } 50 | default: 51 | return nil 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /resource/pod_exec.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Cruise LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // https://www.apache.org/licenses/LICENSE-2.0 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | package resource 14 | 15 | import ( 16 | "context" 17 | "encoding/json" 18 | "strings" 19 | 20 | admissionv1 "k8s.io/api/admission/v1" 21 | corev1 "k8s.io/api/core/v1" 22 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 23 | ) 24 | 25 | // PodExecResource contains the information needed for processing by a Policy 26 | type PodExecResource struct { 27 | Command string 28 | ResourceName string 29 | ResourceKind string 30 | } 31 | 32 | // GetPodExecResource extracts and PodExecResource from an AdmissionRequest 33 | func GetPodExecResource(ctx context.Context, ar *admissionv1.AdmissionRequest) *PodExecResource { 34 | c := GetResourceCache(ctx) 35 | return c.getOrSet(cacheKeyPodExec, func() interface{} { 36 | return decodePodExecResource(ar) 37 | }).(*PodExecResource) 38 | } 39 | 40 | func decodePodExecResource(ar *admissionv1.AdmissionRequest) *PodExecResource { 41 | switch ar.Kind { 42 | case metav1.GroupVersionKind{Group: "", Version: "v1", Kind: "PodExecOptions"}: 43 | podExecOptions := corev1.PodExecOptions{} 44 | if err := json.Unmarshal(ar.Object.Raw, &podExecOptions); err != nil { 45 | return nil 46 | } 47 | return &PodExecResource{ 48 | Command: strings.Join(podExecOptions.Command, " "), 49 | ResourceName: ar.Name, 50 | ResourceKind: "PodExec", 51 | } 52 | default: 53 | return nil 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /resource/pod_test.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | admissionv1 "k8s.io/api/admission/v1" 8 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 9 | "k8s.io/apimachinery/pkg/runtime" 10 | ) 11 | 12 | func BenchmarkDecodePodWithoutCaching(b *testing.B) { 13 | req := fakeReq([]byte(podExample)) 14 | ctx := context.TODO() 15 | b.ResetTimer() 16 | for i := 0; i < b.N; i++ { 17 | _ = GetPodResource(ctx, req) 18 | } 19 | } 20 | 21 | func BenchmarkDecodePodCaching(b *testing.B) { 22 | req := fakeReq([]byte(podExample)) 23 | ctx := WithResourceCache(context.TODO()) 24 | b.ResetTimer() 25 | for i := 0; i < b.N; i++ { 26 | _ = GetPodResource(ctx, req) 27 | } 28 | } 29 | 30 | func TestWithPodExec(t *testing.T) { 31 | req := fakePodExecReq(nil) 32 | ctx := WithResourceCache(context.TODO()) 33 | res := GetPodResource(ctx, req) 34 | if res != nil { 35 | t.Fatal("should have gotten nil for pod exec request") 36 | } 37 | } 38 | 39 | func fakePodExecReq(b []byte) *admissionv1.AdmissionRequest { 40 | return &admissionv1.AdmissionRequest{ 41 | Kind: metav1.GroupVersionKind{Group: "", Version: "v1", Kind: "PodExecOptions"}, 42 | Name: "any", 43 | Namespace: "test", 44 | Object: runtime.RawExtension{Raw: b}, 45 | } 46 | } 47 | 48 | func fakeReq(b []byte) *admissionv1.AdmissionRequest { 49 | return &admissionv1.AdmissionRequest{ 50 | Resource: metav1.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"}, 51 | Name: "any", 52 | Namespace: "test", 53 | Object: runtime.RawExtension{Raw: b}, 54 | } 55 | } 56 | 57 | const podExample = ` 58 | kind: Pod 59 | apiVersion: apps/v1 60 | metadata: 61 | name: foobar 62 | namespace: testing 63 | annotations: 64 | created-by: alpe 65 | labels: 66 | app: foo 67 | spec: 68 | containers: 69 | - name: foo 70 | image: v0.1.0 71 | ports: 72 | - name: http 73 | containerPort: 8080 74 | ` 75 | -------------------------------------------------------------------------------- /resource/role_binding.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Cruise LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // https://www.apache.org/licenses/LICENSE-2.0 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | package resource 14 | 15 | import ( 16 | "context" 17 | 18 | admissionv1 "k8s.io/api/admission/v1" 19 | rbacv1 "k8s.io/api/rbac/v1" 20 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 21 | ) 22 | 23 | // RoleBindingResource contains the information needed for processing by a Policy 24 | type RoleBindingResource struct { 25 | RoleBinding rbacv1.RoleBinding 26 | ResourceName string 27 | ResourceKind string 28 | } 29 | 30 | // GetRoleBindingResource extracts a RoleBindingResource from an AdmissionRequest 31 | func GetRoleBindingResource(ctx context.Context, ar *admissionv1.AdmissionRequest) *RoleBindingResource { 32 | c := GetResourceCache(ctx) 33 | return c.getOrSet(cacheKeyRoleBinding, func() interface{} { 34 | return decodeRoleBindingResource(ar) 35 | }).(*RoleBindingResource) 36 | } 37 | 38 | func decodeRoleBindingResource(ar *admissionv1.AdmissionRequest) *RoleBindingResource { 39 | switch ar.Kind { 40 | case metav1.GroupVersionKind{Group: "rbac.authorization.k8s.io", Version: "v1", Kind: "RoleBinding"}: 41 | rb := rbacv1.RoleBinding{} 42 | if err := decodeObject(ar.Object.Raw, &rb); err != nil { 43 | return nil 44 | } 45 | return &RoleBindingResource{ 46 | RoleBinding: rb, 47 | ResourceName: GetResourceName(rb.ObjectMeta), 48 | ResourceKind: "RoleBinding", 49 | } 50 | default: 51 | return nil 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /resource/service.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Cruise LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // https://www.apache.org/licenses/LICENSE-2.0 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | package resource 14 | 15 | import ( 16 | "context" 17 | 18 | admissionv1 "k8s.io/api/admission/v1" 19 | corev1 "k8s.io/api/core/v1" 20 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 21 | ) 22 | 23 | // ServiceResource contains the information needed for processing by a Policy 24 | type ServiceResource struct { 25 | Service corev1.Service 26 | ResourceName string 27 | ResourceKind string 28 | } 29 | 30 | // GetServiceResource extracts and ServiceResource from an AdmissionRequest 31 | func GetServiceResource(ctx context.Context, ar *admissionv1.AdmissionRequest) *ServiceResource { 32 | c := GetResourceCache(ctx) 33 | return c.getOrSet(cacheKeyService, func() interface{} { 34 | return decodeServiceResource(ar) 35 | }).(*ServiceResource) 36 | } 37 | 38 | func decodeServiceResource(ar *admissionv1.AdmissionRequest) *ServiceResource { 39 | switch ar.Resource { 40 | case metav1.GroupVersionResource{Group: "", Version: "v1", Resource: "services"}: 41 | svc := corev1.Service{} 42 | if err := decodeObject(ar.Object.Raw, &svc); err != nil { 43 | return nil 44 | } 45 | return &ServiceResource{ 46 | Service: svc, 47 | ResourceName: GetResourceName(svc.ObjectMeta), 48 | ResourceKind: "Service", 49 | } 50 | default: 51 | return nil 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /resource/virtual_service.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Cruise LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // https://www.apache.org/licenses/LICENSE-2.0 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | package resource 14 | 15 | import ( 16 | "context" 17 | 18 | networkingistiov1beta1 "istio.io/api/networking/v1beta1" 19 | admissionv1 "k8s.io/api/admission/v1" 20 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 21 | runtime "k8s.io/apimachinery/pkg/runtime" 22 | ) 23 | 24 | type VirtualService struct { 25 | metav1.TypeMeta `json:",inline"` 26 | // Standard object's metadata. 27 | // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata 28 | // +optional 29 | metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` 30 | 31 | // Spec defines the behavior of a service. 32 | // https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status 33 | // +optional 34 | Spec networkingistiov1beta1.VirtualService `json:"spec,omitempty" protobuf:"bytes,2,opt,name=spec"` 35 | } 36 | 37 | // DeepCopyInto is a deepcopy function, copying the receiver, writing into out. in must be non-nil. 38 | func (in *VirtualService) DeepCopyInto(out *VirtualService) { 39 | *out = *in 40 | out.TypeMeta = in.TypeMeta 41 | in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) 42 | in.Spec.DeepCopyInto(&out.Spec) 43 | return 44 | } 45 | 46 | // DeepCopy is a deepcopy function, copying the receiver, creating a new Service. 47 | func (in *VirtualService) DeepCopy() *VirtualService { 48 | if in == nil { 49 | return nil 50 | } 51 | out := new(VirtualService) 52 | in.DeepCopyInto(out) 53 | return out 54 | } 55 | 56 | // DeepCopyObject is a deepcopy function, copying the receiver, creating a new runtime.Object. 57 | func (in *VirtualService) DeepCopyObject() runtime.Object { 58 | if c := in.DeepCopy(); c != nil { 59 | return c 60 | } 61 | return nil 62 | } 63 | 64 | // ServiceResource contains the information needed for processing by a Policy 65 | type VirtualServiceResource struct { 66 | VirtualService VirtualService 67 | ResourceName string 68 | ResourceKind string 69 | } 70 | 71 | // GetVirtualServiceResource extracts a VirtualServiceResource from an AdmissionRequest 72 | func GetVirtualServiceResource(ctx context.Context, ar *admissionv1.AdmissionRequest) *VirtualServiceResource { 73 | c := GetResourceCache(ctx) 74 | return c.getOrSet(cacheKeyVirtualService, func() interface{} { 75 | return decodeVirtualServiceResource(ar) 76 | }).(*VirtualServiceResource) 77 | } 78 | 79 | func decodeVirtualServiceResource(ar *admissionv1.AdmissionRequest) *VirtualServiceResource { 80 | switch ar.Resource { 81 | case metav1.GroupVersionResource{Group: "networking.istio.io", Version: "v1beta1", Resource: "virtualservices"}, metav1.GroupVersionResource{Group: "networking.istio.io", Version: "v1alpha3", Resource: "virtualservices"}: 82 | vsvc := VirtualService{} 83 | if err := decodeObject(ar.Object.Raw, &vsvc); err != nil { 84 | return nil 85 | } 86 | return &VirtualServiceResource{ 87 | VirtualService: vsvc, 88 | ResourceName: GetResourceName(vsvc.ObjectMeta), 89 | ResourceKind: "VirtualService", 90 | } 91 | default: 92 | return nil 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /server/config.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Cruise LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // https://www.apache.org/licenses/LICENSE-2.0 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | package server 14 | 15 | import ( 16 | "github.com/cruise-automation/k-rail/v3/policies" 17 | ) 18 | 19 | type PolicySettings struct { 20 | Name string 21 | Enabled bool 22 | ReportOnly bool `json:"report_only"` 23 | } 24 | 25 | type Config struct { 26 | LogLevel string `json:"log_level"` 27 | ClusterName string `json:"cluster_name"` 28 | BlacklistedNamespaces []string `json:"blacklisted_namespaces"` 29 | TLS struct { 30 | Cert string 31 | Key string 32 | } 33 | GlobalReportOnly bool `json:"global_report_only"` 34 | GlobalMetricsEnabled bool `json:"global_metrics_enabled"` 35 | Policies []PolicySettings 36 | PolicyConfig policies.Config `json:"policy_config"` 37 | PluginConfig map[string]interface{} `json:"plugin_config"` 38 | } 39 | -------------------------------------------------------------------------------- /server/metrics.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Cruise LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // https://www.apache.org/licenses/LICENSE-2.0 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | package server 14 | 15 | import ( 16 | "github.com/prometheus/client_golang/prometheus" 17 | metrics "github.com/slok/go-http-metrics/metrics/prometheus" 18 | "github.com/slok/go-http-metrics/middleware" 19 | ) 20 | 21 | var prometheusMiddleware = middleware.New(middleware.Config{ 22 | Recorder: metrics.NewRecorder(metrics.Config{}), 23 | }) 24 | 25 | var totalRegisteredPolicies = prometheus.NewCounter( 26 | prometheus.CounterOpts{ 27 | Namespace: "krail", 28 | Name: "total_registered_policies", 29 | Help: "Total Policies Registered", 30 | }, 31 | ) 32 | 33 | var totalLoadedPlugins = prometheus.NewCounter( 34 | prometheus.CounterOpts{ 35 | Namespace: "krail", 36 | Name: "total_loaded_plugins", 37 | Help: "Total Plugins Loaded", 38 | }, 39 | ) 40 | 41 | var policyViolations = prometheus.NewCounterVec( 42 | prometheus.CounterOpts{ 43 | Namespace: "krail", 44 | Name: "policy_violations", 45 | Help: "Count of Violations", 46 | }, 47 | []string{"resource", "namespace", "policy", "enforced"}, 48 | ) 49 | -------------------------------------------------------------------------------- /server/server_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Cruise LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // https://www.apache.org/licenses/LICENSE-2.0 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | package server 14 | 15 | import ( 16 | "io/ioutil" 17 | "net/http" 18 | "net/http/httptest" 19 | "testing" 20 | 21 | "github.com/stretchr/testify/assert" 22 | "github.com/stretchr/testify/require" 23 | ) 24 | 25 | func testPrometheusMetrics(t *testing.T, h http.Handler, expMetrics []string) { 26 | require := require.New(t) 27 | assert := assert.New(t) 28 | 29 | // Setup server. 30 | server := httptest.NewServer(h) 31 | t.Cleanup(func() { server.Close() }) 32 | 33 | // Get metrics. 34 | r, err := http.NewRequest(http.MethodGet, server.URL+"/metrics", nil) 35 | require.NoError(err) 36 | resp, err := http.DefaultClient.Do(r) 37 | require.NoError(err) 38 | 39 | // Check. 40 | b, err := ioutil.ReadAll(resp.Body) 41 | require.NoError(err) 42 | metrics := string(b) 43 | 44 | assert.Contains(metrics, "go_") 45 | assert.Contains(metrics, "go_gc_") 46 | assert.Contains(metrics, "http_") 47 | assert.Contains(metrics, "krail_") 48 | assert.Contains(metrics, "promhttp_metric_handler") 49 | } 50 | -------------------------------------------------------------------------------- /server/webhook_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Cruise LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // https://www.apache.org/licenses/LICENSE-2.0 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | package server 14 | 15 | import ( 16 | "encoding/json" 17 | "testing" 18 | 19 | log "github.com/sirupsen/logrus" 20 | 21 | "github.com/cruise-automation/k-rail/v3/policies" 22 | admissionv1 "k8s.io/api/admission/v1" 23 | authenticationv1 "k8s.io/api/authentication/v1" 24 | corev1 "k8s.io/api/core/v1" 25 | apiresource "k8s.io/apimachinery/pkg/api/resource" 26 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 27 | "k8s.io/apimachinery/pkg/runtime" 28 | ) 29 | 30 | type test struct { 31 | name string 32 | resourceName string 33 | resourceNamespace string 34 | podSpec corev1.PodSpec 35 | allow bool 36 | } 37 | 38 | func test_setup() (Server, []test) { 39 | rawExemptions := []policies.RawExemption{ 40 | { 41 | ResourceName: "test-resource", 42 | Namespace: "test-namespace", 43 | ExemptPolicies: []string{"*"}, 44 | }, 45 | } 46 | var compiledExemptions []policies.CompiledExemption 47 | for _, e := range rawExemptions { 48 | compiledExemptions = append(compiledExemptions, e.Compile()) 49 | } 50 | 51 | testSrv := Server{ 52 | Config: Config{ 53 | Policies: []PolicySettings{ 54 | { 55 | Name: "pod_no_host_network", 56 | Enabled: true, 57 | ReportOnly: false, 58 | }, 59 | }, 60 | PolicyConfig: policies.Config{ 61 | MutateEmptyDirSizeLimit: policies.MutateEmptyDirSizeLimit{ 62 | MaximumSizeLimit: apiresource.MustParse("2Gi"), 63 | DefaultSizeLimit: apiresource.MustParse("1Gi"), 64 | }, 65 | }, 66 | }, 67 | Exemptions: compiledExemptions, 68 | } 69 | 70 | log.SetLevel(log.ErrorLevel) 71 | 72 | testSrv.registerPolicies() 73 | 74 | tests := []test{ 75 | { 76 | name: "deny by policy", 77 | allow: false, 78 | podSpec: corev1.PodSpec{ 79 | HostNetwork: true, 80 | }, 81 | }, 82 | { 83 | name: "allow by policy", 84 | allow: true, 85 | podSpec: corev1.PodSpec{ 86 | HostNetwork: false, 87 | }, 88 | }, 89 | { 90 | name: "allow by name exemption", 91 | allow: true, 92 | resourceNamespace: "test-namespace", 93 | resourceName: "test-resource-lol", 94 | podSpec: corev1.PodSpec{ 95 | HostNetwork: true, 96 | }, 97 | }, 98 | } 99 | 100 | return testSrv, tests 101 | } 102 | 103 | func runBenchmark(b *testing.B, testSrv Server, tt test) { 104 | pod := corev1.Pod{Spec: tt.podSpec} 105 | pod.Name = tt.resourceName 106 | raw, _ := json.Marshal(pod) 107 | ar := admissionv1.AdmissionReview{ 108 | Request: &admissionv1.AdmissionRequest{ 109 | Namespace: tt.resourceNamespace, 110 | Object: runtime.RawExtension{Raw: raw}, 111 | Resource: metav1.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"}, 112 | UserInfo: authenticationv1.UserInfo{Groups: []string{"group1"}}, 113 | }, 114 | } 115 | 116 | if got := testSrv.validateResources(ar); got.Response.Allowed != tt.allow { 117 | b.Errorf("Server.validateResources() = %v, want %v", got.Response.Allowed, tt.allow) 118 | } 119 | } 120 | 121 | func BenchmarkServer_DenyPolicy(b *testing.B) { 122 | testSrv, tests := test_setup() 123 | tt := tests[0] 124 | b.RunParallel(func(pb *testing.PB) { 125 | for pb.Next() { 126 | runBenchmark(b, testSrv, tt) 127 | } 128 | }) 129 | } 130 | 131 | func BenchmarkServer_AllowPolicy(b *testing.B) { 132 | testSrv, tests := test_setup() 133 | tt := tests[1] 134 | b.RunParallel(func(pb *testing.PB) { 135 | for pb.Next() { 136 | runBenchmark(b, testSrv, tt) 137 | } 138 | }) 139 | } 140 | 141 | func BenchmarkServer_AllowExemption(b *testing.B) { 142 | testSrv, tests := test_setup() 143 | tt := tests[2] 144 | b.RunParallel(func(pb *testing.PB) { 145 | for pb.Next() { 146 | runBenchmark(b, testSrv, tt) 147 | } 148 | }) 149 | } 150 | 151 | func TestServer_validateResources(t *testing.T) { 152 | testSrv, tests := test_setup() 153 | 154 | for _, tt := range tests { 155 | t.Run(tt.name, func(t *testing.T) { 156 | pod := corev1.Pod{Spec: tt.podSpec} 157 | pod.Name = tt.resourceName 158 | raw, _ := json.Marshal(pod) 159 | ar := admissionv1.AdmissionReview{ 160 | Request: &admissionv1.AdmissionRequest{ 161 | Namespace: tt.resourceNamespace, 162 | Object: runtime.RawExtension{Raw: raw}, 163 | Resource: metav1.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"}, 164 | UserInfo: authenticationv1.UserInfo{Groups: []string{"group1"}}, 165 | }, 166 | } 167 | 168 | if got := testSrv.validateResources(ar); got.Response.Allowed != tt.allow { 169 | t.Errorf("Server.validateResources() = %v, want %v", got.Response.Allowed, tt.allow) 170 | } 171 | }) 172 | } 173 | } 174 | --------------------------------------------------------------------------------