├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── krew-plugin-release.yml ├── .gitignore ├── .goreleaser.yml ├── .krew.yaml ├── Jenkinsfile ├── LICENSE ├── Makefile ├── README.MD ├── advisor ├── advisor.go ├── processor │ ├── generate.go │ ├── generate_test.go │ └── get.go ├── report │ └── report.go └── types │ ├── lintreport.go │ ├── lintreport_test.go │ ├── portrange.go │ ├── portrange_test.go │ ├── pspgrant.go │ └── securityspec.go ├── comparator └── comparator.go ├── container └── Dockerfile ├── examples ├── README.MD ├── ns.yaml ├── pods-allow.yaml ├── pods-deny.yaml ├── pods.yaml ├── roles.yaml └── sa.yaml ├── generator ├── generator.go └── generator_test.go ├── go.mod ├── go.sum ├── kube-psp-advisor.go ├── kube-psp-advisor_test.go ├── scripts ├── build ├── example └── test ├── test-yaml ├── base-busybox.yaml ├── psp-grant.yaml ├── srcYamls │ ├── busy-box.yaml │ └── nginx.yaml ├── subpath.yaml ├── targetYamls │ ├── busy-box.yaml │ ├── nginx.yaml │ └── web-deployment.yaml └── test-opa.yaml ├── utils ├── utils.go ├── version.go └── version_test.go └── version /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/krew-plugin-release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | on: 3 | push: 4 | tags: 5 | - 'v*.*.*' 6 | jobs: 7 | goreleaser: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@master 12 | - name: Setup Go 13 | uses: actions/setup-go@v1 14 | with: 15 | go-version: 1.16 16 | - name: GoReleaser 17 | uses: goreleaser/goreleaser-action@v1 18 | with: 19 | version: latest 20 | args: release --rm-dist 21 | env: 22 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 23 | - name: Update new version in krew-index 24 | uses: rajatjindal/krew-release-bot@v0.0.38 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor* 2 | .idea* 3 | kube-psp-advisor 4 | advise-psp 5 | psp.yaml 6 | *.tar.gz 7 | .DS_Store 8 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | before: 2 | hooks: 3 | - go mod download 4 | builds: 5 | - id: kubectl-advise-psp 6 | main: ./ 7 | binary: kubectl-advise-psp 8 | env: 9 | - CGO_ENABLED=0 10 | goos: 11 | - linux 12 | - darwin 13 | goarch: 14 | - amd64 15 | - arm64 16 | 17 | archives: 18 | - builds: 19 | - kubectl-advise-psp 20 | name_template: "{{ .ProjectName }}_{{ .Tag }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}" 21 | wrap_in_directory: false 22 | format: tar.gz 23 | files: 24 | - LICENSE 25 | -------------------------------------------------------------------------------- /.krew.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: krew.googlecontainertools.github.com/v1alpha2 2 | kind: Plugin 3 | metadata: 4 | name: advise-psp 5 | spec: 6 | version: {{ .TagName }} 7 | homepage: https://github.com/sysdiglabs/kube-psp-advisor 8 | platforms: 9 | - selector: 10 | matchLabels: 11 | os: darwin 12 | arch: amd64 13 | {{addURIAndSha "https://github.com/sysdiglabs/kube-psp-advisor/releases/download/{{ .TagName }}/kube-psp-advisor_{{ .TagName }}_darwin_amd64.tar.gz" .TagName }} 14 | bin: kubectl-advise-psp 15 | - selector: 16 | matchLabels: 17 | os: linux 18 | arch: amd64 19 | {{addURIAndSha "https://github.com/sysdiglabs/kube-psp-advisor/releases/download/{{ .TagName }}/kube-psp-advisor_{{ .TagName }}_linux_amd64.tar.gz" .TagName }} 20 | bin: kubectl-advise-psp 21 | shortDescription: Suggests PodSecurityPolicies for cluster. 22 | description: | 23 | Suggests PSPs based on the required capabilities of the currently running 24 | workloads or a given manifest. 25 | -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | pipeline { 2 | agent { 3 | label 'builder-backend-j8' 4 | } 5 | 6 | stages { 7 | stage("Build") { 8 | steps { 9 | dir(".") { 10 | script { 11 | sh "ls && pwd && echo $PWD && docker run --rm -v \$(pwd):/usr/src/myapp -w /usr/src/myapp golang:1.13 make build" 12 | } 13 | } 14 | } 15 | } 16 | stage("Tests") { 17 | steps { 18 | dir(".") { 19 | script { 20 | sh "docker run --rm -v \$(pwd):/usr/src/myapp -w /usr/src/myapp golang:1.13 go test ./..." 21 | } 22 | } 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all 2 | 3 | all: build test 4 | IMG="sysdig/kube-psp-advisor" 5 | VERSION=$(shell cat version) 6 | 7 | test: 8 | @echo "+ $@" 9 | ./scripts/test 10 | unit-test: 11 | @echo "+ $@" 12 | go test ./... 13 | example: 14 | @echo "+ $@" 15 | ./scripts/example 16 | build: 17 | @echo "+ $@" 18 | ./scripts/build 19 | build-image: 20 | @echo "+ $@" 21 | docker build -f container/Dockerfile -t ${IMG}:${VERSION} . 22 | push-image: 23 | @echo "+ $@" 24 | docker push ${IMG}:${VERSION} 25 | docker tag ${IMG}:${VERSION} ${IMG}:latest 26 | docker push ${IMG}:latest 27 | -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | # Kube PodSecurityPolicy Advisor 2 | 3 | kube-psp-advisor is a tool that makes it easier to create K8s Pod Security Policies (PSPs) or OPA Policy from either a live K8s environment or from a single .yaml file containing a pod specification (Deployment, DaemonSet, Pod, etc). 4 | 5 | It has 2 subcommands, `kube-psp-advisor inspect` and `kube-psp-advisor convert`. `inspect` connects to a K8s API server, scans the security context of workloads in a given namespace or the entire cluster, and generates a PSP or an OPA Policy based on the security context. `convert` works without connecting to an API Server, reading a single .yaml file containing a object with a pod spec and generating a PSP or OPA Policy based on the file. 6 | 7 | ## Installation as a Krew Plugin 8 | 9 | Follow the [instructions](https://github.com/kubernetes-sigs/krew#installation) to install `krew`. Then run the following command: 10 | 11 | ``` 12 | kubectl krew install advise-psp 13 | ``` 14 | 15 | The plugin will be available as `kubectl advise-psp`. 16 | 17 | ## Build and Run locally 18 | 1. ```make build``` 19 | 2. ```./kube-psp-advisor inspect``` to generate Pod Security Policy based on running cluster configuration 20 | - 2.1 ```./kube-psp-advisor inspect --report``` to print the details reports (why this PSP is recommended for the cluster) 21 | - 2.2 ```./kube-psp-advisor inspect --grant``` to print PSPs, roles and rolebindings for service accounts (refer to [psp-grant.yaml](./test-yaml/psp-grant.yaml)) 22 | - 2.3 ```./kube-psp-advisor inspect --namespace=``` to print report or PSP(s) within a given namespace (default to all) 23 | - 2.4 ```./kube-psp-advisor inspect --policy opa``` to generate OPA Policy based on running cluster configuration 24 | - 2.5 ```./kube-psp-advisor inspect --policy opa --deny-by-default``` to generate an OPA Policy, where OPA Default Rule is Deny ALL 25 | 4. ```./kube-psp-advisor convert --podFile --pspFile ``` to generate a PSP from a single .yaml file. 26 | - 4.1 ```./kube-psp-advisor convert --podFile --pspFile --opa``` to generate an OPA Policy from a single .yaml file. 27 | - 4.2 ```./kube-psp-advisor convert --podFile --pspFile --opa --deny-by-default``` to generate an OPA Policy from a single .yaml file, where OPA Default Rule is Deny ALL. 28 | 29 | ## Build and Run as Container 30 | 1. ```docker build -t -f container/Dockerfile .``` 31 | 2. ```docker run -v ~/.kube:/root/.kube -v ~/.aws:/root/.aws ``` (the `.aws` folder mount is optional and totally depends on your clould provider) 32 | 33 | ## Use Cases 34 | 1. Help verify the deployment, daemonset settings in cluster and plan to reduce unnecessary privileges/resources 35 | 2. Apply Pod Security Policy or OPA policy to the target cluster 36 | 3. Using flag `--namespace=` with `--report` to debug and narrow down the security context per namespace 37 | 38 | ## Attributes Aggregated for Pod Security Policy 39 | - allowPrivilegeEscalation 40 | - allowedCapabilities 41 | - allowedHostPaths 42 | - readOnly 43 | - hostIPC 44 | - hostNetwork 45 | - hostPID 46 | - privileged 47 | - readOnlyRootFilesystem 48 | - runAsUser 49 | - runAsGroup 50 | - Volume 51 | - hostPorts 52 | - allowedUnsafeSysctls 53 | 54 | ## Limitations 55 | Some attributes(e.g. capabilities) required gathering runtime information in order to provide the followings: 56 | - Least privilege (capabilities captured from runtime) 57 | 58 | ## High-level todo list 59 | 60 | - [x] Basic functionalities; 61 | - [ ] Create PSP's for common charts 62 | - [x] Kubectl plugin 63 | 64 | ## Sample Pod Security Policy 65 | Command: `./kube-psp-advisor inspect --namespace=psp-test` 66 | ``` 67 | apiVersion: policy/v1beta1 68 | kind: PodSecurityPolicy 69 | metadata: 70 | creationTimestamp: null 71 | name: pod-security-policy-20181130114734 72 | spec: 73 | allowedCapabilities: 74 | - SYS_ADMIN 75 | - NET_ADMIN 76 | allowedHostPaths: 77 | - pathPrefix: /bin 78 | - pathPrefix: /tmp 79 | - pathPrefix: /usr/sbin 80 | - pathPrefix: /usr/bin 81 | fsGroup: 82 | rule: RunAsAny 83 | hostIPC: false 84 | hostNetwork: false 85 | hostPID: false 86 | privileged: true 87 | runAsUser: 88 | rule: RunAsAny 89 | seLinux: 90 | rule: RunAsAny 91 | supplementalGroups: 92 | rule: RunAsAny 93 | volumes: 94 | - hostPath 95 | - configMap 96 | - secret 97 | ``` 98 | 99 | ## Sample Report 100 | Command: `./kube-psp-advisor inspect --namespace=psp-test --report | jq .podSecuritySpecs` 101 | ``` 102 | { 103 | "hostIPC": [ 104 | { 105 | "metadata": { 106 | "name": "busy-rs", 107 | "kind": "ReplicaSet" 108 | }, 109 | "namespace": "psp-test", 110 | "hostPID": true, 111 | "hostNetwork": true, 112 | "hostIPC": true, 113 | "volumeTypes": [ 114 | "configMap" 115 | ] 116 | }, 117 | { 118 | "metadata": { 119 | "name": "busy-job", 120 | "kind": "Job" 121 | }, 122 | "namespace": "psp-test", 123 | "hostIPC": true, 124 | "volumeTypes": [ 125 | "hostPath" 126 | ], 127 | "mountedHostPath": [ 128 | "/usr/bin" 129 | ] 130 | } 131 | ], 132 | "hostNetwork": [ 133 | { 134 | "metadata": { 135 | "name": "busy-rs", 136 | "kind": "ReplicaSet" 137 | }, 138 | "namespace": "psp-test", 139 | "hostPID": true, 140 | "hostNetwork": true, 141 | "hostIPC": true, 142 | "volumeTypes": [ 143 | "configMap" 144 | ] 145 | }, 146 | { 147 | "metadata": { 148 | "name": "busy-pod", 149 | "kind": "Pod" 150 | }, 151 | "namespace": "psp-test", 152 | "hostNetwork": true, 153 | "volumeTypes": [ 154 | "hostPath", 155 | "secret" 156 | ], 157 | "mountedHostPath": [ 158 | "/usr/bin" 159 | ] 160 | } 161 | ], 162 | "hostPID": [ 163 | { 164 | "metadata": { 165 | "name": "busy-deploy", 166 | "kind": "Deployment" 167 | }, 168 | "namespace": "psp-test", 169 | "hostPID": true, 170 | "volumeTypes": [ 171 | "hostPath" 172 | ], 173 | "mountedHostPath": [ 174 | "/tmp" 175 | ] 176 | }, 177 | { 178 | "metadata": { 179 | "name": "busy-rs", 180 | "kind": "ReplicaSet" 181 | }, 182 | "namespace": "psp-test", 183 | "hostPID": true, 184 | "hostMetwork": true, 185 | "hostIPC": true, 186 | "volumeTypes": [ 187 | "configMap" 188 | ] 189 | } 190 | ] 191 | } 192 | ``` 193 | 194 | ## Commercial 195 | Generating PSPs based on runtime activity, simulating PSPs and managing different PSPs across Kubernetes namespaces can simplify the life of every Kubernetes operator. 196 | Check out how Sysdig Secure can help - https://sysdig.com/blog/psp-in-production/ 197 | -------------------------------------------------------------------------------- /advisor/advisor.go: -------------------------------------------------------------------------------- 1 | package advisor 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/open-policy-agent/opa/ast" 9 | 10 | "github.com/sysdiglabs/kube-psp-advisor/advisor/types" 11 | 12 | "github.com/sysdiglabs/kube-psp-advisor/advisor/processor" 13 | "github.com/sysdiglabs/kube-psp-advisor/advisor/report" 14 | 15 | "k8s.io/api/policy/v1beta1" 16 | k8sJSON "k8s.io/apimachinery/pkg/runtime/serializer/json" 17 | "k8s.io/client-go/kubernetes" 18 | _ "k8s.io/client-go/plugin/pkg/client/auth/gcp" 19 | ) 20 | 21 | type Advisor struct { 22 | podSecurityPolicy *v1beta1.PodSecurityPolicy 23 | OPAModulePolicy *ast.Module 24 | k8sClient *kubernetes.Clientset 25 | processor *processor.Processor 26 | report *report.Report 27 | grants []types.PSPGrant 28 | grantWarnings string 29 | } 30 | 31 | // Create an podSecurityPolicy advisor instance 32 | func NewAdvisor(kubeconfig string) (*Advisor, error) { 33 | p, err := processor.NewProcessor(kubeconfig) 34 | 35 | if err != nil { 36 | return nil, err 37 | } 38 | 39 | return &Advisor{ 40 | podSecurityPolicy: nil, 41 | processor: p, 42 | report: nil, 43 | grants: []types.PSPGrant{}, 44 | }, nil 45 | } 46 | 47 | func (advisor *Advisor) Process(namespace string, excludeNamespaces []string, OPAformat string, OPAdefaultRule bool) error { 48 | advisor.processor.SetNamespace(namespace) 49 | advisor.processor.SetExcludeNamespaces(excludeNamespaces) 50 | 51 | cssList, pssList, err := advisor.processor.GetSecuritySpec() 52 | 53 | if err != nil { 54 | return err 55 | } 56 | 57 | if OPAformat == "opa" { 58 | advisor.OPAModulePolicy = advisor.processor.GenerateOPA(cssList, pssList, OPAdefaultRule) 59 | } else if OPAformat == "psp" { 60 | advisor.podSecurityPolicy = advisor.processor.GeneratePSP(cssList, pssList) 61 | } 62 | 63 | advisor.report = advisor.processor.GenerateReport(cssList, pssList) 64 | 65 | advisor.grants, advisor.grantWarnings = advisor.processor.GeneratePSPGrant(cssList, pssList) 66 | 67 | return nil 68 | } 69 | 70 | func (advisor *Advisor) PrintReport() { 71 | jsonOutput, err := json.Marshal(advisor.report) 72 | 73 | if err != nil { 74 | panic(err) 75 | } 76 | fmt.Println(string(jsonOutput)) 77 | } 78 | 79 | func (advisor *Advisor) PrintPodSecurityPolicy() error { 80 | e := k8sJSON.NewYAMLSerializer(k8sJSON.DefaultMetaFactory, nil, nil) 81 | 82 | err := e.Encode(advisor.podSecurityPolicy, os.Stdout) 83 | 84 | return err 85 | } 86 | 87 | func (advisor *Advisor) PrintOPAPolicy() string { 88 | if advisor.OPAModulePolicy != nil { 89 | err := advisor.OPAModulePolicy.String() 90 | fmt.Printf(err) 91 | return err 92 | } else { 93 | return "" 94 | } 95 | } 96 | func (advisor *Advisor) GetPodSecurityPolicy() *v1beta1.PodSecurityPolicy { 97 | return advisor.podSecurityPolicy 98 | } 99 | 100 | func (advisor *Advisor) PrintPodSecurityPolicyWithGrants() error { 101 | var err error 102 | e := k8sJSON.NewYAMLSerializer(k8sJSON.DefaultMetaFactory, nil, nil) 103 | 104 | if advisor.grantWarnings != "" { 105 | fmt.Println(advisor.grantWarnings) 106 | printYamlSeparator() 107 | } 108 | 109 | for _, pspGrant := range advisor.grants { 110 | fmt.Println(pspGrant.Comment) 111 | 112 | if err = e.Encode(pspGrant.PodSecurityPolicy, os.Stdout); err != nil { 113 | return err 114 | } 115 | 116 | printYamlSeparator() 117 | 118 | if err = e.Encode(pspGrant.Role, os.Stdout); err != nil { 119 | return err 120 | } 121 | 122 | printYamlSeparator() 123 | 124 | if err = e.Encode(pspGrant.RoleBinding, os.Stdout); err != nil { 125 | return err 126 | } 127 | 128 | printYamlSeparator() 129 | } 130 | 131 | return nil 132 | } 133 | 134 | func printYamlSeparator() { 135 | fmt.Println("---") 136 | } 137 | -------------------------------------------------------------------------------- /advisor/processor/generate.go: -------------------------------------------------------------------------------- 1 | package processor 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | "strings" 7 | 8 | "github.com/open-policy-agent/opa/ast" 9 | 10 | "github.com/sysdiglabs/kube-psp-advisor/advisor/report" 11 | "github.com/sysdiglabs/kube-psp-advisor/advisor/types" 12 | "github.com/sysdiglabs/kube-psp-advisor/generator" 13 | 14 | v1 "k8s.io/api/core/v1" 15 | "k8s.io/api/policy/v1beta1" 16 | "k8s.io/client-go/kubernetes" 17 | "k8s.io/client-go/tools/clientcmd" 18 | ) 19 | 20 | type Processor struct { 21 | k8sClient *kubernetes.Clientset 22 | namespace string 23 | excludeNamespaces []string // excludeNamespaces is a slice of namespaces that will be excluded from processing 24 | serviceAccountMap map[string]v1.ServiceAccount 25 | serverGitVersion string 26 | gen *generator.Generator 27 | } 28 | 29 | // NewProcessor returns a new processor 30 | func NewProcessor(kubeconfig string) (*Processor, error) { 31 | 32 | gen, err := generator.NewGenerator() 33 | if err != nil { 34 | return nil, fmt.Errorf("Could not create generator: %v", err) 35 | } 36 | 37 | config, err := clientcmd.BuildConfigFromFlags("", kubeconfig) 38 | if err != nil { 39 | return nil, err 40 | } 41 | clientset, err := kubernetes.NewForConfig(config) 42 | if err != nil { 43 | return nil, err 44 | } 45 | 46 | info, err := clientset.ServerVersion() 47 | 48 | if err != nil { 49 | return nil, err 50 | } 51 | 52 | return &Processor{ 53 | k8sClient: clientset, 54 | serverGitVersion: info.GitVersion, 55 | gen: gen, 56 | }, nil 57 | } 58 | 59 | func (p *Processor) SetNamespace(ns string) { 60 | p.namespace = ns 61 | } 62 | 63 | func (p *Processor) SetExcludeNamespaces(excludeNamespaces []string) { 64 | p.excludeNamespaces = excludeNamespaces 65 | } 66 | 67 | func (p *Processor) getFieldSelector() string { 68 | var list []string 69 | for _, ns := range p.excludeNamespaces { 70 | if ns == "" { 71 | continue 72 | } 73 | list = append(list, "metadata.namespace!="+ns) 74 | } 75 | 76 | return strings.Join(list, ",") 77 | } 78 | 79 | // GeneratePSP generates Pod Security Policy 80 | func (p *Processor) GeneratePSP(cssList []types.ContainerSecuritySpec, pssList []types.PodSecuritySpec) *v1beta1.PodSecurityPolicy { 81 | return p.gen.GeneratePSP(cssList, pssList, p.namespace, p.serverGitVersion) 82 | } 83 | 84 | func (p *Processor) GenerateOPA(cssList []types.ContainerSecuritySpec, pssList []types.PodSecuritySpec, OPAdefaultRule bool) *ast.Module { 85 | return p.gen.GenerateOPA(cssList, pssList, p.namespace, p.serverGitVersion, OPAdefaultRule) 86 | } 87 | 88 | // GeneratePSPGrant generates Pod Security Policies, Roles, RoleBindings for service accounts to use PSP 89 | func (p *Processor) GeneratePSPGrant(cssList []types.ContainerSecuritySpec, pssList []types.PodSecuritySpec) (types.PSPGrantList, string) { 90 | saSecuritySpecMap := map[string]*types.SASecuritySpec{} 91 | pspGrantList := []types.PSPGrant{} 92 | grantWarnings := "" 93 | 94 | for _, css := range cssList { 95 | key := fmt.Sprintf("%s:%s", css.Namespace, css.ServiceAccount) 96 | if _, exists := saSecuritySpecMap[key]; !exists { 97 | saSecuritySpecMap[key] = types.NewSASecuritySpec(css.Namespace, css.ServiceAccount) 98 | } 99 | saSecuritySpecMap[key].AddContainerSecuritySpec(css) 100 | } 101 | 102 | for _, pss := range pssList { 103 | key := fmt.Sprintf("%s:%s", pss.Namespace, pss.ServiceAccount) 104 | if _, exists := saSecuritySpecMap[key]; !exists { 105 | saSecuritySpecMap[key] = types.NewSASecuritySpec(pss.Namespace, pss.ServiceAccount) 106 | } 107 | saSecuritySpecMap[key].AddPodSecuritySpec(pss) 108 | } 109 | 110 | saSecuritySpecList := types.SASecuritySpecList{} 111 | 112 | // convert saSecuritySpecMap into list and then sort 113 | for _, saSecuritySpec := range saSecuritySpecMap { 114 | saSecuritySpecList = append(saSecuritySpecList, saSecuritySpec) 115 | } 116 | 117 | sort.Sort(saSecuritySpecList) 118 | 119 | for _, s := range saSecuritySpecList { 120 | if !s.IsDefaultServiceAccount() { 121 | pspGrant := types.PSPGrant{ 122 | Comment: s.GenerateComment(), 123 | ServiceAccount: s.ServiceAccount, 124 | Namespace: s.Namespace, 125 | Role: s.GenerateRole(), 126 | RoleBinding: s.GenerateRoleBinding(), 127 | PodSecurityPolicy: p.gen.GeneratePSPWithName(s.ContainerSecuritySpecList, s.PodSecuritySpecList, s.Namespace, p.serverGitVersion, s.GeneratePSPName()), 128 | } 129 | 130 | pspGrantList = append(pspGrantList, pspGrant) 131 | } else { 132 | grantWarnings += s.GenerateComment() 133 | } 134 | } 135 | 136 | return pspGrantList, grantWarnings 137 | } 138 | 139 | // GenerateReport generate a JSON report 140 | func (p *Processor) GenerateReport(cssList []types.ContainerSecuritySpec, pssList []types.PodSecuritySpec) *report.Report { 141 | r := report.NewReport() 142 | 143 | for _, c := range cssList { 144 | r.AddContainer(c) 145 | } 146 | 147 | for _, p := range pssList { 148 | r.AddPod(p) 149 | } 150 | 151 | return r 152 | } 153 | 154 | // GetSecuritySpec security posture 155 | func (p *Processor) GetSecuritySpec() ([]types.ContainerSecuritySpec, []types.PodSecuritySpec, error) { 156 | cssList := []types.ContainerSecuritySpec{} 157 | pssList := []types.PodSecuritySpec{} 158 | 159 | // get and cache service account list in the specified namespace 160 | var err error 161 | p.serviceAccountMap, err = p.getServiceAccountMap() 162 | if err != nil { 163 | return cssList, pssList, err 164 | } 165 | 166 | // get security spec from daemonsets 167 | cspList0, pspList0, err := p.getSecuritySpecFromDaemonSets() 168 | 169 | if err != nil { 170 | return cssList, pssList, err 171 | } 172 | 173 | cssList = append(cssList, cspList0...) 174 | pssList = append(pssList, pspList0...) 175 | 176 | // get security spec from deployments 177 | cssList1, pssList1, err := p.getSecuritySpecFromDeployments() 178 | 179 | if err != nil { 180 | return cssList, pssList, err 181 | } 182 | 183 | cssList = append(cssList, cssList1...) 184 | pssList = append(pssList, pssList1...) 185 | 186 | // get security spec from replicasets 187 | cssList2, pssList2, err := p.getSecuritySpecFromReplicaSets() 188 | if err != nil { 189 | return cssList, pssList, err 190 | } 191 | 192 | cssList = append(cssList, cssList2...) 193 | pssList = append(pssList, pssList2...) 194 | 195 | // get security spec from statefulsets 196 | cssList3, pssList3, err := p.getSecuritySpecFromStatefulSets() 197 | if err != nil { 198 | return cssList, pssList, err 199 | } 200 | 201 | cssList = append(cssList, cssList3...) 202 | pssList = append(pssList, pssList3...) 203 | 204 | // get security spec from replication controller 205 | cssList4, pssList4, err := p.getSecuritySpecFromReplicationController() 206 | if err != nil { 207 | return cssList, pssList, err 208 | } 209 | 210 | cssList = append(cssList, cssList4...) 211 | pssList = append(pssList, pssList4...) 212 | 213 | // get security spec from cron job 214 | cssList5, pssList5, err := p.getSecuritySpecFromCronJobs() 215 | if err != nil { 216 | return cssList, pssList, err 217 | } 218 | 219 | cssList = append(cssList, cssList5...) 220 | pssList = append(pssList, pssList5...) 221 | 222 | // get security spec from job 223 | cssList6, pssList6, err := p.getSecuritySpecFromJobs() 224 | if err != nil { 225 | return cssList, pssList, err 226 | } 227 | 228 | cssList = append(cssList, cssList6...) 229 | pssList = append(pssList, pssList6...) 230 | 231 | // get security spec from pods 232 | cssList7, pssList7, err := p.getSecuritySpecFromPods() 233 | if err != nil { 234 | return cssList, pssList, err 235 | } 236 | 237 | cssList = append(cssList, cssList7...) 238 | pssList = append(pssList, pssList7...) 239 | 240 | return cssList, pssList, nil 241 | } 242 | -------------------------------------------------------------------------------- /advisor/processor/generate_test.go: -------------------------------------------------------------------------------- 1 | package processor 2 | 3 | import "testing" 4 | 5 | func TestGetFieldSelector(t *testing.T) { 6 | p := Processor{ 7 | excludeNamespaces: []string{ 8 | "ns1", 9 | "", 10 | "ns3", 11 | "ns4", 12 | "", 13 | }, 14 | } 15 | 16 | expected := "metadata.namespace!=ns1,metadata.namespace!=ns3,metadata.namespace!=ns4" 17 | 18 | if p.getFieldSelector() != expected { 19 | t.Fatalf("expected field selector to match %s", expected) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /advisor/processor/get.go: -------------------------------------------------------------------------------- 1 | package processor 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/sysdiglabs/kube-psp-advisor/advisor/types" 7 | 8 | v1 "k8s.io/api/core/v1" 9 | v1meta "k8s.io/apimachinery/pkg/apis/meta/v1" 10 | ) 11 | 12 | const ( 13 | DaemonSet = "DaemonSet" 14 | Deployment = "Deployment" 15 | Pod = "Pod" 16 | StatefulSet = "StatefulSet" 17 | ReplicaSet = "ReplicaSet" 18 | ReplicationController = "ReplicationController" 19 | Job = "Job" 20 | CronJob = "CronJob" 21 | ) 22 | 23 | func (p *Processor) getSecuritySpecFromDaemonSets() ([]types.ContainerSecuritySpec, []types.PodSecuritySpec, error) { 24 | clientset := p.k8sClient 25 | cspList := []types.ContainerSecuritySpec{} 26 | pspList := []types.PodSecuritySpec{} 27 | 28 | lo := v1meta.ListOptions{FieldSelector: p.getFieldSelector()} 29 | daemonSetList, err := clientset.AppsV1().DaemonSets(p.namespace).List(lo) 30 | 31 | if err != nil { 32 | return cspList, pspList, err 33 | } 34 | 35 | for _, ds := range daemonSetList.Items { 36 | sa := p.GetServiceAccount(ds.Namespace, ds.Spec.Template.Spec.ServiceAccountName) 37 | 38 | cspList2, podSecurityPosture := p.gen.GetSecuritySpecFromPodSpec(types.Metadata{ 39 | Name: ds.Name, 40 | Kind: DaemonSet, 41 | }, ds.Namespace, ds.Spec.Template.Spec, &sa) 42 | 43 | pspList = append(pspList, podSecurityPosture) 44 | cspList = append(cspList, cspList2...) 45 | } 46 | 47 | return cspList, pspList, nil 48 | } 49 | 50 | func (p *Processor) getSecuritySpecFromReplicaSets() ([]types.ContainerSecuritySpec, []types.PodSecuritySpec, error) { 51 | clientset := p.k8sClient 52 | cssList := []types.ContainerSecuritySpec{} 53 | pssList := []types.PodSecuritySpec{} 54 | 55 | lo := v1meta.ListOptions{FieldSelector: p.getFieldSelector()} 56 | replicaSetList, err := clientset.AppsV1().ReplicaSets(p.namespace).List(lo) 57 | 58 | if err != nil { 59 | return cssList, pssList, err 60 | } 61 | 62 | for _, rs := range replicaSetList.Items { 63 | if len(rs.OwnerReferences) > 0 { 64 | continue 65 | } 66 | 67 | sa := p.GetServiceAccount(rs.Namespace, rs.Spec.Template.Spec.ServiceAccountName) 68 | cspList2, psc := p.gen.GetSecuritySpecFromPodSpec(types.Metadata{ 69 | Name: rs.Name, 70 | Kind: ReplicaSet, 71 | }, rs.Namespace, rs.Spec.Template.Spec, &sa) 72 | 73 | pssList = append(pssList, psc) 74 | cssList = append(cssList, cspList2...) 75 | } 76 | 77 | return cssList, pssList, nil 78 | } 79 | 80 | func (p *Processor) getSecuritySpecFromStatefulSets() ([]types.ContainerSecuritySpec, []types.PodSecuritySpec, error) { 81 | clientset := p.k8sClient 82 | cssList := []types.ContainerSecuritySpec{} 83 | pssList := []types.PodSecuritySpec{} 84 | 85 | lo := v1meta.ListOptions{FieldSelector: p.getFieldSelector()} 86 | statefulSetList, err := clientset.AppsV1().StatefulSets(p.namespace).List(lo) 87 | 88 | if err != nil { 89 | return cssList, pssList, err 90 | } 91 | 92 | for _, sts := range statefulSetList.Items { 93 | sa := p.GetServiceAccount(sts.Namespace, sts.Spec.Template.Spec.ServiceAccountName) 94 | cspList2, pss := p.gen.GetSecuritySpecFromPodSpec(types.Metadata{ 95 | Name: sts.Name, 96 | Kind: StatefulSet, 97 | }, sts.Namespace, sts.Spec.Template.Spec, &sa) 98 | 99 | pssList = append(pssList, pss) 100 | cssList = append(cssList, cspList2...) 101 | } 102 | 103 | return cssList, pssList, nil 104 | } 105 | 106 | func (p *Processor) getSecuritySpecFromReplicationController() ([]types.ContainerSecuritySpec, []types.PodSecuritySpec, error) { 107 | clientset := p.k8sClient 108 | cssList := []types.ContainerSecuritySpec{} 109 | pssList := []types.PodSecuritySpec{} 110 | 111 | lo := v1meta.ListOptions{FieldSelector: p.getFieldSelector()} 112 | replicationControllerList, err := clientset.CoreV1().ReplicationControllers(p.namespace).List(lo) 113 | 114 | if err != nil { 115 | return cssList, pssList, err 116 | } 117 | 118 | for _, rc := range replicationControllerList.Items { 119 | sa := p.GetServiceAccount(rc.Namespace, rc.Spec.Template.Spec.ServiceAccountName) 120 | cspList2, pss := p.gen.GetSecuritySpecFromPodSpec(types.Metadata{ 121 | Name: rc.Name, 122 | Kind: ReplicationController, 123 | }, rc.Namespace, rc.Spec.Template.Spec, &sa) 124 | 125 | pssList = append(pssList, pss) 126 | cssList = append(cssList, cspList2...) 127 | } 128 | 129 | return cssList, pssList, nil 130 | } 131 | 132 | func (p *Processor) getSecuritySpecFromCronJobs() ([]types.ContainerSecuritySpec, []types.PodSecuritySpec, error) { 133 | clientset := p.k8sClient 134 | cssList := []types.ContainerSecuritySpec{} 135 | pssList := []types.PodSecuritySpec{} 136 | 137 | lo := v1meta.ListOptions{FieldSelector: p.getFieldSelector()} 138 | jobList, err := clientset.BatchV1beta1().CronJobs(p.namespace).List(lo) 139 | 140 | if err != nil { 141 | return cssList, pssList, err 142 | } 143 | 144 | for _, cronJob := range jobList.Items { 145 | sa := p.GetServiceAccount(cronJob.Namespace, cronJob.Spec.JobTemplate.Spec.Template.Spec.ServiceAccountName) 146 | cspList2, pss := p.gen.GetSecuritySpecFromPodSpec(types.Metadata{ 147 | Name: cronJob.Name, 148 | Kind: CronJob, 149 | }, cronJob.Namespace, cronJob.Spec.JobTemplate.Spec.Template.Spec, &sa) 150 | 151 | pssList = append(pssList, pss) 152 | cssList = append(cssList, cspList2...) 153 | } 154 | 155 | return cssList, pssList, nil 156 | } 157 | 158 | func (p *Processor) getSecuritySpecFromJobs() ([]types.ContainerSecuritySpec, []types.PodSecuritySpec, error) { 159 | clientset := p.k8sClient 160 | cssList := []types.ContainerSecuritySpec{} 161 | pssList := []types.PodSecuritySpec{} 162 | 163 | lo := v1meta.ListOptions{FieldSelector: p.getFieldSelector()} 164 | jobList, err := clientset.BatchV1().Jobs(p.namespace).List(lo) 165 | 166 | if err != nil { 167 | return cssList, pssList, err 168 | } 169 | 170 | for _, job := range jobList.Items { 171 | if len(job.OwnerReferences) > 0 { 172 | continue 173 | } 174 | sa := p.GetServiceAccount(job.Namespace, job.Spec.Template.Spec.ServiceAccountName) 175 | cspList2, pss := p.gen.GetSecuritySpecFromPodSpec(types.Metadata{ 176 | Name: job.Name, 177 | Kind: Job, 178 | }, job.Namespace, job.Spec.Template.Spec, &sa) 179 | 180 | pssList = append(pssList, pss) 181 | cssList = append(cssList, cspList2...) 182 | } 183 | 184 | return cssList, pssList, nil 185 | } 186 | 187 | func (p *Processor) getSecuritySpecFromDeployments() ([]types.ContainerSecuritySpec, []types.PodSecuritySpec, error) { 188 | clientset := p.k8sClient 189 | cssList := []types.ContainerSecuritySpec{} 190 | pssList := []types.PodSecuritySpec{} 191 | 192 | lo := v1meta.ListOptions{FieldSelector: p.getFieldSelector()} 193 | deployments, err := clientset.AppsV1().Deployments(p.namespace).List(lo) 194 | 195 | if err != nil { 196 | return cssList, pssList, err 197 | } 198 | 199 | for _, deploy := range deployments.Items { 200 | sa := p.GetServiceAccount(deploy.Namespace, deploy.Spec.Template.Spec.ServiceAccountName) 201 | cspList2, pss := p.gen.GetSecuritySpecFromPodSpec(types.Metadata{ 202 | Name: deploy.Name, 203 | Kind: Deployment, 204 | }, deploy.Namespace, deploy.Spec.Template.Spec, &sa) 205 | 206 | pssList = append(pssList, pss) 207 | cssList = append(cssList, cspList2...) 208 | } 209 | 210 | return cssList, pssList, nil 211 | } 212 | 213 | func (p *Processor) getSecuritySpecFromPods() ([]types.ContainerSecuritySpec, []types.PodSecuritySpec, error) { 214 | clientset := p.k8sClient 215 | cssList := []types.ContainerSecuritySpec{} 216 | pssList := []types.PodSecuritySpec{} 217 | 218 | lo := v1meta.ListOptions{FieldSelector: p.getFieldSelector()} 219 | pods, err := clientset.CoreV1().Pods(p.namespace).List(lo) 220 | 221 | if err != nil { 222 | return cssList, pssList, err 223 | } 224 | 225 | for _, pod := range pods.Items { 226 | if len(pod.OwnerReferences) > 0 { 227 | continue 228 | } 229 | 230 | sa := p.GetServiceAccount(pod.Namespace, pod.Spec.ServiceAccountName) 231 | cspList2, podSecurityPosture := p.gen.GetSecuritySpecFromPodSpec(types.Metadata{ 232 | Name: pod.Name, 233 | Kind: Pod, 234 | }, pod.Namespace, pod.Spec, &sa) 235 | 236 | pssList = append(pssList, podSecurityPosture) 237 | cssList = append(cssList, cspList2...) 238 | } 239 | 240 | return cssList, pssList, nil 241 | } 242 | 243 | func (p *Processor) getServiceAccountMap() (map[string]v1.ServiceAccount, error) { 244 | serviceAccountMap := map[string]v1.ServiceAccount{} 245 | 246 | lo := v1meta.ListOptions{FieldSelector: p.getFieldSelector()} 247 | serviceAccounts, err := p.k8sClient.CoreV1().ServiceAccounts(p.namespace).List(lo) 248 | if err != nil { 249 | return serviceAccountMap, err 250 | } 251 | 252 | // service account is an namespaced object 253 | for _, sa := range serviceAccounts.Items { 254 | key := fmt.Sprintf("%s:%s", sa.Namespace, sa.Name) 255 | serviceAccountMap[key] = sa 256 | } 257 | 258 | return serviceAccountMap, nil 259 | } 260 | 261 | func (p *Processor) GetServiceAccount(ns, saName string) v1.ServiceAccount { 262 | if saName == "" { 263 | saName = "default" 264 | } 265 | 266 | key := fmt.Sprintf("%s:%s", ns, saName) 267 | 268 | sa, exists := p.serviceAccountMap[key] 269 | 270 | if !exists { 271 | return v1.ServiceAccount{} 272 | } 273 | 274 | return sa 275 | } 276 | -------------------------------------------------------------------------------- /advisor/report/report.go: -------------------------------------------------------------------------------- 1 | package report 2 | 3 | import ( 4 | "github.com/sysdiglabs/kube-psp-advisor/advisor/types" 5 | ) 6 | 7 | const ( 8 | allPrivilegEscalation = "allowPrivilegeEscalation" 9 | runAsUser = "runAsUser" 10 | runAsGroup = "runAsGroup" 11 | runAsNonRoot = "runAsNonRoot" 12 | dropCapabilities = "dropCapabilities" 13 | addCapabilities = "addCapabilities" 14 | privileged = "privileged" 15 | readOnlyRootFileSystem = "readOnlyRootFileSystem" 16 | hostPID = "hostPID" 17 | hostIPC = "hostIPC" 18 | hostNetwork = "hostNetwork" 19 | subPath = "subPath" 20 | ) 21 | 22 | type Report struct { 23 | PodSecuritySpecs map[string][]types.PodSecuritySpec `json:"podSecuritySpecs"` 24 | PodVolumes map[string][]types.PodSecuritySpec `json:"podVolumeTypes"` 25 | Containers map[string][]types.ContainerSecuritySpec `json:"containerSecuritySpec"` 26 | } 27 | 28 | func NewReport() *Report { 29 | r := &Report{ 30 | Containers: map[string][]types.ContainerSecuritySpec{}, 31 | PodSecuritySpecs: map[string][]types.PodSecuritySpec{}, 32 | PodVolumes: map[string][]types.PodSecuritySpec{}, 33 | } 34 | 35 | // container related security posture report 36 | r.Containers[allPrivilegEscalation] = []types.ContainerSecuritySpec{} 37 | r.Containers[runAsUser] = []types.ContainerSecuritySpec{} 38 | r.Containers[runAsNonRoot] = []types.ContainerSecuritySpec{} 39 | r.Containers[dropCapabilities] = []types.ContainerSecuritySpec{} 40 | r.Containers[addCapabilities] = []types.ContainerSecuritySpec{} 41 | r.Containers[runAsGroup] = []types.ContainerSecuritySpec{} 42 | r.Containers[privileged] = []types.ContainerSecuritySpec{} 43 | r.Containers[readOnlyRootFileSystem] = []types.ContainerSecuritySpec{} 44 | r.Containers[subPath] = []types.ContainerSecuritySpec{} 45 | 46 | // pod related security posture report 47 | r.PodSecuritySpecs[hostPID] = []types.PodSecuritySpec{} 48 | r.PodSecuritySpecs[hostNetwork] = []types.PodSecuritySpec{} 49 | r.PodSecuritySpecs[hostIPC] = []types.PodSecuritySpec{} 50 | 51 | return r 52 | } 53 | 54 | func (r *Report) AddPod(p types.PodSecuritySpec) { 55 | if p.HostPID { 56 | r.PodSecuritySpecs[hostPID] = append(r.PodSecuritySpecs[hostPID], p) 57 | } 58 | 59 | if p.HostNetwork { 60 | r.PodSecuritySpecs[hostNetwork] = append(r.PodSecuritySpecs[hostNetwork], p) 61 | } 62 | 63 | if p.HostIPC { 64 | r.PodSecuritySpecs[hostIPC] = append(r.PodSecuritySpecs[hostIPC], p) 65 | } 66 | 67 | for _, v := range p.VolumeTypes { 68 | if _, exists := r.PodVolumes[v]; !exists { 69 | r.PodVolumes[v] = []types.PodSecuritySpec{} 70 | } 71 | 72 | r.PodVolumes[v] = append(r.PodVolumes[v], p) 73 | } 74 | } 75 | 76 | func (r *Report) AddContainer(c types.ContainerSecuritySpec) { 77 | if c.AllowPrivilegeEscalation != nil && *c.AllowPrivilegeEscalation { 78 | r.Containers[allPrivilegEscalation] = append(r.Containers[allPrivilegEscalation], c) 79 | } 80 | 81 | if c.RunAsUser != nil { 82 | r.Containers[runAsUser] = append(r.Containers[runAsUser], c) 83 | } 84 | 85 | if c.RunAsNonRoot != nil { 86 | r.Containers[runAsNonRoot] = append(r.Containers[runAsNonRoot], c) 87 | } 88 | 89 | if len(c.DroppedCap) > 0 { 90 | r.Containers[dropCapabilities] = append(r.Containers[dropCapabilities], c) 91 | } 92 | 93 | if len(c.AddedCap) > 0 { 94 | r.Containers[addCapabilities] = append(r.Containers[addCapabilities], c) 95 | } 96 | 97 | if c.RunAsGroup != nil { 98 | r.Containers[runAsGroup] = append(r.Containers[runAsGroup], c) 99 | } 100 | 101 | if c.Privileged { 102 | r.Containers[privileged] = append(r.Containers[privileged], c) 103 | } 104 | 105 | if c.ReadOnlyRootFS { 106 | r.Containers[readOnlyRootFileSystem] = append(r.Containers[readOnlyRootFileSystem], c) 107 | } 108 | 109 | for _, vm := range c.VolumeMounts { 110 | if vm.UsesSubPath() { 111 | r.Containers[subPath] = append(r.Containers[subPath], c) 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /advisor/types/lintreport.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "github.com/sysdiglabs/kube-psp-advisor/utils" 5 | ) 6 | 7 | const ( 8 | Reduced = -1 9 | NoChange = 0 10 | Escalated = 1 11 | ) 12 | 13 | var ( 14 | m = map[int]string{ 15 | Reduced: "Reduced", 16 | NoChange: "No Change", 17 | Escalated: "Escalated", 18 | } 19 | ) 20 | 21 | const ( 22 | root = "root" 23 | nonRoot = "non-root" 24 | ) 25 | 26 | type LintReport struct { 27 | TotalSourceWorkloads int `json:"total_source_workloads"` 28 | TotalTargetWorkloads int `json:"total_target_workloads"` 29 | TotalSourceImages int `json:"total_source_images"` 30 | TotalTargetImages int `json:"total_target_images"` 31 | TotalEscalation int `json:"escalation_count"` 32 | TotalReduction int `json:"reduction_count"` 33 | Escalations []Metadata `json:"escalations"` 34 | Reductions []Metadata `json:"reductions"` 35 | NewPrivileged *Escalation `json:"new_privileged"` 36 | RemovedPrivileged *Escalation `json:"removed_privileged"` 37 | NewHostIPC *Escalation `json:"new_hostIPC"` 38 | RemovedHostIPC *Escalation `json:"removed_hostIPC"` 39 | NewHostNetwork *Escalation `json:"new_hostNetwork"` 40 | RemovedHostNetwork *Escalation `json:"removed_hostNetwork"` 41 | NewHostPID *Escalation `json:"new_hostPID"` 42 | RemovedHostPID *Escalation `json:"removed_hostPID"` 43 | NewHostPaths map[string]bool `json:"-"` 44 | RemovedHostPaths map[string]bool `json:"-"` 45 | NewVolumeTypes map[string]*Escalation `json:"new_volume_types"` 46 | RemovedVolumeTypes map[string]*Escalation `json:"removed_volume_types"` 47 | NewCapabilities map[string]*Escalation `json:"new_capabilities"` 48 | RemovedCapabilities map[string]*Escalation `json:"reduced_capabilities"` 49 | NewRunUserAsRoot *Escalation `json:"new_run_user_as_root"` 50 | RemovedRunUserAsRoot *Escalation `json:"removed_run_user_as_root"` 51 | NewRunGroupAsRoot *Escalation `json:"new_run_group_as_root"` 52 | RemovedRunGroupAsRoot *Escalation `json:"removed_run_group_as_root"` 53 | NewReadOnlyRootFS *Escalation `json:"new_read_only_root_fs"` 54 | RemovedReadOnlyRootFS *Escalation `json:"removed_read_only_root_fs"` 55 | } 56 | 57 | type Escalation struct { 58 | Status int `json:"-"` 59 | StatusMessage string `json:"status"` 60 | Previous string `json:"previous"` 61 | Current string `json:"current"` 62 | Workloads []Metadata `json:"workloads"` 63 | WorkloadCount int `json:"workloads_count"` 64 | workloadMap map[Metadata]bool `json:"-"` 65 | } 66 | 67 | // InitEscalation returns an initialized escalation object 68 | func InitEscalation(status int, prev, cur string) *Escalation { 69 | return &Escalation{ 70 | Status: status, 71 | StatusMessage: getEscalatedStatus(status), 72 | Previous: prev, 73 | Current: cur, 74 | Workloads: []Metadata{}, 75 | workloadMap: map[Metadata]bool{}, 76 | } 77 | } 78 | 79 | // SetEscalation set escalation status 80 | func (e *Escalation) SetEscalation(status int, prev, cur string) { 81 | e.Status = status 82 | e.StatusMessage = getEscalatedStatus(status) 83 | e.Previous = prev 84 | e.Current = cur 85 | } 86 | 87 | func (e *Escalation) UseSecurityContext() bool { 88 | return len(e.Workloads) > 0 89 | } 90 | 91 | func (e *Escalation) AddWorkload(w Metadata) { 92 | e.workloadMap[w] = true 93 | } 94 | 95 | func (e *Escalation) ConsolidateWorkloadImage() { 96 | m := map[Metadata]bool{} 97 | 98 | for w := range e.workloadMap { 99 | w.Image = "" 100 | m[w] = true 101 | } 102 | 103 | for w := range m { 104 | e.Workloads = append(e.Workloads, w) 105 | } 106 | 107 | e.WorkloadCount = len(e.Workloads) 108 | } 109 | 110 | func (e *Escalation) ConsolidateWorkload() { 111 | for w := range e.workloadMap { 112 | e.Workloads = append(e.Workloads, w) 113 | } 114 | 115 | e.WorkloadCount = len(e.Workloads) 116 | } 117 | 118 | func (e *Escalation) NoChanges() bool { 119 | return !e.UseSecurityContext() 120 | } 121 | 122 | func (e *Escalation) IsEscalated() bool { 123 | return e.Status == Escalated && e.UseSecurityContext() 124 | } 125 | 126 | func (e *Escalation) IsReduced() bool { 127 | return e.Status == Reduced && e.UseSecurityContext() 128 | } 129 | 130 | // NewEscalationReport returns an escalation report object 131 | func NewEscalationReport() *LintReport { 132 | return &LintReport{ 133 | TotalSourceWorkloads: 0, 134 | TotalTargetWorkloads: 0, 135 | TotalEscalation: 0, 136 | TotalReduction: 0, 137 | Escalations: []Metadata{}, 138 | Reductions: []Metadata{}, 139 | NewPrivileged: InitEscalation(Escalated, "false", "true"), 140 | RemovedPrivileged: InitEscalation(Reduced, "true", "false"), 141 | NewHostNetwork: InitEscalation(Escalated, "false", "true"), 142 | RemovedHostNetwork: InitEscalation(Reduced, "true", "false"), 143 | NewHostIPC: InitEscalation(Escalated, "false", "true"), 144 | RemovedHostIPC: InitEscalation(Reduced, "true", "false"), 145 | NewHostPID: InitEscalation(Escalated, "false", "true"), 146 | RemovedHostPID: InitEscalation(Reduced, "true", "false"), 147 | NewHostPaths: map[string]bool{}, 148 | RemovedHostPaths: map[string]bool{}, 149 | NewCapabilities: map[string]*Escalation{}, 150 | RemovedCapabilities: map[string]*Escalation{}, 151 | NewVolumeTypes: map[string]*Escalation{}, 152 | RemovedVolumeTypes: map[string]*Escalation{}, 153 | NewRunGroupAsRoot: InitEscalation(Escalated, nonRoot, root), 154 | RemovedRunGroupAsRoot: InitEscalation(Reduced, root, nonRoot), 155 | NewRunUserAsRoot: InitEscalation(Escalated, nonRoot, root), 156 | RemovedRunUserAsRoot: InitEscalation(Reduced, root, nonRoot), 157 | NewReadOnlyRootFS: InitEscalation(Reduced, "false", "true"), 158 | RemovedReadOnlyRootFS: InitEscalation(Escalated, "true", "false"), 159 | } 160 | } 161 | 162 | // privileged mode 163 | func (er *LintReport) PrivilegedEscalated() bool { 164 | return er.NewPrivileged.IsEscalated() 165 | } 166 | 167 | // privileged mode 168 | func (er *LintReport) PrivilegedReduced() bool { 169 | return er.RemovedPrivileged.IsReduced() 170 | } 171 | 172 | // privileged mode 173 | func (er *LintReport) PrivilegedNoChange() bool { 174 | return !er.PrivilegedReduced() && !er.PrivilegedReduced() 175 | } 176 | 177 | // HostIPC 178 | func (er *LintReport) HostIPCEscalated() bool { 179 | return er.NewHostIPC.IsEscalated() 180 | } 181 | 182 | // HostIPC 183 | func (er *LintReport) HostIPCReduced() bool { 184 | return er.RemovedHostIPC.IsReduced() 185 | } 186 | 187 | // HostIPC 188 | func (er *LintReport) HostIPCNoChange() bool { 189 | return !er.HostIPCEscalated() && !er.HostIPCReduced() 190 | } 191 | 192 | // HostNetwork 193 | func (er *LintReport) HostNetworkEscalated() bool { 194 | return er.NewHostNetwork.IsEscalated() 195 | } 196 | 197 | // HostNetwork 198 | func (er *LintReport) HostNetworkReduced() bool { 199 | return er.RemovedHostNetwork.IsReduced() 200 | } 201 | 202 | // HostNetwork 203 | func (er *LintReport) HostNetworkNoChange() bool { 204 | return !er.HostNetworkEscalated() && !er.HostNetworkReduced() 205 | } 206 | 207 | // HostPID 208 | func (er *LintReport) HostPIDEscalated() bool { 209 | return er.NewHostPID.IsEscalated() 210 | } 211 | 212 | // HostPID 213 | func (er *LintReport) HostPIDReduced() bool { 214 | return er.RemovedHostPID.IsReduced() 215 | } 216 | 217 | // HostPID 218 | func (er *LintReport) HostPIDNoChange() bool { 219 | return !er.HostPIDEscalated() && !er.HostPIDReduced() 220 | } 221 | 222 | // ReadOnlyRootFileSystem 223 | func (er *LintReport) ReadOnlyRootFSEscalated() bool { 224 | return er.RemovedReadOnlyRootFS.IsEscalated() 225 | } 226 | 227 | // ReadOnlyRootFileSystem 228 | func (er *LintReport) ReadOnlyRootFSReduced() bool { 229 | return er.NewReadOnlyRootFS.IsReduced() 230 | } 231 | 232 | // ReadOnlyRootFileSystem 233 | func (er *LintReport) ReadOnlyRootFSNoChange() bool { 234 | return !er.ReadOnlyRootFSEscalated() && !er.ReadOnlyRootFSReduced() 235 | } 236 | 237 | // runAsUser (non root -> root) 238 | func (er *LintReport) RunUserAsRootEscalated() bool { 239 | return er.NewRunUserAsRoot.IsEscalated() 240 | } 241 | 242 | // runAsUser (root -> non root) 243 | func (er *LintReport) RunUserAsRootReduced() bool { 244 | return er.RemovedRunUserAsRoot.IsReduced() 245 | } 246 | 247 | // runAsUser 248 | func (er *LintReport) RunUserAsRootNoChange() bool { 249 | return !er.RunUserAsRootEscalated() && !er.RunUserAsRootReduced() 250 | } 251 | 252 | // runAsGroup (non root -> root) 253 | func (er *LintReport) RunGroupAsRootEscalated() bool { 254 | return er.NewRunGroupAsRoot.IsEscalated() 255 | } 256 | 257 | // runAsGroup (root -> non root) 258 | func (er *LintReport) RunGroupAsRootReduced() bool { 259 | return er.RemovedRunGroupAsRoot.IsReduced() 260 | } 261 | 262 | // runAsGroup 263 | func (er *LintReport) RunGroupAsRootNoChange() bool { 264 | return er.NewRunGroupAsRoot.NoChanges() 265 | } 266 | 267 | // newly added volume types 268 | func (er *LintReport) AddedVolumes() bool { 269 | return len(er.NewVolumeTypes) > 0 270 | } 271 | 272 | // removed volume types 273 | func (er *LintReport) RemovedVolumes() bool { 274 | return len(er.RemovedVolumeTypes) > 0 275 | } 276 | 277 | // added capabilities 278 | func (er *LintReport) AddedCapabilities() bool { 279 | return len(er.NewCapabilities) > 0 280 | } 281 | 282 | // dropped capabilities 283 | func (er *LintReport) DroppedCapabilities() bool { 284 | return len(er.RemovedCapabilities) > 0 285 | } 286 | 287 | func (er *LintReport) Escalated() bool { 288 | if er.PrivilegedEscalated() || er.HostNetworkEscalated() || er.HostPIDEscalated() || er.HostIPCEscalated() || er.AddedVolumes() || 289 | er.AddedCapabilities() || er.ReadOnlyRootFSEscalated() || er.RunGroupAsRootEscalated() || er.RunUserAsRootEscalated() { 290 | return true 291 | } 292 | 293 | return false 294 | } 295 | 296 | func (er *LintReport) Reduced() bool { 297 | if er.PrivilegedReduced() || er.HostNetworkReduced() || er.HostPIDReduced() || er.HostIPCReduced() || er.RemovedVolumes() || 298 | er.DroppedCapabilities() || er.ReadOnlyRootFSReduced() || er.RunGroupAsRootReduced() || er.RunUserAsRootReduced() { 299 | return true 300 | } 301 | 302 | return false 303 | } 304 | 305 | // GenerateEscalationReportFromSecurityContext returns a escalation report after comparing the source and target YAML files 306 | func (er *LintReport) GenerateEscalationReportFromSecurityContext(srcCssList, targetCssList []ContainerSecuritySpec, srcPssList, targetPssList []PodSecuritySpec) { 307 | srcCssMap := NewContainerSecuritySpecMap(srcCssList) 308 | targetCssMap := NewContainerSecuritySpecMap(targetCssList) 309 | 310 | srcPssMap := NewPodSecuritySpecMap(srcPssList) 311 | targetPssMap := NewPodSecuritySpecMap(targetPssList) 312 | 313 | escalations := InitEscalation(Escalated, "", "") 314 | reductions := InitEscalation(Reduced, "", "") 315 | 316 | // privileged - false to true (escalated) 317 | for meta, targetCss := range targetCssMap { 318 | srcCss, exits := srcCssMap[meta] 319 | if targetCss.Privileged && (!exits || !srcCss.Privileged) { 320 | er.NewPrivileged.AddWorkload(meta) 321 | escalations.AddWorkload(meta) 322 | } 323 | } 324 | er.NewPrivileged.ConsolidateWorkload() 325 | 326 | // privileged - true to false (reduced) 327 | for meta, srcCss := range srcCssMap { 328 | targetCss, exists := targetCssMap[meta] 329 | 330 | if srcCss.Privileged && (!exists || !targetCss.Privileged) { 331 | er.RemovedPrivileged.AddWorkload(meta) 332 | reductions.AddWorkload(meta) 333 | } 334 | } 335 | er.RemovedPrivileged.ConsolidateWorkload() 336 | 337 | // hostNetwork - false to true (escalated) 338 | for meta, targetPss := range targetPssMap { 339 | srcPss, exits := srcPssMap[meta] 340 | if targetPss.HostNetwork && (!exits || !srcPss.HostNetwork) { 341 | er.NewHostNetwork.AddWorkload(meta) 342 | escalations.AddWorkload(meta) 343 | } 344 | } 345 | er.NewHostNetwork.ConsolidateWorkload() 346 | 347 | // hostNetwork - true to false (reduced) 348 | for meta, srcPss := range srcPssMap { 349 | targetPss, exists := targetPssMap[meta] 350 | 351 | if srcPss.HostNetwork && (!exists || !targetPss.HostNetwork) { 352 | er.RemovedHostNetwork.AddWorkload(meta) 353 | reductions.AddWorkload(meta) 354 | } 355 | } 356 | er.RemovedHostNetwork.ConsolidateWorkload() 357 | 358 | // hostIPC - false to true (escalated) 359 | for meta, targetPss := range targetPssMap { 360 | srcPss, exits := srcPssMap[meta] 361 | if targetPss.HostIPC && (!exits || !srcPss.HostIPC) { 362 | er.NewHostIPC.AddWorkload(meta) 363 | escalations.AddWorkload(meta) 364 | } 365 | } 366 | er.NewHostIPC.ConsolidateWorkload() 367 | 368 | // hostIPC - true to false (reduced) 369 | for meta, srcPss := range srcPssMap { 370 | targetPss, exists := targetPssMap[meta] 371 | 372 | if srcPss.HostIPC && (!exists || !targetPss.HostIPC) { 373 | er.RemovedHostIPC.AddWorkload(meta) 374 | reductions.AddWorkload(meta) 375 | } 376 | } 377 | er.RemovedHostIPC.ConsolidateWorkload() 378 | 379 | // hostPID - false to true (escalated) 380 | for meta, targetPss := range targetPssMap { 381 | srcPss, exits := srcPssMap[meta] 382 | if targetPss.HostPID && (!exits || !srcPss.HostPID) { 383 | er.NewHostPID.AddWorkload(meta) 384 | escalations.AddWorkload(meta) 385 | } 386 | } 387 | er.NewHostPID.ConsolidateWorkload() 388 | 389 | // hostPID - true to false (reduced) 390 | for meta, srcPss := range srcPssMap { 391 | targetPss, exists := targetPssMap[meta] 392 | 393 | if srcPss.HostPID && (!exists || !targetPss.HostPID) { 394 | er.RemovedHostPID.AddWorkload(meta) 395 | reductions.AddWorkload(meta) 396 | } 397 | } 398 | er.RemovedHostPID.ConsolidateWorkload() 399 | 400 | // readOnlyRootFS - true to false (escalated) 401 | for meta, targetCss := range targetCssMap { 402 | srcCss, exists := srcCssMap[meta] 403 | if !targetCss.ReadOnlyRootFS && (!exists || srcCss.ReadOnlyRootFS) { 404 | er.RemovedReadOnlyRootFS.AddWorkload(meta) 405 | escalations.AddWorkload(meta) 406 | } 407 | } 408 | er.RemovedReadOnlyRootFS.ConsolidateWorkload() 409 | 410 | // readOnlyRootFS - false to true (reduced) 411 | for meta, srcCss := range srcCssMap { 412 | targetCss, exists := targetCssMap[meta] 413 | 414 | if !srcCss.ReadOnlyRootFS && (!exists || targetCss.ReadOnlyRootFS) { 415 | er.NewReadOnlyRootFS.AddWorkload(meta) 416 | reductions.AddWorkload(meta) 417 | } 418 | } 419 | er.NewReadOnlyRootFS.ConsolidateWorkload() 420 | 421 | // runAsUer - non root to root (escalated) 422 | for meta, targetCss := range targetCssMap { 423 | srcCss, exists := srcCssMap[meta] 424 | if (targetCss.RunAsUser == nil || *targetCss.RunAsUser == 0) && (!exists || (srcCss.RunAsUser != nil && *srcCss.RunAsUser > 0)) { 425 | er.NewRunUserAsRoot.AddWorkload(meta) 426 | escalations.AddWorkload(meta) 427 | } 428 | } 429 | er.NewRunUserAsRoot.ConsolidateWorkload() 430 | 431 | // runAsUer - root to non root (reduced) 432 | for meta, srcCss := range srcCssMap { 433 | targetCss, exists := targetCssMap[meta] 434 | 435 | if (srcCss.RunAsUser == nil || *srcCss.RunAsUser == 0) && (!exists || (targetCss.RunAsUser != nil && *targetCss.RunAsUser > 0)) { 436 | er.RemovedRunUserAsRoot.workloadMap[meta] = true 437 | reductions.AddWorkload(meta) 438 | } 439 | } 440 | er.RemovedRunUserAsRoot.ConsolidateWorkload() 441 | 442 | // runAsGroup - non root to root (escalated) 443 | for meta, targetCss := range targetCssMap { 444 | srcCss, exists := srcCssMap[meta] 445 | if (targetCss.RunAsGroup == nil || *targetCss.RunAsGroup == 0) && (!exists || (srcCss.RunAsGroup != nil && *srcCss.RunAsGroup > 0)) { 446 | er.NewRunGroupAsRoot.AddWorkload(meta) 447 | escalations.AddWorkload(meta) 448 | } 449 | } 450 | er.NewRunGroupAsRoot.ConsolidateWorkload() 451 | 452 | // runAsGroup - root to non root (reduced) 453 | for meta, srcCss := range srcCssMap { 454 | targetCss, exists := targetCssMap[meta] 455 | 456 | if (srcCss.RunAsGroup == nil || *srcCss.RunAsGroup == 0) && (!exists || (targetCss.RunAsGroup != nil && *targetCss.RunAsGroup > 0)) { 457 | er.RemovedRunGroupAsRoot.AddWorkload(meta) 458 | reductions.AddWorkload(meta) 459 | } 460 | } 461 | er.RemovedRunGroupAsRoot.ConsolidateWorkload() 462 | 463 | // caps 464 | for meta, targetCss := range targetCssMap { 465 | srcCss, exists := srcCssMap[meta] 466 | 467 | if exists { 468 | leftDiff, rightDiff := diff(srcCss.Capabilities, targetCss.Capabilities) 469 | 470 | for _, cap := range rightDiff { 471 | if _, capExists := er.NewCapabilities[cap]; !capExists { 472 | er.NewCapabilities[cap] = InitEscalation(Escalated, "", cap) 473 | } 474 | er.NewCapabilities[cap].AddWorkload(meta) 475 | escalations.AddWorkload(meta) 476 | } 477 | 478 | for _, cap := range leftDiff { 479 | if _, capExists := er.RemovedCapabilities[cap]; !capExists { 480 | er.RemovedCapabilities[cap] = InitEscalation(Reduced, cap, "") 481 | } 482 | 483 | er.RemovedCapabilities[cap].AddWorkload(meta) 484 | reductions.AddWorkload(meta) 485 | } 486 | } 487 | } 488 | 489 | for _, e := range er.NewCapabilities { 490 | e.ConsolidateWorkload() 491 | } 492 | 493 | for _, e := range er.RemovedCapabilities { 494 | e.ConsolidateWorkload() 495 | } 496 | 497 | // volume types (configMap, secret, emptryDir etc.) 498 | for meta, targetPss := range targetPssMap { 499 | srcPss, exists := srcPssMap[meta] 500 | 501 | if exists { 502 | leftDiff, rightDiff := diff(srcPss.VolumeTypes, targetPss.VolumeTypes) 503 | 504 | for _, vol := range rightDiff { 505 | if _, volExists := er.NewVolumeTypes[vol]; !volExists { 506 | er.NewVolumeTypes[vol] = InitEscalation(Escalated, "", vol) 507 | } 508 | er.NewVolumeTypes[vol].AddWorkload(meta) 509 | escalations.AddWorkload(meta) 510 | } 511 | 512 | for _, vol := range leftDiff { 513 | if _, volExists := er.RemovedVolumeTypes[vol]; !volExists { 514 | er.RemovedVolumeTypes[vol] = InitEscalation(Reduced, vol, "") 515 | } 516 | er.RemovedVolumeTypes[vol].AddWorkload(meta) 517 | reductions.AddWorkload(meta) 518 | } 519 | } 520 | } 521 | 522 | for _, e := range er.NewVolumeTypes { 523 | e.ConsolidateWorkload() 524 | } 525 | 526 | for _, e := range er.RemovedVolumeTypes { 527 | e.ConsolidateWorkload() 528 | } 529 | 530 | escalations.ConsolidateWorkloadImage() 531 | reductions.ConsolidateWorkloadImage() 532 | 533 | er.Escalations = append(er.Escalations, escalations.Workloads...) 534 | er.Reductions = append(er.Reductions, reductions.Workloads...) 535 | 536 | er.TotalEscalation = len(er.Escalations) 537 | er.TotalReduction = len(er.Reductions) 538 | er.TotalSourceWorkloads = len(srcPssMap) 539 | er.TotalTargetWorkloads = len(targetPssMap) 540 | er.TotalSourceImages = len(srcCssMap) 541 | er.TotalTargetImages = len(targetCssMap) 542 | } 543 | 544 | func getEscalatedStatus(status int) string { 545 | return m[status] 546 | } 547 | 548 | func diff(left, right []string) (leftDiff, rightDiff []string) { 549 | leftMap := utils.ArrayToMap(left) 550 | rightMap := utils.ArrayToMap(right) 551 | for cap := range leftMap { 552 | if _, exists := rightMap[cap]; exists { 553 | delete(leftMap, cap) 554 | delete(rightMap, cap) 555 | } 556 | } 557 | 558 | return utils.MapToArray(leftMap), utils.MapToArray(rightMap) 559 | } 560 | -------------------------------------------------------------------------------- /advisor/types/lintreport_test.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | 8 | "k8s.io/api/policy/v1beta1" 9 | 10 | "github.com/ghodss/yaml" 11 | ) 12 | 13 | var ( 14 | pspRestrictedStr = ` 15 | apiVersion: policy/v1beta1 16 | kind: PodSecurityPolicy 17 | metadata: 18 | name: restricted 19 | spec: 20 | privileged: false # Don't allow privileged pods! 21 | # The rest fills in some required fields. 22 | runAsUser: 23 | rule: MustRunAsNonRoot 24 | runAsGroup: 25 | rule: MustRunAsNonRoot 26 | volumes: 27 | - 'secret' 28 | - 'configMap' 29 | - 'emptyDir' 30 | readOnlyRootFilesystem: true 31 | ` 32 | pspPrivilegedStr = ` 33 | apiVersion: policy/v1beta1 34 | kind: PodSecurityPolicy 35 | metadata: 36 | name: privileged 37 | annotations: 38 | seccomp.security.alpha.kubernetes.io/allowedProfileNames: '*' 39 | spec: 40 | privileged: true 41 | allowPrivilegeEscalation: true 42 | allowedCapabilities: 43 | - '*' 44 | volumes: 45 | - '*' 46 | hostNetwork: true 47 | hostIPC: true 48 | hostPID: true 49 | runAsUser: 50 | rule: 'RunAsAny' 51 | seLinux: 52 | rule: 'RunAsAny' 53 | supplementalGroups: 54 | rule: 'RunAsAny' 55 | fsGroup: 56 | rule: 'RunAsAny' 57 | ` 58 | ) 59 | 60 | func readPSPYaml(pspInput string) (*v1beta1.PodSecurityPolicy, error) { 61 | var psp v1beta1.PodSecurityPolicy 62 | 63 | pspRestrictedJSON, err := yaml.YAMLToJSON([]byte(pspInput)) 64 | if err != nil { 65 | return nil, err 66 | } 67 | 68 | var anyJson map[string]interface{} 69 | 70 | err = json.Unmarshal(pspRestrictedJSON, &anyJson) 71 | 72 | if err != nil { 73 | return nil, err 74 | } 75 | 76 | decoder := json.NewDecoder(bytes.NewReader(pspRestrictedJSON)) 77 | decoder.DisallowUnknownFields() 78 | 79 | switch kind := anyJson["kind"]; kind { 80 | case "PodSecurityPolicy": 81 | if err := decoder.Decode(&psp); err != nil { 82 | return nil, err 83 | } 84 | default: 85 | return nil, fmt.Errorf("not a valid psp file") 86 | } 87 | 88 | return &psp, nil 89 | } 90 | -------------------------------------------------------------------------------- /advisor/types/portrange.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | ) 7 | 8 | type PortRangeList []*PortRange 9 | 10 | type PortRange struct { 11 | Min int32 12 | Max int32 13 | } 14 | 15 | func NewPortRange(min, max int32) *PortRange { 16 | return &PortRange{ 17 | Min: min, 18 | Max: max, 19 | } 20 | } 21 | 22 | func (pl PortRangeList) Consolidate() PortRangeList { 23 | newPortRangeList := PortRangeList{} 24 | 25 | max := pl.GetMax() 26 | 27 | min := pl.GetMin() 28 | 29 | if min == -1 { 30 | return newPortRangeList 31 | } 32 | 33 | tmpPl := make(PortRangeList, max+1) 34 | 35 | for _, pr := range pl { 36 | tmpPl[pr.Min] = pr 37 | } 38 | 39 | pr := tmpPl[min] 40 | i := min 41 | 42 | for ; i <= max; i++ { 43 | if tmpPl[i] != nil { 44 | pr.Max = tmpPl[i].Max 45 | } else { 46 | // there is a break 47 | newPortRangeList = append(newPortRangeList, pr) 48 | 49 | // look for next port range 50 | for { 51 | i++ 52 | if i > max { 53 | break 54 | } 55 | 56 | if tmpPl[i] != nil { 57 | pr = tmpPl[i] 58 | break 59 | } 60 | } 61 | } 62 | } 63 | 64 | newPortRangeList = append(newPortRangeList, pr) 65 | 66 | sort.Sort(newPortRangeList) 67 | 68 | return newPortRangeList 69 | } 70 | 71 | func (pl PortRangeList) GetMin() int32 { 72 | min := int32(-1) 73 | 74 | for _, pr := range pl { 75 | if pr != nil { 76 | if min == int32(-1) { 77 | min = pr.Min 78 | } 79 | 80 | if pr.Min < min { 81 | min = pr.Min 82 | } 83 | } 84 | } 85 | 86 | return min 87 | } 88 | 89 | func (pl PortRangeList) GetMax() int32 { 90 | max := int32(-1) 91 | 92 | for _, pr := range pl { 93 | if pr != nil { 94 | if pr.Max > max { 95 | max = pr.Max 96 | } 97 | } 98 | } 99 | 100 | return max 101 | } 102 | 103 | func (pl PortRangeList) String() string { 104 | ret := "[" 105 | 106 | for idx, pr := range pl { 107 | ret += fmt.Sprintf("{%d %d}", pr.Min, pr.Max) 108 | 109 | if idx < len(pl)-1 { 110 | ret += ", " 111 | } 112 | } 113 | 114 | ret += "]" 115 | 116 | return ret 117 | } 118 | 119 | func (pl PortRangeList) Less(i, j int) bool { return pl[i].Min < pl[j].Min } 120 | 121 | func (pl PortRangeList) Len() int { return len(pl) } 122 | 123 | func (pl PortRangeList) Swap(i, j int) { pl[j], pl[i] = pl[i], pl[j] } 124 | -------------------------------------------------------------------------------- /advisor/types/portrange_test.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | var ( 8 | prList = PortRangeList{ 9 | &PortRange{Min: 1, Max: 1}, 10 | &PortRange{Min: 2, Max: 2}, 11 | &PortRange{Min: 3, Max: 3}, 12 | &PortRange{Min: 5, Max: 5}, 13 | &PortRange{Min: 6, Max: 6}, 14 | &PortRange{Min: 7, Max: 7}, 15 | &PortRange{Min: 50, Max: 50}, 16 | &PortRange{Min: 99, Max: 99}, 17 | } 18 | 19 | expectedPrList = PortRangeList{ 20 | &PortRange{Min: 1, Max: 3}, 21 | &PortRange{Min: 5, Max: 7}, 22 | &PortRange{Min: 50, Max: 50}, 23 | &PortRange{Min: 99, Max: 99}, 24 | } 25 | ) 26 | 27 | func TestPortRange(t *testing.T) { 28 | newPrList := prList.Consolidate() 29 | 30 | if len(newPrList) != 4 { 31 | t.Errorf("length is not 3: %+v", newPrList) 32 | } 33 | 34 | for i := range expectedPrList { 35 | if newPrList[i].Min != expectedPrList[i].Min || newPrList[i].Max != expectedPrList[i].Max { 36 | t.Errorf("expected port range: %v; actual port range: %v", *expectedPrList[i], *newPrList[i]) 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /advisor/types/pspgrant.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "k8s.io/api/policy/v1beta1" 8 | rbacv1 "k8s.io/api/rbac/v1" 9 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 10 | ) 11 | 12 | const ( 13 | rbacV1APIVersion = "rbac.authorization.k8s.io/v1" 14 | rbacAPIGroup = "rbac.authorization.k8s.io" 15 | Role = "Role" 16 | RoleBinding = "RoleBinding" 17 | ServiceAccount = "ServiceAccount" 18 | ) 19 | 20 | type SASecuritySpecList []*SASecuritySpec 21 | 22 | func (sl SASecuritySpecList) Less(i, j int) bool { 23 | keyI := sl[i].Key() 24 | keyJ := sl[j].Key() 25 | 26 | return keyI < keyJ 27 | } 28 | 29 | func (sl SASecuritySpecList) Len() int { return len(sl) } 30 | 31 | func (sl SASecuritySpecList) Swap(i, j int) { sl[i], sl[j] = sl[j], sl[i] } 32 | 33 | type SASecuritySpec struct { 34 | PSPName string // psp name 35 | 36 | ServiceAccount string // serviceAccount 37 | 38 | Namespace string // namespace 39 | 40 | ContainerSecuritySpecList []ContainerSecuritySpec 41 | 42 | PodSecuritySpecList []PodSecuritySpec 43 | } 44 | 45 | func NewSASecuritySpec(ns, sa string) *SASecuritySpec { 46 | return &SASecuritySpec{ 47 | ServiceAccount: sa, 48 | Namespace: ns, 49 | ContainerSecuritySpecList: []ContainerSecuritySpec{}, 50 | PodSecuritySpecList: []PodSecuritySpec{}, 51 | } 52 | } 53 | 54 | func (s *SASecuritySpec) Key() string { 55 | return fmt.Sprintf("%s:%s", s.Namespace, s.ServiceAccount) 56 | } 57 | 58 | // IsDefaultServiceAccount returns whether the service account is default 59 | func (s *SASecuritySpec) IsDefaultServiceAccount() bool { 60 | return s.ServiceAccount == "default" 61 | } 62 | 63 | // AddContainerSecuritySpec adds container security spec object to the associated service account 64 | func (s *SASecuritySpec) AddContainerSecuritySpec(css ContainerSecuritySpec) { 65 | s.ContainerSecuritySpecList = append(s.ContainerSecuritySpecList, css) 66 | } 67 | 68 | // AddPodSecuritySpec adds pod security spec object to the associated service account 69 | func (s *SASecuritySpec) AddPodSecuritySpec(pss PodSecuritySpec) { 70 | s.PodSecuritySpecList = append(s.PodSecuritySpecList, pss) 71 | } 72 | 73 | // GeneratePSPName generates psp name 74 | func (s *SASecuritySpec) GeneratePSPName() string { 75 | if s.PSPName == "" { 76 | s.PSPName = fmt.Sprintf("psp-for-%s-%s", s.Namespace, s.ServiceAccount) 77 | } 78 | 79 | return s.PSPName 80 | } 81 | 82 | // GenerateComment generate comments for the psp grants (no psp will be created for default service account) 83 | func (s *SASecuritySpec) GenerateComment() string { 84 | decision := "will be" 85 | 86 | if s.IsDefaultServiceAccount() { 87 | decision = "will NOT be" 88 | } 89 | 90 | commentsForWorkloads := []string{} 91 | comment := fmt.Sprintf("# Pod security policies %s created for service account '%s' in namespace '%s' with following workdloads:\n", decision, s.ServiceAccount, s.Namespace) 92 | for _, wlImg := range s.GetWorkloadImages() { 93 | commentsForWorkloads = append(commentsForWorkloads, fmt.Sprintf("#\t%s", wlImg)) 94 | } 95 | 96 | comment += strings.Join(commentsForWorkloads, "\n") 97 | return comment 98 | } 99 | 100 | // GetWorkloadImages returns a list of workload images in the format of "kind, Name, Image Name" 101 | func (s *SASecuritySpec) GetWorkloadImages() []string { 102 | workLoadImageList := []string{} 103 | 104 | for _, css := range s.ContainerSecuritySpecList { 105 | workLoadImage := fmt.Sprintf("Kind: %s, Name: %s, Image: %s", css.Metadata.Kind, css.Metadata.Name, css.ImageName) 106 | workLoadImageList = append(workLoadImageList, workLoadImage) 107 | } 108 | 109 | return workLoadImageList 110 | } 111 | 112 | // GenerateRole creates a role object contains the privilege to use the psp 113 | func (s *SASecuritySpec) GenerateRole() *rbacv1.Role { 114 | roleName := fmt.Sprintf("use-psp-by-%s:%s", s.Namespace, s.ServiceAccount) 115 | 116 | rule := rbacv1.PolicyRule{ 117 | Verbs: []string{"use"}, 118 | APIGroups: []string{"policy"}, 119 | Resources: []string{"podsecuritypolicies"}, 120 | ResourceNames: []string{s.GeneratePSPName()}, 121 | } 122 | 123 | return &rbacv1.Role{ 124 | TypeMeta: metav1.TypeMeta{ 125 | Kind: Role, 126 | APIVersion: rbacV1APIVersion, 127 | }, 128 | ObjectMeta: metav1.ObjectMeta{ 129 | Namespace: s.Namespace, 130 | Name: roleName, 131 | }, 132 | Rules: []rbacv1.PolicyRule{rule}, 133 | } 134 | } 135 | 136 | // GenerateRoleBinding creates a rolebinding for the service account to use the psp 137 | func (s *SASecuritySpec) GenerateRoleBinding() *rbacv1.RoleBinding { 138 | roleBindingName := fmt.Sprintf("use-psp-by-%s:%s-binding", s.Namespace, s.ServiceAccount) 139 | roleName := fmt.Sprintf("use-psp-by-%s:%s", s.Namespace, s.ServiceAccount) 140 | 141 | return &rbacv1.RoleBinding{ 142 | TypeMeta: metav1.TypeMeta{ 143 | Kind: RoleBinding, 144 | APIVersion: rbacV1APIVersion, 145 | }, 146 | ObjectMeta: metav1.ObjectMeta{ 147 | Namespace: s.Namespace, 148 | Name: roleBindingName, 149 | }, 150 | Subjects: []rbacv1.Subject{ 151 | {Kind: ServiceAccount, Name: s.ServiceAccount, Namespace: s.Namespace}, 152 | }, 153 | RoleRef: rbacv1.RoleRef{ 154 | APIGroup: rbacAPIGroup, 155 | Kind: Role, 156 | Name: roleName, 157 | }, 158 | } 159 | } 160 | 161 | type PSPGrantList []PSPGrant 162 | 163 | func (pgl PSPGrantList) ToMap() map[string]PSPGrant { 164 | m := map[string]PSPGrant{} 165 | 166 | for _, pg := range pgl { 167 | m[pg.Key()] = pg 168 | } 169 | 170 | return m 171 | } 172 | 173 | type PSPGrant struct { 174 | Comment string 175 | PodSecurityPolicy *v1beta1.PodSecurityPolicy 176 | Role *rbacv1.Role 177 | RoleBinding *rbacv1.RoleBinding 178 | ServiceAccount string 179 | Namespace string 180 | } 181 | 182 | func (pg PSPGrant) Key() string { 183 | return fmt.Sprintf("%s:%s", pg.Namespace, pg.ServiceAccount) 184 | } 185 | -------------------------------------------------------------------------------- /advisor/types/securityspec.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | var ( 4 | DefaultCaps = []string{ 5 | "SETPCAP", 6 | "MKNOD", 7 | "AUDIT_WRITE", 8 | "CHOWN", 9 | "NET_RAW", 10 | "DAC_OVERRIDE", 11 | "FOWNER", 12 | "FSETID", 13 | "KILL", 14 | "SETGID", 15 | "SETUID", 16 | "NET_BIND_SERVICE", 17 | "SYS_CHROOT", 18 | "SETFCAP", 19 | } 20 | ) 21 | 22 | const ( 23 | Version1_11 = "v1.11" 24 | ) 25 | 26 | //PodSecurityPolicy Recommendation System help in the following attributes: 27 | // 1. allowPrivilegeEscalation - done 28 | // 2. allowedCapabilities - done 29 | // 3. allowedHostPaths - done 30 | // 4. hostIPC - done 31 | // 5. hostNetwork - done 32 | // 6. hostPID - done 33 | // 7. hostPorts - done 34 | // 8. privileged - done 35 | // 9. readOnlyRootFilesystem - done 36 | // 10. runAsUser - done 37 | // 11. runAsGroup - done 38 | // 12. Volume - done 39 | // 13. seLinux and others - need further investigation 40 | // 14. allowedUnsafeSysctls - done 41 | 42 | type VolumeMount struct { 43 | MountPath string `json:"mountPath"` 44 | Name string `json:"name"` 45 | SubPath string `json:"subPath,omitempty"` 46 | ReadOnly bool `json:"readOnly,omitempty"` 47 | SubPathExpr string `json:"subPathExpr,omitempty"` 48 | } 49 | 50 | func (vm VolumeMount) IsReadOnlyMount() bool { 51 | return vm.ReadOnly == true 52 | } 53 | 54 | func (vm VolumeMount) UsesSubPath() bool { 55 | if vm.SubPath != "" || vm.SubPathExpr != "" { 56 | return true 57 | } 58 | 59 | return false 60 | } 61 | 62 | type ContainerSecuritySpec struct { 63 | Metadata Metadata `json:"parentMetadata"` 64 | ContainerID string `json:"containerID"` 65 | ContainerName string `json:"containerName"` 66 | PodName string `json:"podName"` 67 | Namespace string `json:"namespace"` 68 | ImageName string `json:"imageName"` 69 | ImageSHA string `json:"imageSHA"` 70 | HostName string `json:"hostName"` 71 | Capabilities []string `json:"effectiveCapabilities,omitempty"` 72 | DroppedCap []string `json:"droppedCapabilities,omitempty"` 73 | AddedCap []string `json:"addedCapabilities,omitempty"` 74 | Privileged bool `json:"privileged,omitempty"` 75 | ReadOnlyRootFS bool `json:"readOnlyRootFileSystem,omitempty"` 76 | RunAsNonRoot *bool `json:"runAsNonRoot,omitempty"` 77 | AllowPrivilegeEscalation *bool `json:"allowPrivilegeEscalation,omitempty"` 78 | RunAsUser *int64 `json:"runAsUser,omitempty"` 79 | RunAsGroup *int64 `json:"runAsGroup,omitempty"` 80 | HostPorts []int32 `json:"hostPorts,omitempty"` 81 | ServiceAccount string `json:"serviceAccount,omitempty"` 82 | VolumeMounts []VolumeMount `json:"volumeMounts"` 83 | } 84 | 85 | type PodSecuritySpec struct { 86 | Metadata Metadata `json:"metadata"` 87 | Namespace string `json:"namespace"` 88 | HostPID bool `json:"hostPID,omitempty"` 89 | HostNetwork bool `json:"hostNetwork,omitempty"` 90 | HostIPC bool `json:"hostIPC,omitempty"` 91 | VolumeTypes []string `json:"volumeTypes,omitempty"` 92 | VolumeMounts map[string]bool `json:"volumeMounts,omitempty"` //--> NEW 93 | MountHostPaths map[string]bool `json:"mountedHostPath,omitempty"` 94 | ServiceAccount string `json:"serviceAccount,omitempty"` 95 | Sysctls []string `json:"sysctls,omitempty"` 96 | } 97 | 98 | type Metadata struct { 99 | Name string `json:"name"` 100 | Kind string `json:"kind"` 101 | Namespace string `json:"namespace"` 102 | YamlFile string `json:"file,omitempty"` 103 | Image string `json:"image,omitempty"` 104 | } 105 | 106 | type PodSecuritySpecMap map[Metadata]PodSecuritySpec 107 | 108 | func NewPodSecuritySpecMap(pssList []PodSecuritySpec) PodSecuritySpecMap { 109 | pssMap := PodSecuritySpecMap{} 110 | 111 | for _, pss := range pssList { 112 | pssMap[pss.Metadata] = pss 113 | } 114 | 115 | return pssMap 116 | } 117 | 118 | type ContainerSecuritySpecMap map[Metadata]ContainerSecuritySpec 119 | 120 | func NewContainerSecuritySpecMap(cssList []ContainerSecuritySpec) ContainerSecuritySpecMap { 121 | cssMap := ContainerSecuritySpecMap{} 122 | for _, css := range cssList { 123 | css.Metadata.Image = css.ImageName 124 | cssMap[css.Metadata] = css 125 | } 126 | 127 | return cssMap 128 | } 129 | -------------------------------------------------------------------------------- /comparator/comparator.go: -------------------------------------------------------------------------------- 1 | package comparator 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | "github.com/sysdiglabs/kube-psp-advisor/generator" 8 | 9 | "github.com/sysdiglabs/kube-psp-advisor/advisor/types" 10 | ) 11 | 12 | const ( 13 | Source = "Source" 14 | Target = "Target" 15 | ) 16 | 17 | type Comparator struct { 18 | escalationReport *types.LintReport 19 | gen *generator.Generator 20 | srcCssList []types.ContainerSecuritySpec 21 | srcPssList []types.PodSecuritySpec 22 | targetCssList []types.ContainerSecuritySpec 23 | targetPssList []types.PodSecuritySpec 24 | } 25 | 26 | // NewComparator returns a new comparator object 27 | func NewComparator() (*Comparator, error) { 28 | gen, err := generator.NewGenerator() 29 | 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | return &Comparator{ 35 | gen: gen, 36 | escalationReport: types.NewEscalationReport(), 37 | srcCssList: []types.ContainerSecuritySpec{}, 38 | srcPssList: []types.PodSecuritySpec{}, 39 | targetCssList: []types.ContainerSecuritySpec{}, 40 | targetPssList: []types.PodSecuritySpec{}, 41 | }, nil 42 | } 43 | 44 | // LoadYamls loads yamls from files 45 | func (c *Comparator) LoadYamls(yamls []string, dirType string) error { 46 | if dirType != Source && dirType != Target { 47 | return fmt.Errorf("invalid directory type: %s (expected 'Source' or 'Target')", dirType) 48 | } 49 | cssList := []types.ContainerSecuritySpec{} 50 | pssList := []types.PodSecuritySpec{} 51 | for _, yamlFile := range yamls { 52 | csl, psl, err := c.gen.LoadYaml(yamlFile) 53 | if err != nil { 54 | return err 55 | } 56 | 57 | if len(csl) > 0 { 58 | cssList = append(cssList, csl...) 59 | pssList = append(pssList, psl...) 60 | } 61 | } 62 | 63 | if dirType == Source { 64 | c.srcCssList = cssList 65 | c.srcPssList = pssList 66 | } else { 67 | c.targetCssList = cssList 68 | c.targetPssList = pssList 69 | } 70 | 71 | return nil 72 | } 73 | 74 | // Compare compares security contexts between the source YAMLs and target YAMLs 75 | func (c *Comparator) Compare() { 76 | c.escalationReport.GenerateEscalationReportFromSecurityContext(c.srcCssList, c.targetCssList, c.srcPssList, c.targetPssList) 77 | } 78 | 79 | // Clear clears everything in the comparator 80 | func (c *Comparator) Clear() { 81 | c.srcCssList = []types.ContainerSecuritySpec{} 82 | c.targetCssList = []types.ContainerSecuritySpec{} 83 | c.srcPssList = []types.PodSecuritySpec{} 84 | c.targetPssList = []types.PodSecuritySpec{} 85 | 86 | c.escalationReport = types.NewEscalationReport() 87 | } 88 | 89 | // PrintEscalationReport prints escalation report to STDOUT 90 | func (c *Comparator) PrintEscalationReport() { 91 | data, _ := json.Marshal(c.escalationReport) 92 | 93 | fmt.Println(string(data)) 94 | 95 | } 96 | -------------------------------------------------------------------------------- /container/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.12.9-alpine3.10 AS builder 2 | RUN apk add --no-cache build-base git 3 | WORKDIR $GOPATH/src/kube-psp-advisor 4 | COPY . $GOPATH/src/kube-psp-advisor 5 | RUN env GO111MODULE=on GOOS=$(uname -s | tr '[:upper:]' '[:lower:]') GOARCH=amd64 go build -a 6 | 7 | FROM alpine 8 | RUN apk update && apk add ca-certificates && rm -rf /var/cache/apk/* 9 | COPY --from=builder /go/src/kube-psp-advisor/kube-psp-advisor /kube-psp-advisor 10 | 11 | ENTRYPOINT ["/kube-psp-advisor"] 12 | CMD ["inspect"] 13 | -------------------------------------------------------------------------------- /examples/README.MD: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | ## Environment 4 | **Namespace** | **Service Account** | **Role** | **Pod Security Policy** 5 | --- | --- | --- | --- 6 | privileged | privileged-sa | psp-privileged-role | psp-privielged 7 | restricted | restricted-sa | psp-restricted-role | psp-restricted 8 | 9 | In order to enforce the pod security policies, the service accounts need to be authorized to use the pod security policy. 10 | 11 | **Role** | **Rule Verb** | **Rule API Group** | **Rule Resources** | **Rule Resource Name** 12 | --- | --- | --- | --- | --- 13 | psp-privileged-role | use | policy | podsecuritypolicies | psp-privileged 14 | psp-restricted-role | use | policy | podsecuritypolicies | psp-restricted 15 | 16 | ## Try out examples 17 | 1. setup k8s cluster 18 | 2. run `make build` to build the `kube-psp-advisor` binary 19 | 3. run `make example` to setup the example environment and show the test scenarios 20 | 21 | ## Explanation of examples 22 | 1. deploy `restrcited` and `privileged` namespaces with services account, roles, rolebindings and pods to k8s cluster 23 | 2. `kube-psp-advisor` generates pod security policies seperately for namespaces `restricted` and `privileged` based on the running pods, and then apply the pod security policies to cluster 24 | 3. test pod security policies with pods that comply with pod security policies (pods-allows.yaml) 25 | 4. test pod security policies with pods that violate pod security policies (pods-deny.yaml) 26 | -------------------------------------------------------------------------------- /examples/ns.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: restricted 5 | --- 6 | apiVersion: v1 7 | kind: Namespace 8 | metadata: 9 | name: privileged 10 | -------------------------------------------------------------------------------- /examples/pods-allow.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: privileged-busybox-0 5 | namespace: privileged 6 | spec: 7 | serviceAccountName: privileged-sa 8 | containers: 9 | - name: my-busybox-container 10 | image: busybox 11 | args: ["sleep", "3000"] 12 | securityContext: 13 | privileged: false 14 | runAsNonRoot: false 15 | readOnlyRootFilesystem: false 16 | allowPrivilegeEscalation: true 17 | hostPID: true 18 | hostIPC: false 19 | hostNetwork: true 20 | --- 21 | apiVersion: v1 22 | kind: Pod 23 | metadata: 24 | name: restricted-busybox-0 25 | namespace: restricted 26 | spec: 27 | serviceAccountName: restricted-sa 28 | containers: 29 | - name: my-busybox-container 30 | image: busybox 31 | args: ["sleep", "3000"] 32 | securityContext: 33 | privileged: false 34 | runAsNonRoot: false 35 | readOnlyRootFilesystem: false 36 | allowPrivilegeEscalation: false 37 | hostPID: false 38 | hostIPC: false 39 | hostNetwork: false 40 | -------------------------------------------------------------------------------- /examples/pods-deny.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: restricted-busybox-1 5 | namespace: restricted 6 | spec: 7 | serviceAccountName: restricted-sa 8 | containers: 9 | - name: busybox-container-1 10 | image: busybox 11 | args: ["sleep", "3000"] 12 | securityContext: 13 | privileged: true # try to launch a privileged pod in restricted namespace 14 | runAsNonRoot: false 15 | readOnlyRootFilesystem: false 16 | allowPrivilegeEscalation: false 17 | capabilities: 18 | drop: 19 | - SYS_CHROOT 20 | hostPID: false 21 | hostIPC: false 22 | hostNetwork: false 23 | --- 24 | apiVersion: v1 25 | kind: Pod 26 | metadata: 27 | name: privileged-busybox-1 28 | namespace: privileged 29 | spec: 30 | serviceAccountName: restricted-sa # restricted-sa is trying to launch a privileged pod in privileged namespace 31 | containers: 32 | - name: busybox-container-1 33 | image: busybox 34 | args: ["sleep", "3000"] 35 | securityContext: 36 | privileged: true # try to launch a privileged pod in privileged namespace 37 | runAsNonRoot: false 38 | readOnlyRootFilesystem: false 39 | allowPrivilegeEscalation: false 40 | capabilities: 41 | drop: 42 | - SYS_CHROOT 43 | hostPID: false 44 | hostIPC: false 45 | hostNetwork: false 46 | -------------------------------------------------------------------------------- /examples/pods.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: privileged-busybox-base 5 | namespace: privileged 6 | spec: 7 | serviceAccountName: privileged-sa 8 | containers: 9 | - name: my-busybox-container 10 | image: busybox 11 | args: ["sleep", "3000"] 12 | securityContext: 13 | capabilities: 14 | add: 15 | - SYS_ADMIN 16 | drop: 17 | - SYS_CHROOT 18 | privileged: true 19 | runAsNonRoot: false 20 | readOnlyRootFilesystem: false 21 | allowPrivilegeEscalation: true 22 | hostPID: true 23 | hostIPC: true 24 | hostNetwork: true 25 | --- 26 | apiVersion: v1 27 | kind: Pod 28 | metadata: 29 | name: restricted-busybox-base 30 | namespace: restricted 31 | spec: 32 | serviceAccountName: restricted-sa 33 | containers: 34 | - name: my-busybox-container 35 | image: busybox 36 | args: ["sleep", "3000"] 37 | securityContext: 38 | capabilities: 39 | drop: 40 | - SYS_CHROOT 41 | privileged: false 42 | runAsNonRoot: false 43 | readOnlyRootFilesystem: false 44 | allowPrivilegeEscalation: false 45 | hostPID: false 46 | hostIPC: false 47 | hostNetwork: false 48 | -------------------------------------------------------------------------------- /examples/roles.yaml: -------------------------------------------------------------------------------- 1 | kind: Role 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | metadata: 4 | name: psp-restricted-role 5 | namespace: restricted 6 | rules: 7 | - apiGroups: ['policy'] 8 | resources: ['podsecuritypolicies'] 9 | resourceNames: ['psp-restricted'] 10 | verbs: ['use'] 11 | --- 12 | kind: Role 13 | apiVersion: rbac.authorization.k8s.io/v1 14 | metadata: 15 | name: psp-privileged-role 16 | namespace: privileged 17 | rules: 18 | - apiGroups: ['policy'] 19 | resources: ['podsecuritypolicies'] 20 | resourceNames: ['psp-privileged'] 21 | verbs: ['use'] 22 | --- 23 | apiVersion: rbac.authorization.k8s.io/v1 24 | kind: RoleBinding 25 | metadata: 26 | name: psp-restricted-rolebinding 27 | namespace: restricted 28 | roleRef: 29 | apiGroup: rbac.authorization.k8s.io 30 | kind: Role 31 | name: psp-restricted-role 32 | subjects: 33 | - kind: ServiceAccount 34 | name: restricted-sa 35 | namespace: restricted 36 | --- 37 | apiVersion: rbac.authorization.k8s.io/v1 38 | kind: RoleBinding 39 | metadata: 40 | name: psp-privileged-rolebinding 41 | namespace: privileged 42 | roleRef: 43 | apiGroup: rbac.authorization.k8s.io 44 | kind: Role 45 | name: psp-privileged-role 46 | subjects: 47 | - kind: ServiceAccount 48 | name: privileged-sa 49 | namespace: privileged 50 | -------------------------------------------------------------------------------- /examples/sa.yaml: -------------------------------------------------------------------------------- 1 | kind: ServiceAccount 2 | apiVersion: v1 3 | metadata: 4 | name: restricted-sa 5 | namespace: restricted 6 | --- 7 | kind: ServiceAccount 8 | apiVersion: v1 9 | metadata: 10 | name: privileged-sa 11 | namespace: privileged 12 | -------------------------------------------------------------------------------- /generator/generator.go: -------------------------------------------------------------------------------- 1 | package generator 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io/ioutil" 8 | "log" 9 | "os" 10 | "path/filepath" 11 | "strconv" 12 | 13 | "github.com/open-policy-agent/opa/ast" 14 | 15 | "k8s.io/client-go/kubernetes/scheme" 16 | 17 | "github.com/ghodss/yaml" 18 | 19 | "github.com/sysdiglabs/kube-psp-advisor/advisor/types" 20 | "github.com/sysdiglabs/kube-psp-advisor/utils" 21 | 22 | appsv1 "k8s.io/api/apps/v1" 23 | batch "k8s.io/api/batch/v1" 24 | batchv1beta1 "k8s.io/api/batch/v1beta1" 25 | corev1 "k8s.io/api/core/v1" 26 | policyv1beta1 "k8s.io/api/policy/v1beta1" 27 | 28 | "reflect" 29 | "strings" 30 | "time" 31 | ) 32 | 33 | const ( 34 | volumeTypeSecret = "secret" 35 | ) 36 | 37 | type Generator struct { 38 | } 39 | 40 | func NewGenerator() (*Generator, error) { 41 | 42 | return &Generator{}, nil 43 | } 44 | 45 | func getVolumeTypes(spec corev1.PodSpec, sa *corev1.ServiceAccount) (volumeTypes []string) { 46 | volumeTypeMap := map[string]bool{} 47 | for _, v := range spec.Volumes { 48 | if volumeType := getVolumeType(v); volumeType != "" { 49 | volumeTypeMap[getVolumeType(v)] = true 50 | } 51 | } 52 | 53 | // If don't opt out of automounting API credentials for a service account 54 | // or a particular pod, "secret" needs to be into PSP allowed volume types. 55 | if sa == nil || mountServiceAccountToken(spec, *sa) { 56 | volumeTypeMap[volumeTypeSecret] = true 57 | } 58 | 59 | volumeTypes = utils.MapToArray(volumeTypeMap) 60 | return 61 | } 62 | 63 | //NEW OPA 64 | func getVolumeMounts(spec corev1.PodSpec) map[string]bool { 65 | containerMountMap := map[string]bool{} 66 | 67 | for _, c := range spec.Containers { 68 | for _, vm := range c.VolumeMounts { 69 | if _, exists := containerMountMap[vm.Name]; !exists { 70 | containerMountMap[vm.Name] = vm.ReadOnly 71 | } else { 72 | containerMountMap[vm.Name] = containerMountMap[vm.Name] && vm.ReadOnly 73 | } 74 | } 75 | } 76 | 77 | return containerMountMap 78 | } 79 | 80 | func getVolumeHostPaths(spec corev1.PodSpec) map[string]bool { 81 | hostPathMap := map[string]bool{} 82 | 83 | containerMountMap := map[string]bool{} 84 | 85 | for _, c := range spec.Containers { 86 | for _, vm := range c.VolumeMounts { 87 | if _, exists := containerMountMap[vm.Name]; !exists { 88 | containerMountMap[vm.Name] = vm.ReadOnly 89 | } else { 90 | containerMountMap[vm.Name] = containerMountMap[vm.Name] && vm.ReadOnly 91 | } 92 | } 93 | } 94 | 95 | for _, c := range spec.InitContainers { 96 | for _, vm := range c.VolumeMounts { 97 | if _, exists := containerMountMap[vm.Name]; !exists { 98 | containerMountMap[vm.Name] = vm.ReadOnly 99 | } else { 100 | containerMountMap[vm.Name] = containerMountMap[vm.Name] && vm.ReadOnly 101 | } 102 | } 103 | } 104 | 105 | for _, v := range spec.Volumes { 106 | if v.HostPath != nil { 107 | if _, exists := containerMountMap[v.Name]; exists { 108 | hostPathMap[v.HostPath.Path] = containerMountMap[v.Name] 109 | } 110 | } 111 | } 112 | 113 | return hostPathMap 114 | } 115 | 116 | func getVolumeType(v corev1.Volume) string { 117 | val := reflect.ValueOf(v.VolumeSource) 118 | for i := 0; i < val.Type().NumField(); i++ { 119 | if !val.Field(i).IsNil() { 120 | protos := strings.Split(val.Type().Field(i).Tag.Get("protobuf"), ",") 121 | for _, p := range protos { 122 | if strings.HasPrefix(p, "name=") { 123 | return p[5:] 124 | } 125 | } 126 | } 127 | } 128 | return "" 129 | } 130 | 131 | func getRunAsUser(sc *corev1.SecurityContext, psc *corev1.PodSecurityContext) *int64 { 132 | if sc == nil { 133 | if psc != nil { 134 | return psc.RunAsUser 135 | } 136 | return nil 137 | } 138 | 139 | return sc.RunAsUser 140 | } 141 | 142 | func getRunAsGroup(sc *corev1.SecurityContext, psc *corev1.PodSecurityContext) *int64 { 143 | if sc == nil { 144 | if psc != nil { 145 | return psc.RunAsGroup 146 | } 147 | return nil 148 | } 149 | 150 | return sc.RunAsGroup 151 | } 152 | 153 | func getHostPorts(containerPorts []corev1.ContainerPort) (hostPorts []int32) { 154 | for _, p := range containerPorts { 155 | hostPorts = append(hostPorts, p.HostPort) 156 | } 157 | return 158 | } 159 | 160 | func getEffectiveCapablities(add, drop []string) (effectiveCaps []string) { 161 | dropCapMap := utils.ArrayToMap(drop) 162 | addCapMap := utils.ArrayToMap(add) 163 | defaultCaps := types.DefaultCaps 164 | effectiveCapMap := map[string]bool{} 165 | 166 | for _, cap := range defaultCaps { 167 | if _, exists := dropCapMap[cap]; !exists { 168 | effectiveCapMap[cap] = true 169 | } 170 | } 171 | 172 | for cap := range addCapMap { 173 | if _, exists := dropCapMap[cap]; !exists { 174 | effectiveCapMap[cap] = true 175 | } 176 | } 177 | 178 | effectiveCaps = utils.MapToArray(effectiveCapMap) 179 | 180 | return 181 | } 182 | 183 | func getPrivileged(sc *corev1.SecurityContext) bool { 184 | if sc == nil { 185 | return false 186 | } 187 | 188 | if sc.Privileged == nil { 189 | return false 190 | } 191 | 192 | return *sc.Privileged 193 | } 194 | 195 | func getRunAsNonRootUser(sc *corev1.SecurityContext, psc *corev1.PodSecurityContext) *bool { 196 | if sc == nil { 197 | if psc != nil { 198 | return psc.RunAsNonRoot 199 | } 200 | return nil 201 | } 202 | 203 | return sc.RunAsNonRoot 204 | } 205 | 206 | func getAllowedPrivilegeEscalation(sc *corev1.SecurityContext) *bool { 207 | if sc == nil { 208 | return nil 209 | } 210 | 211 | return sc.AllowPrivilegeEscalation 212 | } 213 | 214 | func getIDs(podStatus corev1.PodStatus, containerName string) (containerID, imageID string) { 215 | containers := podStatus.ContainerStatuses 216 | for _, c := range containers { 217 | if c.Name == containerName { 218 | if len(c.ContainerID) > 0 { 219 | idx := strings.Index(c.ContainerID, "docker://") + 9 220 | if idx > len(c.ContainerID) { 221 | idx = 0 222 | } 223 | containerID = c.ContainerID[idx:] 224 | } 225 | 226 | if len(c.ImageID) > 0 { 227 | imageID = c.ImageID[strings.Index(c.ImageID, "sha256"):] 228 | } 229 | 230 | return 231 | } 232 | } 233 | return 234 | } 235 | 236 | func getReadOnlyRootFileSystem(sc *corev1.SecurityContext) bool { 237 | if sc == nil { 238 | return false 239 | } 240 | 241 | if sc.ReadOnlyRootFilesystem == nil { 242 | return false 243 | } 244 | 245 | return *sc.ReadOnlyRootFilesystem 246 | } 247 | 248 | func getCapabilities(sc *corev1.SecurityContext) (addList []string, dropList []string) { 249 | if sc == nil { 250 | return 251 | } 252 | 253 | if sc.Capabilities == nil { 254 | return 255 | } 256 | 257 | addCaps := sc.Capabilities.Add 258 | dropCaps := sc.Capabilities.Drop 259 | 260 | addCapMap := map[string]bool{} 261 | dropCapMap := map[string]bool{} 262 | 263 | for _, cap := range addCaps { 264 | addCapMap[string(cap)] = true 265 | } 266 | 267 | for _, cap := range dropCaps { 268 | dropCapMap[string(cap)] = true 269 | } 270 | 271 | // delete cap if exists both in the drop list and add list 272 | for cap := range addCapMap { 273 | if _, exists := dropCapMap[cap]; exists { 274 | delete(addCapMap, cap) 275 | delete(dropCapMap, cap) 276 | } 277 | } 278 | return utils.MapToArray(addCapMap), utils.MapToArray(dropCapMap) 279 | } 280 | 281 | func getSysctls(psc *corev1.PodSecurityContext) (sysctls []string) { 282 | if psc == nil { 283 | return 284 | } 285 | 286 | for _, s := range psc.Sysctls { 287 | sysctls = append(sysctls, s.Name) 288 | } 289 | 290 | return sysctls 291 | } 292 | 293 | func mountServiceAccountToken(spec corev1.PodSpec, sa corev1.ServiceAccount) bool { 294 | // First Pod's preference is checked 295 | if spec.AutomountServiceAccountToken != nil { 296 | return *spec.AutomountServiceAccountToken 297 | } 298 | 299 | // Then service account's 300 | if sa.AutomountServiceAccountToken != nil { 301 | return *sa.AutomountServiceAccountToken 302 | } 303 | 304 | return true 305 | } 306 | 307 | func (pg *Generator) GetSecuritySpecFromPodSpec(metadata types.Metadata, namespace string, spec corev1.PodSpec, sa *corev1.ServiceAccount) ([]types.ContainerSecuritySpec, types.PodSecuritySpec) { 308 | cssList := []types.ContainerSecuritySpec{} 309 | podSecuritySpec := types.PodSecuritySpec{ 310 | Metadata: metadata, 311 | Namespace: namespace, 312 | HostPID: spec.HostPID, 313 | HostNetwork: spec.HostNetwork, 314 | HostIPC: spec.HostIPC, 315 | VolumeTypes: getVolumeTypes(spec, sa), 316 | VolumeMounts: getVolumeMounts(spec), 317 | MountHostPaths: getVolumeHostPaths(spec), 318 | ServiceAccount: getServiceAccountName(spec), 319 | Sysctls: getSysctls(spec.SecurityContext), 320 | } 321 | 322 | for _, container := range spec.InitContainers { 323 | addCapList, dropCapList := getCapabilities(container.SecurityContext) 324 | csc := types.ContainerSecuritySpec{ 325 | Metadata: metadata, 326 | ContainerName: container.Name, 327 | ImageName: container.Image, 328 | PodName: metadata.Name, 329 | Namespace: namespace, 330 | HostName: spec.NodeName, 331 | Capabilities: getEffectiveCapablities(addCapList, dropCapList), 332 | AddedCap: addCapList, 333 | DroppedCap: dropCapList, 334 | ReadOnlyRootFS: getReadOnlyRootFileSystem(container.SecurityContext), 335 | RunAsNonRoot: getRunAsNonRootUser(container.SecurityContext, spec.SecurityContext), 336 | AllowPrivilegeEscalation: getAllowedPrivilegeEscalation(container.SecurityContext), 337 | Privileged: getPrivileged(container.SecurityContext), 338 | RunAsGroup: getRunAsGroup(container.SecurityContext, spec.SecurityContext), 339 | RunAsUser: getRunAsUser(container.SecurityContext, spec.SecurityContext), 340 | HostPorts: getHostPorts(container.Ports), 341 | ServiceAccount: getServiceAccountName(spec), 342 | } 343 | cssList = append(cssList, csc) 344 | } 345 | 346 | for _, container := range spec.Containers { 347 | addCapList, dropCapList := getCapabilities(container.SecurityContext) 348 | csc := types.ContainerSecuritySpec{ 349 | Metadata: metadata, 350 | ContainerName: container.Name, 351 | ImageName: container.Image, 352 | PodName: metadata.Name, 353 | Namespace: namespace, 354 | HostName: spec.NodeName, 355 | Capabilities: getEffectiveCapablities(addCapList, dropCapList), 356 | AddedCap: addCapList, 357 | DroppedCap: dropCapList, 358 | ReadOnlyRootFS: getReadOnlyRootFileSystem(container.SecurityContext), 359 | RunAsNonRoot: getRunAsNonRootUser(container.SecurityContext, spec.SecurityContext), 360 | AllowPrivilegeEscalation: getAllowedPrivilegeEscalation(container.SecurityContext), 361 | Privileged: getPrivileged(container.SecurityContext), 362 | RunAsGroup: getRunAsGroup(container.SecurityContext, spec.SecurityContext), 363 | RunAsUser: getRunAsUser(container.SecurityContext, spec.SecurityContext), 364 | HostPorts: getHostPorts(container.Ports), 365 | ServiceAccount: getServiceAccountName(spec), 366 | VolumeMounts: getContainerVolumeMounts(container.VolumeMounts), 367 | } 368 | cssList = append(cssList, csc) 369 | } 370 | return cssList, podSecuritySpec 371 | } 372 | 373 | func (pg *Generator) GeneratePSP(cssList []types.ContainerSecuritySpec, 374 | pssList []types.PodSecuritySpec, 375 | namespace, serverGitVersion string) *policyv1beta1.PodSecurityPolicy { 376 | 377 | return pg.GeneratePSPWithName(cssList, pssList, namespace, serverGitVersion, "") 378 | } 379 | 380 | func (pg *Generator) GenerateOPA(cssList []types.ContainerSecuritySpec, 381 | pssList []types.PodSecuritySpec, 382 | namespace, serverGitVersion string, OPAdefaultRule bool) *ast.Module { 383 | 384 | return pg.GenerateOPAWithName(cssList, pssList, namespace, serverGitVersion, "", OPAdefaultRule) 385 | } 386 | 387 | func (pg *Generator) GenerateOPAPod(cssList []types.ContainerSecuritySpec, 388 | pssList []types.PodSecuritySpec, 389 | namespace, serverGitVersion string, OPAdefaultRule bool) *ast.Module { 390 | 391 | return pg.GenerateOPAWithName(cssList, pssList, namespace, serverGitVersion, "", OPAdefaultRule) 392 | } 393 | 394 | // GeneratePSP generate Pod Security Policy 395 | func (pg *Generator) GeneratePSPWithName( 396 | cssList []types.ContainerSecuritySpec, 397 | pssList []types.PodSecuritySpec, 398 | namespace, serverGitVersion, pspName string) *policyv1beta1.PodSecurityPolicy { 399 | var ns string 400 | // no PSP will be generated if no security spec is provided 401 | if len(cssList) == 0 && len(pssList) == 0 { 402 | return nil 403 | } 404 | 405 | psp := &policyv1beta1.PodSecurityPolicy{} 406 | 407 | psp.APIVersion = "policy/v1beta1" 408 | psp.Kind = "PodSecurityPolicy" 409 | psp.Spec.ReadOnlyRootFilesystem = true 410 | 411 | addedCap := map[string]int{} 412 | droppedCap := map[string]int{} 413 | 414 | effectiveCap := map[string]bool{} 415 | 416 | runAsUser := map[int64]bool{} 417 | 418 | runAsGroup := map[int64]bool{} 419 | 420 | volumeTypes := map[string]bool{} 421 | 422 | hostPaths := map[string]bool{} 423 | 424 | hostPorts := map[int32]bool{} 425 | 426 | sysctls := map[string]bool{} 427 | 428 | runAsUserCount := 0 429 | 430 | runAsGroupCount := 0 431 | 432 | runAsNonRootCount := 0 433 | 434 | notAllowPrivilegeEscationCount := 0 435 | 436 | ns = namespace 437 | 438 | if ns == "" { 439 | ns = "all" 440 | } 441 | 442 | if pspName == "" { 443 | psp.Name = fmt.Sprintf("%s-%s-%s", "pod-security-policy", ns, time.Now().Format("20060102150405")) 444 | } else { 445 | psp.Name = pspName 446 | } 447 | 448 | for _, sc := range pssList { 449 | psp.Spec.HostPID = psp.Spec.HostPID || sc.HostPID 450 | psp.Spec.HostIPC = psp.Spec.HostIPC || sc.HostIPC 451 | psp.Spec.HostNetwork = psp.Spec.HostNetwork || sc.HostNetwork 452 | 453 | for _, t := range sc.VolumeTypes { 454 | volumeTypes[t] = true 455 | } 456 | 457 | for path, readOnly := range sc.MountHostPaths { 458 | if _, exists := hostPaths[path]; !exists { 459 | hostPaths[path] = readOnly 460 | } else { 461 | hostPaths[path] = readOnly && hostPaths[path] 462 | } 463 | } 464 | 465 | for _, s := range sc.Sysctls { 466 | sysctls[s] = true 467 | } 468 | } 469 | 470 | for _, sc := range cssList { 471 | for _, cap := range sc.Capabilities { 472 | effectiveCap[cap] = true 473 | } 474 | 475 | for _, cap := range sc.AddedCap { 476 | addedCap[cap]++ 477 | } 478 | 479 | for _, cap := range sc.DroppedCap { 480 | droppedCap[cap]++ 481 | } 482 | 483 | psp.Spec.Privileged = psp.Spec.Privileged || sc.Privileged 484 | 485 | psp.Spec.ReadOnlyRootFilesystem = psp.Spec.ReadOnlyRootFilesystem && sc.ReadOnlyRootFS 486 | 487 | if sc.RunAsNonRoot != nil && *sc.RunAsNonRoot { 488 | runAsNonRootCount++ 489 | } 490 | 491 | // runAsUser is set and not to root 492 | if sc.RunAsUser != nil && *sc.RunAsUser != 0 { 493 | runAsUser[*sc.RunAsUser] = true 494 | runAsUserCount++ 495 | } 496 | 497 | // runAsGroup is set 498 | if sc.RunAsGroup != nil && *sc.RunAsGroup != 0 { 499 | runAsGroup[*sc.RunAsGroup] = true 500 | runAsGroupCount++ 501 | } 502 | 503 | if sc.AllowPrivilegeEscalation != nil && !*sc.AllowPrivilegeEscalation { 504 | notAllowPrivilegeEscationCount++ 505 | } 506 | 507 | for _, port := range sc.HostPorts { 508 | hostPorts[port] = true 509 | } 510 | } 511 | 512 | // set allowedPrivilegeEscalation 513 | if notAllowPrivilegeEscationCount == len(cssList) { 514 | notAllowed := false 515 | psp.Spec.AllowPrivilegeEscalation = ¬Allowed 516 | } 517 | 518 | // set runAsUser strategy 519 | if runAsNonRootCount == len(cssList) { 520 | psp.Spec.RunAsUser.Rule = policyv1beta1.RunAsUserStrategyMustRunAsNonRoot 521 | } 522 | 523 | // set runAsGroup strategy 524 | if runAsGroupCount == len(cssList) { 525 | psp.Spec.RunAsGroup = &policyv1beta1.RunAsGroupStrategyOptions{} 526 | psp.Spec.RunAsGroup.Rule = policyv1beta1.RunAsGroupStrategyMustRunAs 527 | for gid := range runAsGroup { 528 | psp.Spec.RunAsGroup.Ranges = append(psp.Spec.RunAsGroup.Ranges, policyv1beta1.IDRange{ 529 | Min: gid, 530 | Max: gid, 531 | }) 532 | } 533 | } 534 | 535 | // set runAsUser strategy 536 | if runAsUserCount == len(cssList) { 537 | psp.Spec.RunAsUser.Rule = policyv1beta1.RunAsUserStrategyMustRunAs 538 | for uid := range runAsUser { 539 | psp.Spec.RunAsUser.Ranges = append(psp.Spec.RunAsUser.Ranges, policyv1beta1.IDRange{ 540 | Min: uid, 541 | Max: uid, 542 | }) 543 | } 544 | } 545 | 546 | // set allowed host path 547 | enforceReadOnly, _ := utils.CompareVersion(serverGitVersion, types.Version1_11) 548 | 549 | for path, readOnly := range hostPaths { 550 | psp.Spec.AllowedHostPaths = append(psp.Spec.AllowedHostPaths, policyv1beta1.AllowedHostPath{ 551 | PathPrefix: path, 552 | ReadOnly: readOnly || enforceReadOnly, 553 | }) 554 | } 555 | 556 | // set limit volumes 557 | volumeTypeList := utils.MapToArray(volumeTypes) 558 | 559 | for _, v := range volumeTypeList { 560 | psp.Spec.Volumes = append(psp.Spec.Volumes, policyv1beta1.FSType(v)) 561 | } 562 | 563 | // set allowedCapabilities 564 | defaultCap := utils.ArrayToMap(types.DefaultCaps) 565 | for cap := range defaultCap { 566 | if _, exists := effectiveCap[cap]; exists { 567 | delete(effectiveCap, cap) 568 | } 569 | } 570 | 571 | // set allowedAddCapabilities 572 | for cap := range effectiveCap { 573 | psp.Spec.AllowedCapabilities = append(psp.Spec.AllowedCapabilities, corev1.Capability(cap)) 574 | } 575 | 576 | // set defaultAddCapabilities 577 | for k, v := range addedCap { 578 | if v == len(cssList) { 579 | psp.Spec.DefaultAddCapabilities = append(psp.Spec.DefaultAddCapabilities, corev1.Capability(k)) 580 | } 581 | } 582 | 583 | // set requiredDroppedCapabilities 584 | for k, v := range droppedCap { 585 | if v == len(cssList) { 586 | psp.Spec.RequiredDropCapabilities = append(psp.Spec.RequiredDropCapabilities, corev1.Capability(k)) 587 | } 588 | } 589 | 590 | // set host ports 591 | portRangeList := types.PortRangeList{} 592 | for hostPort := range hostPorts { 593 | portRange := types.NewPortRange(hostPort, hostPort) 594 | portRangeList = append(portRangeList, portRange) 595 | } 596 | 597 | // set allowedUnsafeSysctls 598 | for s := range sysctls { 599 | psp.Spec.AllowedUnsafeSysctls = append(psp.Spec.AllowedUnsafeSysctls, s) 600 | } 601 | 602 | for _, portRange := range portRangeList.Consolidate() { 603 | psp.Spec.HostPorts = append(psp.Spec.HostPorts, policyv1beta1.HostPortRange{Min: portRange.Min, Max: portRange.Max}) 604 | } 605 | 606 | // set to default values 607 | if string(psp.Spec.RunAsUser.Rule) == "" { 608 | psp.Spec.RunAsUser.Rule = policyv1beta1.RunAsUserStrategyRunAsAny 609 | } 610 | 611 | if psp.Spec.RunAsGroup != nil && string(psp.Spec.RunAsGroup.Rule) == "" { 612 | psp.Spec.RunAsGroup.Rule = policyv1beta1.RunAsGroupStrategyRunAsAny 613 | } 614 | 615 | if string(psp.Spec.FSGroup.Rule) == "" { 616 | psp.Spec.FSGroup.Rule = policyv1beta1.FSGroupStrategyRunAsAny 617 | } 618 | 619 | if string(psp.Spec.SELinux.Rule) == "" { 620 | psp.Spec.SELinux.Rule = policyv1beta1.SELinuxStrategyRunAsAny 621 | } 622 | 623 | if string(psp.Spec.SupplementalGroups.Rule) == "" { 624 | psp.Spec.SupplementalGroups.Rule = policyv1beta1.SupplementalGroupsStrategyRunAsAny 625 | } 626 | 627 | return psp 628 | } 629 | 630 | // GenerateOPA generate OPA Policy 631 | func (pg *Generator) GenerateOPAWithName( 632 | cssList []types.ContainerSecuritySpec, 633 | pssList []types.PodSecuritySpec, 634 | namespace, serverGitVersion, pspName string, OPAdefaultRule bool) *ast.Module { 635 | 636 | var ns string 637 | // no OPA will be generated if no security spec is provided 638 | if len(cssList) == 0 && len(pssList) == 0 { 639 | return nil 640 | } 641 | 642 | var mod ast.Module 643 | pack := ast.MustParsePackage("package kubernetes.admission") 644 | 645 | a := ast.Head{ 646 | Name: "deny", 647 | Key: &ast.Term{ 648 | ast.VarTerm("message").Value, 649 | nil, 650 | }, 651 | } 652 | rule := ast.Rule{ 653 | nil, 654 | false, 655 | &a, 656 | nil, 657 | nil, 658 | nil, 659 | } 660 | 661 | hostPaths := []string{} 662 | volumeMounts := map[string]bool{} 663 | volumeMountValues := []string{} 664 | hostPid := false 665 | HostIPC := false 666 | HostNet := false 667 | sysctls := []string{} 668 | Privileged := false 669 | ReadOnlyRootFS := 0 670 | runAsUserCount := 0 671 | 672 | runAsGroupCount := 0 673 | RunAsNonRoot := 0 674 | AllowPrivilegeEscalation := 0 675 | addedCap := []string{} 676 | droppedCap := []string{} 677 | runAsUser := []string{} 678 | runAsGroup := []string{} 679 | 680 | effectiveCap := map[string]bool{} 681 | hostPorts := []string{} 682 | basepath := "" 683 | 684 | ns = namespace 685 | 686 | if ns == "" { 687 | ns = "all" 688 | } 689 | rule.Body.Append(ast.MustParseExpr("workload := input.request.object")) 690 | rule.Body.Append(ast.NewExpr(ast.VarTerm(checkOPADefault(OPAdefaultRule) + "valueWorkLoadSecContext(workload)"))) 691 | valueWorkLoadSecContext := addOPARule("valueWorkLoadSecContext", "workload") 692 | 693 | for _, wsc := range pssList { 694 | 695 | hostPid = hostPid || wsc.HostPID 696 | HostIPC = HostIPC || wsc.HostIPC 697 | HostNet = HostNet || wsc.HostNetwork 698 | 699 | for path, _ := range wsc.MountHostPaths { 700 | hostPaths = append(hostPaths, "\""+path+"\"") 701 | } 702 | 703 | for name, readOnly := range wsc.MountHostPaths { 704 | if _, exists := volumeMounts[name]; !exists { 705 | if readOnly { 706 | volumeMounts[name] = readOnly 707 | volumeMountValues = append(volumeMountValues, "\""+name+"\"") 708 | } 709 | } else { 710 | volumeMounts[name] = readOnly && volumeMounts[name] 711 | } 712 | } 713 | 714 | // Sysctls is set 715 | for _, s := range wsc.Sysctls { 716 | sysctls = append(sysctls, "\""+s+"\"") 717 | } 718 | 719 | // Check if workload or pod 720 | if wsc.Metadata.Kind == "Deployment" || wsc.Metadata.Kind == "Job" || wsc.Metadata.Kind == "ReplicaSet" || wsc.Metadata.Kind == "DaemonSet" || wsc.Metadata.Kind == "ReplicationController" { 721 | basepath = "input.request.object.spec.template.spec" 722 | } else { 723 | basepath = "input.request.object.spec" 724 | } 725 | 726 | } 727 | 728 | valueWorkLoadSecContext.Body.Append(ast.MustParseExpr("container := " + basepath + ".containers[_]")) 729 | 730 | // Add rule hostPaths 731 | if len(hostPaths) > 0 { 732 | valueWorkLoadSecContext.Body.Append(ast.MustParseExpr("volumeHostPaths(workload)")) 733 | valueHostPathRule := addOPARule("volumeHostPaths", "workload") 734 | valueHostPathRule.Body.Append(ast.MustParseExpr("hostPaths = {" + strings.Join(hostPaths, ",") + "}")) 735 | valueHostPathRule.Body.Append(ast.MustParseExpr("diff_fields := {label | label := " + basepath + ".volumes[_].hostPath.path} - hostPaths")) 736 | valueHostPathRule.Body.Append(ast.MustParseExpr("count(diff_fields) <= 0")) 737 | valueHostPathRule.Body.Append(ast.MustParseExpr("names := {name | path1 := {p | volumes := " + basepath + ".volumes[_];checkHostPort(volumes); p := volumes};name := path1[_].name}")) 738 | valueHostPathRule.Body.Append(ast.MustParseExpr("namesNotRO := {name | path1 := {p | volumeMounts := " + basepath + ".containers[_].volumeMounts[_]; not volumeMounts.readOnly == true; p := volumeMounts}; name := path1[_].name}")) 739 | valueHostPathRule.Body.Append(ast.MustParseExpr("intersect := namesNotRO & names")) 740 | valueHostPathRule.Body.Append(ast.MustParseExpr("not namesNotRO == names")) 741 | valueHostPathRule.Body.Append(ast.MustParseExpr("count(intersect) == 0")) 742 | 743 | for volume := range volumeMounts { 744 | valueHostPathRule := addOPARule("checkHostPort", "volumes") 745 | valueHostPathRule.Body.Append(ast.MustParseExpr("volumes.hostPath.path == \"" + volume + "\"")) 746 | mod.Rules = append(mod.Rules, valueHostPathRule) 747 | } 748 | 749 | mod.Rules = append(mod.Rules, valueHostPathRule) 750 | } 751 | 752 | // Add rule sysctls 753 | if len(sysctls) > 0 { 754 | valueWorkLoadSecContext.Body.Append(ast.MustParseExpr("valueSysctls(workload)")) 755 | valueSysctlsRule := addOPARule("valueSysctls", "sysctls") 756 | valueSysctlsRule.Body.Append(ast.MustParseExpr("sysctls = {" + strings.Join(sysctls, ",") + "}")) 757 | valueSysctlsRule.Body.Append(ast.MustParseExpr("setSysctls := {" + basepath + ".securityContext.sysctls[_] | " + basepath + ".securityContext.sysctls[_] != null}")) 758 | valueSysctlsRule.Body.Append(ast.MustParseExpr("count(setSysctls) > 0")) 759 | valueSysctlsRule.Body.Append(ast.MustParseExpr("diff_fields := {label | label := " + basepath + ".securityContext.sysctls[_]} - sysctls")) 760 | valueSysctlsRule.Body.Append(ast.MustParseExpr("count(diff_fields) <= 0")) 761 | mod.Rules = append(mod.Rules, valueSysctlsRule) 762 | } 763 | 764 | // Add rule hostPid 765 | if hostPid { 766 | valueWorkLoadSecContext.Body.Append(ast.MustParseExpr(basepath + ".hostPID")) 767 | } 768 | 769 | // Add rule HostIPC 770 | if HostIPC { 771 | valueWorkLoadSecContext.Body.Append(ast.MustParseExpr(basepath + ".hostIPC")) 772 | } 773 | 774 | // Add rule HostNet 775 | if HostNet { 776 | valueWorkLoadSecContext.Body.Append(ast.MustParseExpr(basepath + ".hostNetwork")) 777 | } 778 | mod.Rules = append(mod.Rules, valueWorkLoadSecContext) 779 | 780 | valueWorkLoadSecContext.Body.Append(ast.NewExpr(ast.VarTerm("valueSecContext(container)"))) 781 | 782 | for _, sc := range cssList { 783 | for _, cap := range sc.Capabilities { 784 | effectiveCap[cap] = true 785 | } 786 | 787 | for _, cap := range sc.AddedCap { 788 | addedCap = append(addedCap, "\""+cap+"\"") 789 | } 790 | 791 | for _, cap := range sc.DroppedCap { 792 | droppedCap = append(droppedCap, "\""+cap+"\"") 793 | } 794 | 795 | Privileged = Privileged || sc.Privileged 796 | 797 | // runAsUser is set and not to root 798 | if sc.RunAsUser != nil && *sc.RunAsUser != 0 { 799 | runAsUser = append(runAsUser, strconv.FormatInt(*sc.RunAsUser, 10)) 800 | runAsUserCount++ 801 | } 802 | 803 | // runAsGroup is set 804 | if sc.RunAsGroup != nil && *sc.RunAsGroup != 0 { 805 | runAsGroup = append(runAsGroup, strconv.FormatInt(*sc.RunAsGroup, 10)) 806 | runAsGroupCount++ 807 | } 808 | 809 | // port is set 810 | for _, port := range sc.HostPorts { 811 | hostPorts = append(hostPorts, fmt.Sprint(port)) 812 | } 813 | 814 | if sc.ReadOnlyRootFS { 815 | ReadOnlyRootFS++ 816 | } 817 | 818 | if sc.RunAsNonRoot != nil && *sc.RunAsNonRoot { 819 | RunAsNonRoot++ 820 | } 821 | 822 | if sc.AllowPrivilegeEscalation != nil && !*sc.AllowPrivilegeEscalation { 823 | AllowPrivilegeEscalation++ 824 | } 825 | } 826 | 827 | valueSecContextRule := addOPARule("valueSecContext", "container") 828 | 829 | // Add rule addedCap 830 | if len(addedCap) > 0 { 831 | valueSecContextRule.Body.Append(ast.NewExpr(ast.VarTerm("valueAddedCap(container)"))) 832 | valueAddedCapRule := addOPARule("valueAddedCap", "addedCap") 833 | valueAddedCapRule.Body.Append(ast.MustParseExpr("caps = {" + strings.Join(addedCap, ",") + "}")) 834 | valueAddedCapRule.Body.Append(ast.MustParseExpr("diff_fields := {label | label := " + basepath + ".containers[_].securityContext.capabilities.add[_]} - caps")) 835 | valueAddedCapRule.Body.Append(ast.MustParseExpr("count(diff_fields) <= 0")) 836 | mod.Rules = append(mod.Rules, valueAddedCapRule) 837 | } 838 | 839 | // Add rule droppedCap 840 | if len(droppedCap) > 0 { 841 | valueSecContextRule.Body.Append(ast.NewExpr(ast.VarTerm("valueDroppedCap(container)"))) 842 | valueDroppedCapRule := addOPARule("valueDroppedCap", "droppedCap") 843 | valueDroppedCapRule.Body.Append(ast.MustParseExpr("caps = {" + strings.Join(droppedCap, ",") + "}")) 844 | valueDroppedCapRule.Body.Append(ast.MustParseExpr("diff_fields := {label | label := " + basepath + ".containers[_].securityContext.capabilities.drop[_]} - caps")) 845 | valueDroppedCapRule.Body.Append(ast.MustParseExpr("count(diff_fields) <= 0")) 846 | mod.Rules = append(mod.Rules, valueDroppedCapRule) 847 | } 848 | 849 | // Add rule runAsUser 850 | if len(runAsUser) > 0 { 851 | valueSecContextRule.Body.Append(ast.NewExpr(ast.VarTerm("valueRunAsUserID(container)"))) 852 | valueHostRunAsUserRule := addOPARule("valueRunAsUserID", "uid") 853 | valueHostRunAsUserRule.Body.Append(ast.MustParseExpr("uids = {" + strings.Join(runAsUser, ",") + "}")) 854 | valueHostRunAsUserRule.Body.Append(ast.MustParseExpr("setRunAsUser := {" + basepath + ".containers[_].securityContext.runAsUser | " + basepath + ".containers[_].securityContext.runAsUser != null}")) 855 | valueHostRunAsUserRule.Body.Append(ast.MustParseExpr("count(setRunAsUser) > 0")) 856 | valueHostRunAsUserRule.Body.Append(ast.MustParseExpr("diff_fields := setRunAsUser - uids")) 857 | valueHostRunAsUserRule.Body.Append(ast.MustParseExpr("count(diff_fields) <= 0")) 858 | mod.Rules = append(mod.Rules, valueHostRunAsUserRule) 859 | } 860 | 861 | // Add rule runAsGroup 862 | if len(runAsGroup) > 0 { 863 | valueSecContextRule.Body.Append(ast.NewExpr(ast.VarTerm("valueRunAsGroupID(container)"))) 864 | valueHostRunAsGroupRule := addOPARule("valueRunAsGroupID", "gid") 865 | valueHostRunAsGroupRule.Body.Append(ast.MustParseExpr("gids = {" + strings.Join(runAsGroup, ",") + "}")) 866 | valueHostRunAsGroupRule.Body.Append(ast.MustParseExpr("setRunAsGroup := {" + basepath + ".containers[_].securityContext.runAsGroup | " + basepath + ".containers[_].securityContext.runAsGroup != null}")) 867 | valueHostRunAsGroupRule.Body.Append(ast.MustParseExpr("count(setRunAsGroup) > 0")) 868 | valueHostRunAsGroupRule.Body.Append(ast.MustParseExpr("diff_fields := setRunAsGroup - gids")) 869 | valueHostRunAsGroupRule.Body.Append(ast.MustParseExpr("count(diff_fields) <= 0")) 870 | mod.Rules = append(mod.Rules, valueHostRunAsGroupRule) 871 | } 872 | 873 | // Add rule hostPorts 874 | if len(hostPorts) > 0 { 875 | valueSecContextRule.Body.Append(ast.NewExpr(ast.VarTerm("valueHostPort(container)"))) 876 | valueHostPortRule := addOPARule("valueHostPort", "container") 877 | valueHostPortRule.Body.Append(ast.MustParseExpr("ports = {" + strings.Join(hostPorts, ",") + "}")) 878 | valueHostPortRule.Body.Append(ast.MustParseExpr("setHostPort := {" + basepath + ".containers[_].ports[_].hostPort | " + basepath + ".containers[_].ports[_].hostPort != null}")) 879 | valueHostPortRule.Body.Append(ast.MustParseExpr("count(setHostPort) > 0")) 880 | valueHostPortRule.Body.Append(ast.MustParseExpr("diff_fields := setHostPort - ports")) 881 | valueHostPortRule.Body.Append(ast.MustParseExpr("count(diff_fields) <= 0")) 882 | mod.Rules = append(mod.Rules, valueHostPortRule) 883 | } 884 | 885 | // Add rule Privileged 886 | if Privileged { 887 | valueSecContextRule.Body.Append(ast.MustParseExpr("container.securityContext.privileged")) 888 | } 889 | 890 | // Add rule ReadOnlyRootFS 891 | if ReadOnlyRootFS == len(cssList) { 892 | valueSecContextRule.Body.Append(ast.MustParseExpr("container.securityContext.readOnlyRootFilesystem")) 893 | } 894 | 895 | // Add rule RunAsNonRoot 896 | if RunAsNonRoot == len(cssList) { 897 | valueSecContextRule.Body.Append(ast.MustParseExpr("container.securityContext.runAsNonRoot")) 898 | } 899 | 900 | // Add rule AllowPrivilegeEscalation 901 | if AllowPrivilegeEscalation == len(cssList) { 902 | valueSecContextRule.Body.Append(ast.MustParseExpr("container.securityContext.allowPrivilegeEscalation == false")) 903 | } 904 | 905 | mod.Rules = append(mod.Rules, valueSecContextRule) 906 | 907 | rule.Body.Append(ast.MustParseExpr("message := sprintf(\"Workflow or pod compliant with the policy.\", [workload.metadata.name])")) 908 | mod.Package = pack 909 | mod.Rules = append(mod.Rules, &rule) 910 | 911 | return &mod 912 | 913 | } 914 | 915 | // deny-by-default option check 916 | func checkOPADefault(OPAdefaultRule bool) string { 917 | if !OPAdefaultRule { 918 | return "not " 919 | } else { 920 | return "" 921 | } 922 | } 923 | 924 | func addOPARule(nameRuleHead string, arg string) *ast.Rule { 925 | RuleHead := ast.Head{ 926 | Name: ast.Var(nameRuleHead), 927 | Args: []*ast.Term{ 928 | ast.VarTerm(arg), 929 | }, 930 | } 931 | Rule := ast.Rule{ 932 | nil, 933 | false, 934 | &RuleHead, 935 | nil, 936 | nil, 937 | nil, 938 | } 939 | return &Rule 940 | } 941 | 942 | func (pg *Generator) fromPodObj(metadata types.Metadata, spec corev1.PodSpec, OPAformat string, OPAdefaultRule bool) (string, error) { 943 | 944 | cssList, pss := pg.GetSecuritySpecFromPodSpec(metadata, "default", spec, nil) 945 | 946 | pssList := []types.PodSecuritySpec{pss} 947 | 948 | // We assume a namespace "default", which is only used for the 949 | // name of the resulting PSP, and assume a k8s version of 950 | // 1.11, which allows enforcing ReadOnly. 951 | var psp *policyv1beta1.PodSecurityPolicy 952 | var mod *ast.Module 953 | var out string 954 | 955 | if OPAformat == "psp" { 956 | psp = pg.GeneratePSP(cssList, pssList, "default", types.Version1_11) 957 | pspJson, err := json.Marshal(psp) 958 | if err != nil { 959 | return "", fmt.Errorf("Could not marshal resulting PSP: %v", err) 960 | } 961 | pspYaml, err := yaml.JSONToYAML(pspJson) 962 | if err != nil { 963 | return "", fmt.Errorf("Could not convert resulting PSP to Json: %v", err) 964 | } 965 | out = string(pspYaml) 966 | } else if OPAformat == "opa" { 967 | mod = pg.GenerateOPAPod(cssList, pssList, "default", types.Version1_11, OPAdefaultRule) 968 | out = mod.String() 969 | } 970 | 971 | return string(out), nil 972 | } 973 | 974 | func (pg *Generator) fromDaemonSet(ds *appsv1.DaemonSet, OPAformat string, OPAdefaultRule bool) (string, error) { 975 | return pg.fromPodObj(types.Metadata{ 976 | Name: ds.Name, 977 | Kind: ds.Kind, 978 | }, ds.Spec.Template.Spec, OPAformat, OPAdefaultRule) 979 | } 980 | 981 | func (pg *Generator) fromDeployment(dep *appsv1.Deployment, OPAformat string, OPAdefaultRule bool) (string, error) { 982 | return pg.fromPodObj(types.Metadata{ 983 | Name: dep.Name, 984 | Kind: dep.Kind, 985 | }, dep.Spec.Template.Spec, OPAformat, OPAdefaultRule) 986 | } 987 | 988 | func (pg *Generator) fromReplicaSet(rs *appsv1.ReplicaSet, OPAformat string, OPAdefaultRule bool) (string, error) { 989 | return pg.fromPodObj(types.Metadata{ 990 | Name: rs.Name, 991 | Kind: rs.Kind, 992 | }, rs.Spec.Template.Spec, OPAformat, OPAdefaultRule) 993 | } 994 | 995 | func (pg *Generator) fromStatefulSet(ss *appsv1.StatefulSet, OPAformat string, OPAdefaultRule bool) (string, error) { 996 | return pg.fromPodObj(types.Metadata{ 997 | Name: ss.Name, 998 | Kind: ss.Kind, 999 | }, ss.Spec.Template.Spec, OPAformat, OPAdefaultRule) 1000 | } 1001 | 1002 | func (pg *Generator) fromReplicationController(rc *corev1.ReplicationController, OPAformat string, OPAdefaultRule bool) (string, error) { 1003 | return pg.fromPodObj(types.Metadata{ 1004 | Name: rc.Name, 1005 | Kind: rc.Kind, 1006 | }, rc.Spec.Template.Spec, OPAformat, OPAdefaultRule) 1007 | } 1008 | 1009 | func (pg *Generator) fromCronJob(cj *batchv1beta1.CronJob, OPAformat string, OPAdefaultRule bool) (string, error) { 1010 | return pg.fromPodObj(types.Metadata{ 1011 | Name: cj.Name, 1012 | Kind: cj.Kind, 1013 | }, cj.Spec.JobTemplate.Spec.Template.Spec, OPAformat, OPAdefaultRule) 1014 | } 1015 | 1016 | func (pg *Generator) fromJob(job *batch.Job, OPAformat string, OPAdefaultRule bool) (string, error) { 1017 | return pg.fromPodObj(types.Metadata{ 1018 | Name: job.Name, 1019 | Kind: job.Kind, 1020 | }, job.Spec.Template.Spec, OPAformat, OPAdefaultRule) 1021 | } 1022 | 1023 | func (pg *Generator) fromPod(pod *corev1.Pod, OPAformat string, OPAdefaultRule bool) (string, error) { 1024 | return pg.fromPodObj(types.Metadata{ 1025 | Name: pod.Name, 1026 | Kind: pod.Kind, 1027 | }, pod.Spec, OPAformat, OPAdefaultRule) 1028 | } 1029 | 1030 | func (pg *Generator) FromPodObjString(podObjString string, OPAformat string, OPAdefaultRule bool) (string, error) { 1031 | 1032 | podObjJson, err := yaml.YAMLToJSON([]byte(podObjString)) 1033 | if err != nil { 1034 | return "", fmt.Errorf("Could not parse pod Object: %v", err) 1035 | } 1036 | 1037 | var anyJson map[string]interface{} 1038 | 1039 | err = json.Unmarshal(podObjJson, &anyJson) 1040 | 1041 | if err != nil { 1042 | return "", fmt.Errorf("Could not unmarshal json document: %v", err) 1043 | } 1044 | 1045 | decoder := json.NewDecoder(bytes.NewReader(podObjJson)) 1046 | decoder.DisallowUnknownFields() 1047 | 1048 | switch kind := anyJson["kind"]; kind { 1049 | case "DaemonSet": 1050 | var ds appsv1.DaemonSet 1051 | if err = decoder.Decode(&ds); err != nil { 1052 | return "", fmt.Errorf("Could not unmarshal json document as DaemonSet: %v", err) 1053 | } 1054 | return pg.fromDaemonSet(&ds, OPAformat, OPAdefaultRule) 1055 | case "Deployment": 1056 | var dep appsv1.Deployment 1057 | if err = decoder.Decode(&dep); err != nil { 1058 | return "", fmt.Errorf("Could not unmarshal json document as Deployment: %v", err) 1059 | } 1060 | return pg.fromDeployment(&dep, OPAformat, OPAdefaultRule) 1061 | case "ReplicaSet": 1062 | var rs appsv1.ReplicaSet 1063 | if err = decoder.Decode(&rs); err != nil { 1064 | return "", fmt.Errorf("Could not unmarshal json document as ReplicaSet: %v", err) 1065 | } 1066 | return pg.fromReplicaSet(&rs, OPAformat, OPAdefaultRule) 1067 | case "StatefulSet": 1068 | var ss appsv1.StatefulSet 1069 | if err = decoder.Decode(&ss); err != nil { 1070 | return "", fmt.Errorf("Could not unmarshal json document as StatefulSet: %v", err) 1071 | } 1072 | return pg.fromStatefulSet(&ss, OPAformat, OPAdefaultRule) 1073 | case "ReplicationController": 1074 | var rc corev1.ReplicationController 1075 | if err = decoder.Decode(&rc); err != nil { 1076 | return "", fmt.Errorf("Could not unmarshal json document as ReplicationController: %v", err) 1077 | } 1078 | return pg.fromReplicationController(&rc, OPAformat, OPAdefaultRule) 1079 | case "CronJob": 1080 | var cj batchv1beta1.CronJob 1081 | if err = decoder.Decode(&cj); err != nil { 1082 | return "", fmt.Errorf("Could not unmarshal json document as CronJob: %v", err) 1083 | } 1084 | return pg.fromCronJob(&cj, OPAformat, OPAdefaultRule) 1085 | case "Job": 1086 | var job batch.Job 1087 | if err = decoder.Decode(&job); err != nil { 1088 | return "", fmt.Errorf("Could not unmarshal json document as Job: %v", err) 1089 | } 1090 | return pg.fromJob(&job, OPAformat, OPAdefaultRule) 1091 | case "Pod": 1092 | var pod corev1.Pod 1093 | if err = decoder.Decode(&pod); err != nil { 1094 | return "", fmt.Errorf("Could not unmarshal json document as Pod: %v", err) 1095 | } 1096 | return pg.fromPod(&pod, OPAformat, OPAdefaultRule) 1097 | } 1098 | 1099 | return "", fmt.Errorf("K8s Object not one of supported types") 1100 | } 1101 | 1102 | func (pg *Generator) GeneratePSPFormYamls(yamls []string) (*policyv1beta1.PodSecurityPolicy, error) { 1103 | cssList := []types.ContainerSecuritySpec{} 1104 | pssList := []types.PodSecuritySpec{} 1105 | for _, yamlFile := range yamls { 1106 | csl, psl, err := pg.LoadYaml(yamlFile) 1107 | if err != nil { 1108 | return nil, err 1109 | } 1110 | 1111 | if len(csl) > 0 { 1112 | cssList = append(cssList, csl...) 1113 | pssList = append(pssList, psl...) 1114 | } 1115 | } 1116 | 1117 | psp := pg.GeneratePSP(cssList, pssList, "", types.Version1_11) 1118 | 1119 | return psp, nil 1120 | } 1121 | 1122 | func (pg *Generator) LoadYaml(yamlFile string) ([]types.ContainerSecuritySpec, []types.PodSecuritySpec, error) { 1123 | cssList := []types.ContainerSecuritySpec{} 1124 | pssList := []types.PodSecuritySpec{} 1125 | 1126 | file, err := os.Open(yamlFile) 1127 | if err != nil { 1128 | return cssList, pssList, fmt.Errorf("failed to open yaml file %s for reading: %v", yamlFile, err) 1129 | } 1130 | defer file.Close() 1131 | 1132 | fileBytes, err := ioutil.ReadAll(file) 1133 | 1134 | sepYamlFiles := strings.Split(string(fileBytes), "---") 1135 | 1136 | for _, f := range sepYamlFiles { 1137 | if f == "\n" || f == "" { 1138 | // ignore empty cases 1139 | continue 1140 | } 1141 | 1142 | // remove comments: line starts with # 1143 | lines := strings.Split(f, "\n") 1144 | newLines := []string{} 1145 | for _, line := range lines { 1146 | if line != "" && line != "\n" && !strings.HasPrefix(line, "#") { 1147 | newLines = append(newLines, line) 1148 | } 1149 | } 1150 | 1151 | if len(newLines) == 0 { 1152 | continue 1153 | } 1154 | 1155 | newFile := strings.Join(newLines, "\n") 1156 | 1157 | csl := []types.ContainerSecuritySpec{} 1158 | pss := types.PodSecuritySpec{} 1159 | 1160 | decode := scheme.Codecs.UniversalDeserializer().Decode 1161 | obj, _, err := decode([]byte(newFile), nil, nil) 1162 | 1163 | if err != nil { 1164 | log.Println(fmt.Sprintf("Error while decoding YAML object: %s. Error was: %s", newFile, err)) 1165 | continue 1166 | } 1167 | 1168 | fileName := filepath.Base(yamlFile) 1169 | switch o := obj.(type) { 1170 | case *corev1.Pod: 1171 | csl, pss = pg.GetSecuritySpecFromPodSpec(types.Metadata{ 1172 | Name: o.Name, 1173 | Kind: o.Kind, 1174 | Namespace: getNamespace(o.Namespace), 1175 | YamlFile: fileName, 1176 | }, getNamespace(o.Namespace), o.Spec, nil) 1177 | case *appsv1.StatefulSet: 1178 | csl, pss = pg.GetSecuritySpecFromPodSpec(types.Metadata{ 1179 | Name: o.Name, 1180 | Kind: o.Kind, 1181 | Namespace: getNamespace(o.Namespace), 1182 | YamlFile: fileName, 1183 | }, getNamespace(o.Namespace), o.Spec.Template.Spec, nil) 1184 | case *appsv1.DaemonSet: 1185 | csl, pss = pg.GetSecuritySpecFromPodSpec(types.Metadata{ 1186 | Name: o.Name, 1187 | Kind: o.Kind, 1188 | Namespace: getNamespace(o.Namespace), 1189 | YamlFile: fileName, 1190 | }, getNamespace(o.Namespace), o.Spec.Template.Spec, nil) 1191 | case *appsv1.Deployment: 1192 | csl, pss = pg.GetSecuritySpecFromPodSpec(types.Metadata{ 1193 | Name: o.Name, 1194 | Kind: o.Kind, 1195 | Namespace: getNamespace(o.Namespace), 1196 | YamlFile: fileName, 1197 | }, getNamespace(o.Namespace), o.Spec.Template.Spec, nil) 1198 | case *appsv1.ReplicaSet: 1199 | csl, pss = pg.GetSecuritySpecFromPodSpec(types.Metadata{ 1200 | Name: o.Name, 1201 | Kind: o.Kind, 1202 | Namespace: getNamespace(o.Namespace), 1203 | YamlFile: yamlFile, 1204 | }, getNamespace(o.Namespace), o.Spec.Template.Spec, nil) 1205 | case *corev1.ReplicationController: 1206 | csl, pss = pg.GetSecuritySpecFromPodSpec(types.Metadata{ 1207 | Name: o.Name, 1208 | Kind: o.Kind, 1209 | Namespace: getNamespace(o.Namespace), 1210 | YamlFile: fileName, 1211 | }, getNamespace(o.Namespace), o.Spec.Template.Spec, nil) 1212 | case *batchv1beta1.CronJob: 1213 | csl, pss = pg.GetSecuritySpecFromPodSpec(types.Metadata{ 1214 | Name: o.Name, 1215 | Kind: o.Kind, 1216 | Namespace: getNamespace(o.Namespace), 1217 | YamlFile: fileName, 1218 | }, getNamespace(o.Namespace), o.Spec.JobTemplate.Spec.Template.Spec, nil) 1219 | case *batch.Job: 1220 | csl, pss = pg.GetSecuritySpecFromPodSpec(types.Metadata{ 1221 | Name: o.Name, 1222 | Kind: o.Kind, 1223 | Namespace: getNamespace(o.Namespace), 1224 | YamlFile: fileName, 1225 | }, getNamespace(o.Namespace), o.Spec.Template.Spec, nil) 1226 | } 1227 | 1228 | if len(csl) > 0 { 1229 | cssList = append(cssList, csl...) 1230 | pssList = append(pssList, pss) 1231 | } 1232 | } 1233 | 1234 | return cssList, pssList, nil 1235 | } 1236 | 1237 | func getServiceAccountName(spec corev1.PodSpec) string { 1238 | if spec.ServiceAccountName == "" { 1239 | return "default" 1240 | } 1241 | 1242 | return spec.ServiceAccountName 1243 | } 1244 | 1245 | func getNamespace(ns string) string { 1246 | if ns != "" { 1247 | return ns 1248 | } 1249 | 1250 | return "default" 1251 | } 1252 | 1253 | func getContainerVolumeMounts(mounts []corev1.VolumeMount) []types.VolumeMount { 1254 | list := []types.VolumeMount{} 1255 | 1256 | for _, vm := range mounts { 1257 | list = append(list, types.VolumeMount{ 1258 | Name: vm.Name, 1259 | MountPath: vm.MountPath, 1260 | ReadOnly: vm.ReadOnly, 1261 | SubPath: vm.SubPath, 1262 | SubPathExpr: vm.SubPathExpr, 1263 | }) 1264 | } 1265 | 1266 | return list 1267 | } 1268 | -------------------------------------------------------------------------------- /generator/generator_test.go: -------------------------------------------------------------------------------- 1 | package generator 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/sysdiglabs/kube-psp-advisor/advisor/types" 7 | ) 8 | 9 | var ( 10 | allowPrivilegeEscalation = true 11 | notRunAsNonRoot = false 12 | runAsNonRoot = true 13 | namespaceTest = "test" 14 | runAsUser int64 = 100 15 | runAsGroup int64 = 1000 16 | 17 | emptyCSSList = []types.ContainerSecuritySpec{} 18 | 19 | cssList = []types.ContainerSecuritySpec{ 20 | {Metadata: types.Metadata{ 21 | Kind: "Deployment", 22 | Name: "testDeploy", 23 | }, 24 | ContainerName: "containerA", 25 | ImageName: "imageA", 26 | Capabilities: []string{"SYS_ADMIN"}, 27 | Privileged: true, 28 | ReadOnlyRootFS: false, 29 | AllowPrivilegeEscalation: &allowPrivilegeEscalation, 30 | RunAsNonRoot: ¬RunAsNonRoot, 31 | ServiceAccount: "acctA", 32 | Namespace: namespaceTest, 33 | RunAsUser: &runAsUser, 34 | RunAsGroup: &runAsGroup, 35 | HostPorts: []int32{80, 8080}, 36 | }, 37 | {Metadata: types.Metadata{ 38 | Kind: "Deployment", 39 | Name: "testDeploy", 40 | }, 41 | ContainerName: "containerB", 42 | ImageName: "imageA", 43 | Capabilities: []string{"SYS_ADMIN"}, 44 | Privileged: false, 45 | ReadOnlyRootFS: true, 46 | RunAsNonRoot: &runAsNonRoot, 47 | ServiceAccount: "acctB", 48 | Namespace: namespaceTest, 49 | RunAsUser: &runAsUser, 50 | RunAsGroup: &runAsGroup, 51 | HostPorts: []int32{80, 8081}, 52 | }, 53 | } 54 | 55 | emptyPSSList = []types.PodSecuritySpec{} 56 | 57 | pssList = []types.PodSecuritySpec{ 58 | {Metadata: types.Metadata{ 59 | Kind: "Deployment", 60 | Name: "testDeploy", 61 | }, 62 | Namespace: namespaceTest, 63 | HostIPC: true, 64 | HostNetwork: true, 65 | HostPID: true, 66 | VolumeTypes: []string{"secret", "configMap"}, 67 | MountHostPaths: map[string]bool{ 68 | "/proc": true, 69 | }, 70 | }, 71 | {Metadata: types.Metadata{ 72 | Kind: "Deployment", 73 | Name: "testDeploy", 74 | }, 75 | Namespace: namespaceTest, 76 | HostIPC: false, 77 | HostNetwork: false, 78 | HostPID: false, 79 | VolumeTypes: []string{"secret", "emptyDir"}, 80 | MountHostPaths: map[string]bool{ 81 | "/etc": true, 82 | }, 83 | }, 84 | } 85 | ) 86 | 87 | func TestCSS(t *testing.T) { 88 | gen, _ := NewGenerator() 89 | 90 | psp := gen.GeneratePSP(cssList, emptyPSSList, namespaceTest, "v1.12.1") 91 | 92 | if !psp.Spec.Privileged { 93 | t.Fatal("psp should be privileged") 94 | } 95 | 96 | hasSYSADMIN := false 97 | for _, cap := range psp.Spec.AllowedCapabilities { 98 | if string(cap) == "SYS_ADMIN" { 99 | hasSYSADMIN = true 100 | break 101 | } 102 | } 103 | 104 | if !hasSYSADMIN { 105 | t.Fatal("psp should have SYS_ADMIN in capabilities") 106 | } 107 | 108 | if psp.Spec.ReadOnlyRootFilesystem { 109 | t.Fatal("psp should have readonlyrootsystem to false") 110 | } 111 | 112 | if psp.Spec.AllowPrivilegeEscalation != nil && !*psp.Spec.AllowPrivilegeEscalation { 113 | t.Fatal("psp should have allowPrivilegeEscalation to true") 114 | } 115 | 116 | if psp.Spec.RunAsUser.Ranges[0].Min != runAsUser && psp.Spec.RunAsUser.Ranges[0].Max != runAsUser { 117 | t.Fatal("psp should have set run as user to 100") 118 | } 119 | 120 | if psp.Spec.RunAsGroup.Ranges[0].Min != runAsGroup && psp.Spec.RunAsGroup.Ranges[0].Max != runAsGroup { 121 | t.Fatal("psp should have set run as group to 1000") 122 | } 123 | 124 | if len(psp.Spec.HostPorts) != 2 { 125 | t.Fatalf("there should be 2 port ranges, actual: %d", len(psp.Spec.HostPorts)) 126 | } 127 | 128 | if psp.Spec.HostPorts[0].Min != 80 || psp.Spec.HostPorts[0].Max != 80 { 129 | t.Fatalf("Expect port range [80, 80], actual: %v", psp.Spec.HostPorts[0]) 130 | } 131 | 132 | if psp.Spec.HostPorts[1].Min != 8080 || psp.Spec.HostPorts[1].Max != 8081 { 133 | t.Fatalf("Expect port range [8080, 8081], actual: %v", psp.Spec.HostPorts[1]) 134 | } 135 | } 136 | 137 | func TestPSS(t *testing.T) { 138 | gen, _ := NewGenerator() 139 | 140 | psp := gen.GeneratePSP(emptyCSSList, pssList, namespaceTest, "v1.12.1") 141 | 142 | if !psp.Spec.HostPID { 143 | t.Fatal("psp should allow hostPID") 144 | } 145 | 146 | if !psp.Spec.HostNetwork { 147 | t.Fatal("psp should allow hostNetwork") 148 | } 149 | 150 | if !psp.Spec.HostIPC { 151 | t.Fatal("psp should allow hostIPC") 152 | } 153 | 154 | volMap := map[string]bool{} 155 | 156 | for _, fs := range psp.Spec.Volumes { 157 | volMap[string(fs)] = true 158 | } 159 | 160 | if _, exists := volMap["secret"]; !exists { 161 | t.Fatal("psp should allow mount secret") 162 | } 163 | 164 | if _, exists := volMap["configMap"]; !exists { 165 | t.Fatal("psp should allow mount configMap") 166 | } 167 | 168 | if _, exists := volMap["emptyDir"]; !exists { 169 | t.Fatal("psp should allow mount emptyDir") 170 | } 171 | 172 | if len(volMap) > 3 { 173 | t.Fatal("psp allow more volume types than needed") 174 | } 175 | 176 | hpMap := map[string]bool{} 177 | for _, hp := range psp.Spec.AllowedHostPaths { 178 | hpMap[hp.PathPrefix] = true 179 | } 180 | 181 | if _, exists := hpMap["/proc"]; !exists { 182 | t.Fatal("psp shoud allow mount on hostpath /proc") 183 | } 184 | 185 | if _, exists := hpMap["/etc"]; !exists { 186 | t.Fatal("psp shoud allow mount on hostpath /etc") 187 | } 188 | 189 | if len(hpMap) > 2 { 190 | t.Fatal("psp allow more host path mount than needed") 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/sysdiglabs/kube-psp-advisor 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/Azure/go-autorest/autorest v0.9.1 // indirect 7 | github.com/Azure/go-autorest/autorest/adal v0.6.0 // indirect 8 | github.com/ghodss/yaml v1.0.0 9 | github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d // indirect 10 | github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d // indirect 11 | github.com/gophercloud/gophercloud v0.4.0 // indirect 12 | github.com/hashicorp/go-version v1.2.0 13 | github.com/imdario/mergo v0.3.8 // indirect 14 | github.com/open-policy-agent/opa v0.27.1 15 | github.com/sirupsen/logrus v1.6.0 16 | github.com/spf13/cobra v0.0.5 17 | golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d // indirect 18 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 // indirect 19 | golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e // indirect 20 | golang.org/x/tools v0.1.6 // indirect 21 | gopkg.in/inf.v0 v0.9.0 // indirect 22 | k8s.io/api v0.0.0-20190816222004-e3a6b8045b0b 23 | k8s.io/apimachinery v0.0.0-20190816221834-a9f1d8a9c101 24 | k8s.io/client-go v11.0.0+incompatible 25 | k8s.io/klog v1.0.0 // indirect 26 | k8s.io/utils v0.0.0-20190923111123-69764acb6e8e // indirect 27 | ) 28 | -------------------------------------------------------------------------------- /kube-psp-advisor.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/sysdiglabs/kube-psp-advisor/comparator" 8 | 9 | "io/ioutil" 10 | 11 | "os" 12 | 13 | log "github.com/sirupsen/logrus" 14 | 15 | "path/filepath" 16 | 17 | "github.com/spf13/cobra" 18 | "github.com/sysdiglabs/kube-psp-advisor/advisor" 19 | "github.com/sysdiglabs/kube-psp-advisor/generator" 20 | 21 | "k8s.io/client-go/util/homedir" 22 | // Initialize all known client auth plugins. 23 | _ "k8s.io/client-go/plugin/pkg/client/auth" 24 | ) 25 | 26 | var ( 27 | validPolicyTypes = map[string]bool{ 28 | "psp": true, 29 | "opa": true, 30 | } 31 | ) 32 | 33 | func inspect(kubeconfig string, namespace string, excludeNamespaces []string, withReport, withGrant bool, OPAformat string, OPAdefaultRule bool) error { 34 | advisor, err := advisor.NewAdvisor(kubeconfig) 35 | 36 | if err != nil { 37 | return fmt.Errorf("Could not create advisor object: %v", err) 38 | } 39 | 40 | err = advisor.Process(namespace, excludeNamespaces, OPAformat, OPAdefaultRule) 41 | 42 | if err != nil { 43 | return fmt.Errorf("Could not run advisor to inspect cluster and generate PSP: %v", err) 44 | } 45 | 46 | if withReport { 47 | advisor.PrintReport() 48 | return nil 49 | } 50 | 51 | if withGrant { 52 | return advisor.PrintPodSecurityPolicyWithGrants() 53 | } 54 | 55 | if OPAformat == "psp" { 56 | err = advisor.PrintPodSecurityPolicy() 57 | if err != nil { 58 | return fmt.Errorf("Could not print PSP: %v", err) 59 | } 60 | } else if OPAformat == "opa" { 61 | opaRuleOutput := advisor.PrintOPAPolicy() 62 | if opaRuleOutput == "" { 63 | return fmt.Errorf("Could not print OPA rule: %v", err) 64 | } 65 | } 66 | return nil 67 | } 68 | 69 | func convert(podObjFilename string, pspFilename string, OPAformat string, OPAdefaultRule bool) error { 70 | podObjFile, err := os.Open(podObjFilename) 71 | if err != nil { 72 | return fmt.Errorf("Could not open pod object file %s for reading: %v", podObjFilename, err) 73 | } 74 | defer podObjFile.Close() 75 | 76 | log.Debugf("Reading pod Obj File from %s", podObjFilename) 77 | 78 | podObjString, err := ioutil.ReadAll(podObjFile) 79 | 80 | if err != nil { 81 | return fmt.Errorf("failed to read contents of pod object file %s: %v", podObjFilename, err) 82 | } 83 | 84 | log.Debugf("Contents of Obj File: %s", podObjString) 85 | 86 | psp_gen, err := generator.NewGenerator() 87 | if err != nil { 88 | return fmt.Errorf("failed to create PSP Generator: %v", err) 89 | } 90 | 91 | pspString, err := psp_gen.FromPodObjString(string(podObjString), OPAformat, OPAdefaultRule) 92 | if err != nil { 93 | return fmt.Errorf("failed to generate PSP from pod Object: %v", err) 94 | } 95 | 96 | err = ioutil.WriteFile(pspFilename, []byte(pspString), 0644) 97 | 98 | log.Infof("Wrote generated psp to %s", pspFilename) 99 | 100 | return nil 101 | } 102 | 103 | func comparePsp(srcDir, targetDir string) error { 104 | srcYamls, err := getWorkLoadYamls(srcDir) 105 | 106 | if err != nil { 107 | return fmt.Errorf("failed to read source workload directory %s: %s", srcDir, err) 108 | } 109 | 110 | targetYamls, err := getWorkLoadYamls(targetDir) 111 | 112 | if err != nil { 113 | return fmt.Errorf("failed to read target workload directory %s: %s", targetDir, err) 114 | } 115 | 116 | c, err := comparator.NewComparator() 117 | 118 | if err != nil { 119 | return fmt.Errorf("failed to create PSP comparator") 120 | } 121 | 122 | err = c.LoadYamls(srcYamls, comparator.Source) 123 | 124 | if err != nil { 125 | return fmt.Errorf("failed to create PSP comparator") 126 | } 127 | 128 | err = c.LoadYamls(targetYamls, comparator.Target) 129 | 130 | if err != nil { 131 | return fmt.Errorf("failed to create PSP comparator") 132 | } 133 | 134 | c.Compare() 135 | 136 | c.PrintEscalationReport() 137 | 138 | return nil 139 | } 140 | 141 | func main() { 142 | 143 | var kubeconfig string 144 | var withReport bool 145 | var withGrant bool 146 | var namespace string 147 | var excludeNamespaces []string 148 | var podObjFilename string 149 | var pspFilename string 150 | var policyType string 151 | var denyByDefault bool 152 | var logLevel string 153 | var srcYamlDir string 154 | var targetYamlDir string 155 | 156 | log.SetFormatter(&log.TextFormatter{ 157 | FullTimestamp: true, 158 | }) 159 | 160 | var rootCmd = &cobra.Command{ 161 | Use: "kube-psp-advisor", 162 | Short: "kube-psp-advisor generates K8s PodSecurityPolicies", 163 | Long: "A way to generate K8s PodSecurityPolicy objects from a live K8s environment or individual K8s objects containing pod specifications", 164 | PersistentPreRun: func(cmd *cobra.Command, args []string) { 165 | lvl, err := log.ParseLevel(logLevel) 166 | if err != nil { 167 | log.Fatal(err) 168 | } 169 | 170 | log.SetLevel(lvl) 171 | }, 172 | } 173 | 174 | rootCmd.PersistentFlags().StringVar(&logLevel, "level", "info", "Log level") 175 | 176 | var inspectCmd = &cobra.Command{ 177 | Use: "inspect", 178 | Short: "Inspect a live K8s Environment to generate a PodSecurityPolicy or OPA policy", 179 | Long: "Fetch all objects in the provided namespace to generate a Pod Security Policy or OPA policy", 180 | PreRun: func(cmd *cobra.Command, args []string) { 181 | if !validPolicyType(policyType) { 182 | log.Fatalf("invalid policy type") 183 | } 184 | }, 185 | 186 | Run: func(cmd *cobra.Command, args []string) { 187 | 188 | err := inspect(kubeconfig, namespace, excludeNamespaces, withReport, withGrant, policyType, denyByDefault) 189 | 190 | if err != nil { 191 | log.Fatalf("Could not run inspect command: %v", err) 192 | } 193 | }, 194 | } 195 | 196 | var convertCmd = &cobra.Command{ 197 | Use: "convert", 198 | Short: "Generate a PodSecurityPolicy or OPA policy from a single K8s Yaml file", 199 | Long: "Generate a PodSecurityPolicy or OPA policy from a single K8s Yaml file containing a pod Spec e.g. DaemonSet, Deployment, ReplicaSet, StatefulSet, ReplicationController, CronJob, Job, or Pod", 200 | PreRun: func(cmd *cobra.Command, args []string) { 201 | if podObjFilename == "" { 202 | log.Fatalf("--podFile must be provided") 203 | } 204 | 205 | if pspFilename == "" { 206 | log.Fatalf("--pspFile must be provided") 207 | } 208 | 209 | if !validPolicyType(policyType) { 210 | log.Fatalf("invalid policy type") 211 | } 212 | 213 | }, 214 | 215 | Run: func(cmd *cobra.Command, args []string) { 216 | err := convert(podObjFilename, pspFilename, policyType, denyByDefault) 217 | if err != nil { 218 | log.Fatalf("Could not run convert command: %v", err) 219 | } 220 | }, 221 | } 222 | 223 | var compareCmd = &cobra.Command{ 224 | Use: "compare", 225 | Short: "Compare k8s workload YAML files", 226 | Long: "Compare k8s workload YAML files and generate privilege escalation report", 227 | PreRun: func(cmd *cobra.Command, args []string) { 228 | if srcYamlDir == "" { 229 | log.Fatalf("--srcDir must be provided") 230 | } 231 | 232 | if targetYamlDir == "" { 233 | log.Fatalf("--targetDir must be provided") 234 | } 235 | }, 236 | 237 | Run: func(cmd *cobra.Command, args []string) { 238 | err := comparePsp(srcYamlDir, targetYamlDir) 239 | if err != nil { 240 | log.Fatalf("Could not run compare command: %v", err) 241 | } 242 | }, 243 | } 244 | 245 | if home := homedir.HomeDir(); home != "" { 246 | inspectCmd.Flags().StringVar(&kubeconfig, "kubeconfig", filepath.Join(home, ".kube", "config"), "(optional) absolute path to the kubeconfig file") 247 | } else { 248 | inspectCmd.Flags().StringVar(&kubeconfig, "kubeconfig", "", "absolute path to the kubeconfig file") 249 | } 250 | inspectCmd.Flags().BoolVarP(&withReport, "report", "r", false, "(optional) return with detail report") 251 | inspectCmd.Flags().BoolVarP(&withGrant, "grant", "g", false, "(optional) return with pod security policies, roles and rolebindings") 252 | inspectCmd.Flags().StringVarP(&namespace, "namespace", "n", "", "(optional) namespace") 253 | inspectCmd.Flags().StringSliceVarP(&excludeNamespaces, "exclude-namespaces", "e", []string{}, "(optional) comma separated list of namespaces to exclude") 254 | inspectCmd.Flags().StringVarP(&policyType, "policy", "p", "psp", "set policy type, valid policy types: psp and opa") 255 | inspectCmd.Flags().BoolVarP(&denyByDefault, "deny-by-default", "", false, "(optional) OPA default rule: use this option if OPA default rule is Deny ALL") 256 | 257 | convertCmd.Flags().StringVar(&podObjFilename, "podFile", "", "Path to a yaml file containing an object with a pod Spec") 258 | convertCmd.Flags().StringVar(&pspFilename, "pspFile", "", "Write the resulting output to this file") 259 | convertCmd.Flags().StringVarP(&policyType, "policy", "p", "psp", "set policy type, valid policy types: psp and opa)") 260 | convertCmd.Flags().BoolVarP(&denyByDefault, "deny-by-default", "", false, "(optional) OPA default rule: use this option if OPA default rule is Deny ALL") 261 | 262 | compareCmd.Flags().StringVar(&srcYamlDir, "sourceDir", "", "Source YAML directory to load YAMLs") 263 | compareCmd.Flags().StringVar(&targetYamlDir, "targetDir", "", "Target YAML directory to load YAMLs") 264 | 265 | rootCmd.AddCommand(inspectCmd) 266 | rootCmd.AddCommand(convertCmd) 267 | rootCmd.AddCommand(compareCmd) 268 | 269 | rootCmd.Execute() 270 | } 271 | 272 | func getWorkLoadYamls(dir string) ([]string, error) { 273 | yamls := []string{} 274 | 275 | err := filepath.Walk(dir, 276 | func(path string, info os.FileInfo, err error) error { 277 | if err != nil { 278 | return err 279 | } 280 | if !info.IsDir() && (strings.HasSuffix(path, ".yaml") || strings.HasSuffix(path, ".yml")) { 281 | yamls = append(yamls, path) 282 | } 283 | return nil 284 | }) 285 | if err != nil { 286 | log.Println(err) 287 | } 288 | 289 | return yamls, nil 290 | } 291 | 292 | func validPolicyType(policyType string) bool { 293 | _, exists := validPolicyTypes[strings.ToLower(policyType)] 294 | 295 | return exists 296 | } 297 | -------------------------------------------------------------------------------- /kube-psp-advisor_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "sort" 5 | "strings" 6 | "testing" 7 | ) 8 | 9 | var ( 10 | workloadDir = "./test-yaml" 11 | 12 | expectedYamls = []string{ 13 | "test-yaml/base-busybox.yaml", 14 | "test-yaml/psp-grant.yaml", 15 | "test-yaml/srcYamls/busy-box.yaml", 16 | "test-yaml/srcYamls/nginx.yaml", 17 | "test-yaml/targetYamls/busy-box.yaml", 18 | "test-yaml/targetYamls/nginx.yaml", 19 | "test-yaml/targetYamls/web-deployment.yaml", 20 | "test-yaml/test-opa.yaml", 21 | } 22 | ) 23 | 24 | func TestReadYamls(t *testing.T) { 25 | yamls, err := getWorkLoadYamls(workloadDir) 26 | 27 | if err != nil { 28 | t.Fatal(err) 29 | } 30 | 31 | sort.Strings(yamls) 32 | 33 | if strings.Join(yamls, ";") != strings.Join(expectedYamls, ";") { 34 | t.Fatalf("expected: %s\nactual: %s\n", expectedYamls, yamls) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /scripts/build: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -eux 4 | 5 | export GOPATH="$HOME/go" 6 | export PATH="$PATH:$GOPATH/bin" 7 | 8 | go get "golang.org/x/tools/cmd/goimports" 9 | 10 | goimports -w $(find . -type f -name '*.go' -not -path "./vendor/*") || true 11 | 12 | env GO111MODULE=on GOOS=$(uname -s | tr '[:upper:]' '[:lower:]') GOARCH=amd64 go build -a 13 | -------------------------------------------------------------------------------- /scripts/example: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eu 4 | 5 | kubectl delete -f examples/ns.yaml || true 6 | 7 | kubectl delete psp psp-privileged psp-restricted || true 8 | 9 | # create namespaces 10 | kubectl apply -f examples/ns.yaml || true 11 | 12 | # create service accounts 13 | kubectl apply -f examples/sa.yaml || true 14 | 15 | # create roles and rolebindings for service accounts to use pod security policies 16 | kubectl apply -f examples/roles.yaml || true 17 | 18 | # create pods 19 | kubectl apply -f examples/pods.yaml || true 20 | 21 | # generate psp and update the pod security policy name 22 | ./kube-psp-advisor inspect --namespace privileged | sed -e 's/pod-security.*/psp-privileged/g' | kubectl apply -f - 23 | 24 | ./kube-psp-advisor inspect --namespace restricted | sed -e 's/pod-security.*/psp-restricted/g' | kubectl apply -f - 25 | 26 | # test creating pods that pass the pod security policies 27 | kubectl apply -f examples/pods-allow.yaml || true 28 | 29 | kubectl get pods -n privileged 30 | 31 | kubectl get pods -n restricted 32 | 33 | # test creating pod that violate pod security policies 34 | kubectl apply -f examples/pods-deny.yaml || true 35 | 36 | kubectl get pods -n privileged 37 | 38 | kubectl get pods -n restricted 39 | 40 | -------------------------------------------------------------------------------- /scripts/test: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | go test ./... 4 | 5 | kubectl delete -f test-yaml/base-busybox.yaml 6 | kubectl apply -f test-yaml/base-busybox.yaml 7 | 8 | sleep 5 9 | 10 | ./kube-psp-advisor inspect 11 | -------------------------------------------------------------------------------- /test-yaml/base-busybox.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: psp-test 5 | --- 6 | apiVersion: v1 7 | kind: ServiceAccount 8 | metadata: 9 | name: sa-1 10 | namespace: psp-test 11 | --- 12 | apiVersion: v1 13 | kind: ServiceAccount 14 | metadata: 15 | name: sa-2 16 | namespace: psp-test 17 | --- 18 | apiVersion: v1 19 | kind: ServiceAccount 20 | metadata: 21 | name: sa-3 22 | namespace: psp-test 23 | --- 24 | apiVersion: v1 25 | kind: Pod 26 | metadata: 27 | name: busy-pod 28 | namespace: psp-test 29 | spec: 30 | securityContext: 31 | sysctls: 32 | - name: kernel.shm_rmid_forced 33 | value: "0" 34 | - name: net.core.somaxconn 35 | value: "1024" 36 | containers: 37 | - name: my-busybox 38 | image: busybox 39 | volumeMounts: 40 | - mountPath: /test-hostpath 41 | name: test-volume 42 | readOnly: true 43 | command: 44 | - sleep 45 | - "3600" 46 | securityContext: 47 | privileged: false 48 | runAsNonRoot: false 49 | readOnlyRootFilesystem: false 50 | allowPrivilegeEscalation: true 51 | hostPID: false 52 | hostIPC: false 53 | hostNetwork: true 54 | serviceAccount: sa-1 55 | volumes: 56 | - name: test-volume 57 | hostPath: 58 | # directory location on host 59 | path: /usr/bin 60 | # this field is optional 61 | type: Directory 62 | --- 63 | apiVersion: batch/v1 64 | kind: Job 65 | metadata: 66 | name: busy-job 67 | namespace: psp-test 68 | spec: 69 | template: 70 | spec: 71 | serviceAccount: sa-2 72 | restartPolicy: Never 73 | containers: 74 | - name: my-busybox 75 | image: busybox 76 | volumeMounts: 77 | - mountPath: /test-hostpath 78 | name: test-volume 79 | readOnly: true 80 | command: 81 | - sleep 82 | - "3600" 83 | securityContext: 84 | privileged: false 85 | runAsNonRoot: true 86 | readOnlyRootFilesystem: false 87 | allowPrivilegeEscalation: false 88 | runAsUser: 10001 89 | hostPID: false 90 | hostIPC: true 91 | hostNetwork: false 92 | volumes: 93 | - name: test-volume 94 | hostPath: 95 | # directory location on host 96 | path: /usr/bin 97 | # this field is optional 98 | type: Directory 99 | --- 100 | apiVersion: apps/v1 101 | kind: Deployment 102 | metadata: 103 | name: busy-deploy 104 | namespace: psp-test 105 | labels: 106 | app: busy-deploy 107 | spec: 108 | replicas: 3 109 | selector: 110 | matchLabels: 111 | app: busy-deploy 112 | template: 113 | metadata: 114 | labels: 115 | app: busy-deploy 116 | spec: 117 | serviceAccount: sa-2 118 | containers: 119 | - name: my-busybox 120 | image: busybox 121 | volumeMounts: 122 | - mountPath: /test-hostpath 123 | name: test-volume 124 | command: 125 | - sleep 126 | - "3600" 127 | securityContext: 128 | privileged: false 129 | runAsNonRoot: true 130 | readOnlyRootFilesystem: false 131 | allowPrivilegeEscalation: false 132 | runAsUser: 10001 133 | capabilities: 134 | add: 135 | - SYS_ADMIN 136 | - NET_ADMIN 137 | drop: 138 | - SYS_CHROOT 139 | hostPID: true 140 | hostIPC: false 141 | hostNetwork: false 142 | volumes: 143 | - name: test-volume 144 | hostPath: 145 | # directory location on host 146 | path: /tmp 147 | # this field is optional 148 | type: Directory 149 | --- 150 | apiVersion: v1 151 | kind: ReplicationController 152 | metadata: 153 | name: busy-rc 154 | namespace: psp-test 155 | labels: 156 | app: busy-rc 157 | spec: 158 | replicas: 2 159 | selector: 160 | app: busy-rc 161 | template: 162 | metadata: 163 | labels: 164 | app: busy-rc 165 | spec: 166 | containers: 167 | - name: my-busybox 168 | image: busybox 169 | volumeMounts: 170 | - mountPath: /test-hostpath 171 | name: test-volume 172 | readOnly: true 173 | command: 174 | - sleep 175 | - "3600" 176 | securityContext: 177 | privileged: true 178 | runAsNonRoot: false 179 | readOnlyRootFilesystem: false 180 | allowPrivilegeEscalation: true 181 | runAsUser: 10001 182 | hostPID: false 183 | hostIPC: false 184 | hostNetwork: false 185 | volumes: 186 | - name: test-volume 187 | hostPath: 188 | # directory location on host 189 | path: /usr/sbin 190 | # this field is optional 191 | type: Directory 192 | --- 193 | apiVersion: apps/v1 194 | kind: DaemonSet 195 | metadata: 196 | name: busy-ds 197 | namespace: psp-test 198 | labels: 199 | app: busy-ds 200 | spec: 201 | selector: 202 | matchLabels: 203 | app: busy-ds 204 | template: 205 | metadata: 206 | labels: 207 | app: busy-ds 208 | spec: 209 | securityContext: 210 | sysctls: 211 | - name: kernel.shm_rmid_forced 212 | value: "0" 213 | - name: kernel.msgmax 214 | value: "65536" 215 | serviceAccount: sa-2 216 | containers: 217 | - name: my-busybox 218 | image: busybox 219 | volumeMounts: 220 | - mountPath: /test-hostpath 221 | name: test-volume 222 | command: 223 | - sleep 224 | - "3600" 225 | securityContext: 226 | privileged: false 227 | runAsNonRoot: true 228 | readOnlyRootFilesystem: false 229 | allowPrivilegeEscalation: true 230 | runAsUser: 10001 231 | hostPID: false 232 | hostIPC: false 233 | hostNetwork: false 234 | volumes: 235 | - name: test-volume 236 | hostPath: 237 | # directory location on host 238 | path: /bin 239 | # this field is optional 240 | type: Directory 241 | --- 242 | apiVersion: apps/v1 243 | kind: ReplicaSet 244 | metadata: 245 | name: busy-rs 246 | namespace: psp-test 247 | labels: 248 | app: busy-rs 249 | spec: 250 | replicas: 2 251 | selector: 252 | matchLabels: 253 | app: busy-rs 254 | template: 255 | metadata: 256 | labels: 257 | app: busy-rs 258 | spec: 259 | containers: 260 | - name: my-busybox 261 | image: busybox 262 | volumeMounts: 263 | - name: config-vol 264 | mountPath: /game/config 265 | command: 266 | - sleep 267 | - "3600" 268 | securityContext: 269 | privileged: false 270 | runAsNonRoot: true 271 | readOnlyRootFilesystem: false 272 | allowPrivilegeEscalation: false 273 | runAsUser: 20002 274 | capabilities: 275 | add: 276 | - SYS_ADMIN 277 | drop: 278 | - SYS_CHROOT 279 | hostPID: true 280 | hostIPC: true 281 | hostNetwork: true 282 | serviceAccount: sa-1 283 | volumes: 284 | - name: config-vol 285 | configMap: 286 | name: game-config 287 | --- 288 | apiVersion: v1 289 | kind: ConfigMap 290 | metadata: 291 | name: game-config 292 | namespace: psp-test 293 | data: 294 | game.properties: | 295 | enemies=aliens 296 | lives=3 297 | enemies.cheat=true 298 | enemies.cheat.level=noGoodRotten 299 | secret.code.passphrase=UUDDLRLRBABAS 300 | secret.code.allowed=true 301 | secret.code.lives=30 302 | ui.properties: | 303 | color.good=purple 304 | color.bad=yellow 305 | allow.textmode=true 306 | how.nice.to.look=fairlyNice 307 | -------------------------------------------------------------------------------- /test-yaml/psp-grant.yaml: -------------------------------------------------------------------------------- 1 | # Pod security policies will NOT be created for service account 'default' in namespace 'psp-test' with following workdloads: 2 | # Kind: ReplicationController, Name: busy-rc, Image: busybox 3 | --- 4 | # Pod security policies will be created for service account 'sa-1' in namespace 'psp-test' with following workdloads: 5 | # Kind: ReplicaSet, Name: busy-rs, Image: busybox 6 | # Kind: Pod, Name: busy-pod, Image: busybox 7 | apiVersion: policy/v1beta1 8 | kind: PodSecurityPolicy 9 | metadata: 10 | creationTimestamp: null 11 | name: psp-for-psp-test-sa-1 12 | spec: 13 | allowedCapabilities: 14 | - SYS_ADMIN 15 | allowedHostPaths: 16 | - pathPrefix: /usr/bin 17 | readOnly: true 18 | fsGroup: 19 | rule: RunAsAny 20 | hostIPC: true 21 | hostNetwork: true 22 | hostPID: true 23 | runAsUser: 24 | rule: RunAsAny 25 | seLinux: 26 | rule: RunAsAny 27 | supplementalGroups: 28 | rule: RunAsAny 29 | volumes: 30 | - configMap 31 | - secret 32 | - hostPath 33 | --- 34 | apiVersion: rbac.authorization.k8s.io/v1 35 | kind: Role 36 | metadata: 37 | creationTimestamp: null 38 | name: use-psp-by-psp-test:sa-1 39 | namespace: psp-test 40 | rules: 41 | - apiGroups: 42 | - policy 43 | resourceNames: 44 | - psp-for-psp-test-sa-1 45 | resources: 46 | - podsecuritypolicies 47 | verbs: 48 | - use 49 | --- 50 | apiVersion: rbac.authorization.k8s.io/v1 51 | kind: RoleBinding 52 | metadata: 53 | creationTimestamp: null 54 | name: use-psp-by-psp-test:sa-1-binding 55 | namespace: psp-test 56 | roleRef: 57 | apiGroup: rbac.authorization.k8s.io 58 | kind: Role 59 | name: use-psp-by-psp-test:sa-1 60 | subjects: 61 | - kind: ServiceAccount 62 | name: sa-1 63 | namespace: psp-test 64 | --- 65 | # Pod security policies will be created for service account 'sa-2' in namespace 'psp-test' with following workdloads: 66 | # Kind: DaemonSet, Name: busy-ds, Image: busybox 67 | # Kind: Deployment, Name: busy-deploy, Image: busybox 68 | # Kind: Job, Name: busy-job, Image: busybox 69 | apiVersion: policy/v1beta1 70 | kind: PodSecurityPolicy 71 | metadata: 72 | creationTimestamp: null 73 | name: psp-for-psp-test-sa-2 74 | spec: 75 | allowedCapabilities: 76 | - SYS_ADMIN 77 | - NET_ADMIN 78 | allowedHostPaths: 79 | - pathPrefix: /bin 80 | readOnly: true 81 | - pathPrefix: /tmp 82 | readOnly: true 83 | - pathPrefix: /usr/bin 84 | readOnly: true 85 | fsGroup: 86 | rule: RunAsAny 87 | hostIPC: true 88 | hostPID: true 89 | runAsUser: 90 | ranges: 91 | - max: 10001 92 | min: 10001 93 | rule: MustRunAs 94 | seLinux: 95 | rule: RunAsAny 96 | supplementalGroups: 97 | rule: RunAsAny 98 | volumes: 99 | - hostPath 100 | - secret 101 | --- 102 | apiVersion: rbac.authorization.k8s.io/v1 103 | kind: Role 104 | metadata: 105 | creationTimestamp: null 106 | name: use-psp-by-psp-test:sa-2 107 | namespace: psp-test 108 | rules: 109 | - apiGroups: 110 | - policy 111 | resourceNames: 112 | - psp-for-psp-test-sa-2 113 | resources: 114 | - podsecuritypolicies 115 | verbs: 116 | - use 117 | --- 118 | apiVersion: rbac.authorization.k8s.io/v1 119 | kind: RoleBinding 120 | metadata: 121 | creationTimestamp: null 122 | name: use-psp-by-psp-test:sa-2-binding 123 | namespace: psp-test 124 | roleRef: 125 | apiGroup: rbac.authorization.k8s.io 126 | kind: Role 127 | name: use-psp-by-psp-test:sa-2 128 | subjects: 129 | - kind: ServiceAccount 130 | name: sa-2 131 | namespace: psp-test 132 | --- 133 | -------------------------------------------------------------------------------- /test-yaml/srcYamls/busy-box.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: my-busybox 5 | namespace: psp-test 6 | spec: 7 | serviceAccountName: psp-test-sa 8 | containers: 9 | - name: my-busybox-container 10 | image: busybox 11 | args: ["sleep", "3000"] 12 | securityContext: 13 | privileged: true 14 | runAsNonRoot: false 15 | readOnlyRootFilesystem: false 16 | allowPrivilegeEscalation: true 17 | capabilities: 18 | add: 19 | - SYS_ADMIN 20 | hostPID: true 21 | hostIPC: false 22 | hostNetwork: true 23 | -------------------------------------------------------------------------------- /test-yaml/srcYamls/nginx.yaml: -------------------------------------------------------------------------------- 1 | # add comment 2 | apiVersion: v1 3 | kind: Pod 4 | metadata: 5 | name: nginx 6 | labels: 7 | app: nginx 8 | spec: 9 | hostNetwork: false 10 | hostIPC: false 11 | hostPID: false 12 | containers: 13 | - name: nginx 14 | image: kaizheh/nginx 15 | securityContext: 16 | privileged: false 17 | readOnlyRootFilesystem: true 18 | runAsUser: 100 19 | runAsGroup: 1000 20 | -------------------------------------------------------------------------------- /test-yaml/subpath.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: my-lamp-site 5 | spec: 6 | containers: 7 | - name: mysql 8 | image: mysql 9 | env: 10 | # this is a bad example for testing purpose 11 | - name: MYSQL_ROOT_PASSWORD 12 | value: "rootpasswd" 13 | volumeMounts: 14 | - mountPath: /var/lib/mysql 15 | name: site-data 16 | subPath: mysql 17 | - name: php 18 | image: php:7.0-apache 19 | volumeMounts: 20 | - mountPath: /var/www/html 21 | name: site-data 22 | subPath: html 23 | volumes: 24 | - name: site-data 25 | persistentVolumeClaim: 26 | claimName: my-lamp-site-data 27 | -------------------------------------------------------------------------------- /test-yaml/targetYamls/busy-box.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: my-busybox 5 | namespace: psp-test 6 | spec: 7 | serviceAccountName: psp-test-sa 8 | containers: 9 | - name: my-busybox-container 10 | image: busybox 11 | args: ["sleep", "3000"] 12 | securityContext: 13 | privileged: false 14 | runAsNonRoot: true 15 | readOnlyRootFilesystem: true 16 | allowPrivilegeEscalation: false 17 | capabilities: 18 | drop: 19 | - SYS_CHROOT 20 | hostPID: false 21 | hostIPC: true 22 | hostNetwork: false 23 | -------------------------------------------------------------------------------- /test-yaml/targetYamls/nginx.yaml: -------------------------------------------------------------------------------- 1 | # add comment 2 | apiVersion: v1 3 | kind: Pod 4 | metadata: 5 | name: nginx 6 | labels: 7 | app: nginx 8 | spec: 9 | hostNetwork: true 10 | hostIPC: true 11 | hostPID: true 12 | containers: 13 | - name: nginx 14 | image: kaizheh/nginx 15 | securityContext: 16 | privileged: true 17 | readOnlyRootFilesystem: false 18 | volumeMounts: 19 | - name: tmp 20 | mountPath: /var/tmp 21 | volumes: 22 | - name: tmp 23 | hostPath: 24 | path: /tmp 25 | type: Directory 26 | -------------------------------------------------------------------------------- /test-yaml/targetYamls/web-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | labels: 5 | app: apache-unomi 6 | name: apache-unomi-aaa 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: apache-unomi 12 | template: 13 | metadata: 14 | labels: 15 | app: apache-unomi 16 | spec: 17 | hostIPC: false 18 | hostNetwork: true 19 | containers: 20 | - name: apache-unomi 21 | image: vulhub/unomi:1.5.1 22 | env: 23 | - name: UNOMI_ELASTICSEARCH_ADDRESSES 24 | valueFrom: 25 | configMapKeyRef: 26 | name: elasticsearch-configmap 27 | key: db_host 28 | securityContext: 29 | privileged: false 30 | runAsNonRoot: true 31 | readOnlyRootFilesystem: true 32 | allowPrivilegeEscalation: false 33 | runAsUser: 2000 34 | runAsGroup: 3000 35 | capabilities: 36 | drop: 37 | - SYS_CHROOT 38 | volumeMounts: 39 | - name: tmp 40 | mountPath: /var/tmp 41 | - name: security-context-vol 42 | mountPath: /data/test 43 | readOnly: true 44 | ports: 45 | - containerPort: 8181 46 | hostPort: 8181 47 | volumes: 48 | - name: tmp 49 | hostPath: 50 | path: /tmp 51 | type: Directory 52 | - name: security-context-vol 53 | hostPath: 54 | path: /test 55 | type: Directory -------------------------------------------------------------------------------- /test-yaml/test-opa.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: security-context-pod 5 | spec: 6 | securityContext: 7 | runAsUser: 2500 8 | fsGroup: 2000 9 | containers: 10 | - name: security-context-cont 11 | image: supergiantkir/k8s-liveliness 12 | ports: 13 | - name: web 14 | containerPort: 80 15 | hostPort: 80 16 | protocol: TCP 17 | securityContext: 18 | allowPrivilegeEscalation: false 19 | runAsUser: 2500 20 | runAsGroup: 2500 21 | readOnlyRootFilesystem: true 22 | capabilities: 23 | add: 24 | - SYS_CHROOT 25 | - AUDIT_WRITE 26 | drop: 27 | - AAA 28 | volumeMounts: 29 | - name: tmp 30 | mountPath: /var/tmp 31 | - name: security-context-vol 32 | mountPath: /data/test 33 | readOnly: true 34 | - name: security-context-bg 35 | image: supergiantkir/k8s-liveliness 36 | ports: 37 | - name: web 38 | containerPort: 8080 39 | hostPort: 8080 40 | protocol: TCP 41 | volumeMounts: 42 | - name: security-context-vol 43 | mountPath: /data/test 44 | readOnly: true 45 | securityContext: 46 | allowPrivilegeEscalation: false 47 | runAsUser: 2400 48 | runAsGroup: 2400 49 | capabilities: 50 | add: 51 | - SYS_CHROOT 52 | - SYS_ADMIN 53 | - AUDIT_WRITE 54 | volumes: 55 | - name: tmp 56 | hostPath: 57 | path: /tmp 58 | type: Directory 59 | - name: security-context-vol 60 | hostPath: 61 | path: /test 62 | type: Directory -------------------------------------------------------------------------------- /utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | // Convert array to map 4 | func ArrayToMap(input []string) map[string]bool { 5 | output := map[string]bool{} 6 | for _, s := range input { 7 | output[s] = true 8 | } 9 | return output 10 | } 11 | 12 | func MapToArray(input map[string]bool) []string { 13 | output := []string{} 14 | for k := range input { 15 | output = append(output, k) 16 | } 17 | 18 | return output 19 | } 20 | -------------------------------------------------------------------------------- /utils/version.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "github.com/hashicorp/go-version" 5 | ) 6 | 7 | // CompareVersion compare two versions 8 | func CompareVersion(v1, v2 string) (bool, error) { 9 | version1, err := version.NewVersion(v1) 10 | 11 | if err != nil { 12 | return false, err 13 | } 14 | 15 | version2, err := version.NewVersion(v2) 16 | 17 | if err != nil { 18 | return false, err 19 | } 20 | 21 | return version1.GreaterThan(version2), nil 22 | } 23 | -------------------------------------------------------------------------------- /utils/version_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestCompareVersionVersion(t *testing.T) { 8 | v1 := "v1.12.1" 9 | v2 := "v1.11.2" 10 | 11 | ret, err := CompareVersion(v1, v2) 12 | if err != nil { 13 | t.Fatal(err) 14 | } 15 | 16 | if !ret { 17 | t.Fatal("Version comparison failed.") 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /version: -------------------------------------------------------------------------------- 1 | 1.8.0 2 | --------------------------------------------------------------------------------