├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── enhancement-request.md └── workflows │ ├── build.yml │ └── release.yaml ├── .gitignore ├── .goreleaser.yml ├── LICENSE ├── Makefile ├── README.md ├── cmd ├── analysis_cmd.go ├── auditgen_cmd.go ├── generate_cmd.go ├── lookup_cmd.go ├── metadata_arg.go ├── policyrules_cmd.go ├── show_permissions_cmd.go ├── version.go ├── visualize_cmd.go ├── whoami_cmd.go └── whocan_cmd.go ├── download.sh ├── go.mod ├── go.sum ├── hack ├── goreleaser-download.sh └── goreleaser-postbuild.sh ├── img └── rbac-viz-html-example.png ├── krew.yaml ├── main.go ├── notes.md ├── pkg ├── analysis │ ├── analysis.go │ ├── default-rules.yaml │ ├── default_rules.go │ ├── default_rules_test.go │ ├── report.go │ └── types.go ├── audit │ ├── event_filter.go │ ├── event_reader.go │ ├── process.go │ └── util.go ├── kube │ └── client.go ├── rbac │ ├── describer.go │ ├── permissions.go │ ├── static.go │ └── subject_permissions.go ├── utils │ ├── console_printer.go │ ├── namespaces.go │ ├── object_reader.go │ ├── struct_to_map.go │ └── writefile.go ├── visualize │ ├── dotgraph.go │ ├── output.go │ ├── rbacviz.go │ └── types.go └── whoami │ └── whoami.go ├── rbac-tool.png └── testdata ├── auditgen ├── testdata-01.json └── testdata-02.json ├── policyrules └── multiple-role-bindings.yaml ├── viz ├── multiple-rolebindings-in-ns-missing-service-account.yaml ├── multiple-rolebindings-in-ns.yaml └── rbac-with-psp.yaml └── whocan ├── clusterrole-aggregate.yaml ├── gatewat-api-operator.yaml ├── impersonator.yaml ├── nonresourceurl-reader.yaml ├── pod-creator.yaml ├── secret-reader.yaml └── specific-resource-reader.yaml /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Report a bug encountered 4 | title: '' 5 | labels: kind/bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **What happened**: 11 | 12 | **What you expected to happen**: 13 | 14 | **How to reproduce it (as minimally and precisely as possible)**: 15 | 16 | **Anything else we need to know?**: 17 | 18 | **Environment**: 19 | - Kubernetes version (use `kubectl version`): 20 | - Cloud provider or configuration: 21 | - Install tools: 22 | - Others: -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/enhancement-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Enhancement Request 3 | about: Suggest an enhancement 4 | title: '' 5 | labels: kind/enhancement 6 | assignees: '' 7 | 8 | --- 9 | **What would you like to be added**: 10 | 11 | **Why is this needed**: -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build On Push 2 | on: 3 | pull_request: 4 | branches: 5 | - master 6 | jobs: 7 | 8 | build: 9 | name: Build 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Set up Go 13 | uses: actions/setup-go@v5 14 | with: 15 | go-version: '1.22' 16 | id: go 17 | 18 | - name: Check out code into the Go module directory 19 | uses: actions/checkout@v3 20 | 21 | - name: Build & Test 22 | run: | 23 | make get-bins 24 | make test 25 | make gorelease-snapshot 26 | 27 | 28 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Release 3 | on: 4 | push: 5 | # Sequence of patterns matched against refs/tags 6 | tags: 7 | - '*' # Push events to matching v*, i.e. v1.0, v20.15.10 8 | 9 | jobs: 10 | release: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v3 15 | - name: Unshallow 16 | run: git fetch --prune --unshallow 17 | - name: Set up Go 18 | uses: actions/setup-go@v5 19 | with: 20 | go-version: '1.22' 21 | - name: Build & Release 22 | env: 23 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 24 | run: | 25 | make get-bins 26 | make test 27 | make gorelease 28 | - name: Update new version in krew-index 29 | uses: rajatjindal/krew-release-bot@v0.0.46 30 | with: 31 | krew_template_file: krew.yaml 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | vendor/ 16 | 17 | build/* 18 | dist/* 19 | bin/* 20 | 21 | .idea/ 22 | .vscode/ 23 | 24 | 25 | rbac.dot 26 | rbac.html 27 | rbac-tool.png 28 | krew-test.yaml -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | project_name: rbac-tool 2 | env: 3 | #- GO111MODULE=on 4 | #- GOPROXY=https://gocenter.io 5 | before: 6 | hooks: 7 | # You may remove this if you don't use go modules. 8 | #- go mod download 9 | # you may remove this if you don't need go generate 10 | #- go generate ./... 11 | builds: 12 | - env: 13 | - CGO_ENABLED=0 14 | flags: 15 | # Custom ldflags templates. 16 | # Default is `-s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}} -X main.builtBy=goreleaser`. 17 | ldflags: 18 | - -s -w -X github.com/alcideio/rbac-tool/cmd.Commit={{.Commit}} -X github.com/alcideio/rbac-tool/cmd.Version={{.Version}} 19 | # Binary name. 20 | # Can be a path (e.g. `bin/app`) to wrap the binary in a directory. 21 | # Default is the name of the project directory. 22 | binary: rbac-tool 23 | # Path to main.go file or main package. 24 | # Default is `.`. 25 | main: main.go 26 | 27 | goos: 28 | - linux 29 | - darwin 30 | - windows 31 | goarch: 32 | #- 386 33 | - amd64 34 | - arm 35 | - arm64 36 | ignore: 37 | - goos: darwin 38 | goarch: 386 39 | - goos: linux 40 | goarch: 386 41 | - goos: windows 42 | goarch: arm 43 | - goos: windows 44 | goarch: arm64 45 | hooks: 46 | post: /bin/bash hack/goreleaser-postbuild.sh {{ .ProjectName }}_{{ .Os }}_{{ .Arch }} {{ .Os }} 47 | 48 | #signs: 49 | # - artifacts: checksum 50 | 51 | checksum: 52 | name_template: '{{ .ProjectName }}_checksums.txt' 53 | 54 | changelog: 55 | sort: asc 56 | filters: 57 | exclude: 58 | - '^docs:' 59 | - '^test:' 60 | - Merge pull request 61 | - Merge branch 62 | 63 | archives: 64 | - id: default 65 | builds: 66 | - rbac-tool 67 | name_template: '{{ .ProjectName }}_v{{ .Major }}.{{ .Minor }}.{{ .Patch }}_{{ .Os }}_{{ .Arch }}' 68 | format: tar.gz 69 | 70 | 71 | release: 72 | # Repo in which the release will be created. 73 | # Default is extracted from the origin remote URL. 74 | # Note: it can only be one: either github or gitlab or gitea 75 | github: 76 | owner: alcideio 77 | name: rbac-tool 78 | 79 | # If set to true, will not auto-publish the release. 80 | # Default is false. 81 | draft: false 82 | 83 | # If set to auto, will mark the release as not ready for production 84 | # in case there is an indicator for this in the tag e.g. v1.0.0-rc1 85 | # If set to true, will mark the release as not ready for production. 86 | # Default is false. 87 | prerelease: false 88 | 89 | # You can change the name of the GitHub release. 90 | # Default is `{{.Tag}}` 91 | name_template: "v{{ .Major }}.{{ .Minor }}.{{ .Patch }}" 92 | 93 | # You can disable this pipe in order to not upload any artifacts to 94 | # GitHub. 95 | # Defaults to false. 96 | #disable: true 97 | 98 | # You can add extra pre-existing files to the release. 99 | # The filename on the release will be the last part of the path (base). If 100 | # another file with the same name exists, the latest one found will be used. 101 | # Defaults to empty. 102 | # extra_files: 103 | # - glob: ./path/to/file.txt 104 | # - glob: ./glob/**/to/**/file/**/* 105 | # - glob: ./glob/foo/to/bar/file/foobar/override_from_previous 106 | 107 | 108 | -------------------------------------------------------------------------------- /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 | # 2 | # rbac-tool --> Locates : Locate k8s, Helm & kustomize configuration issues and provide recommendation 3 | # 4 | .SECONDARY: 5 | .SECONDEXPANSION: 6 | 7 | BINDIR := $(CURDIR)/bin 8 | DIST_DIRS := find * -type d -exec 9 | DIST_EXES := find * -type f -executable -exec 10 | # Go Targets darwin/amd64 linux/amd64 linux/386 linux/arm linux/arm64 linux/ppc64le linux/s390x windows/amd64 11 | TARGETS := darwin/amd64 linux/amd64 windows/amd64 12 | TARGET_OBJS ?= darwin-amd64.tar.gz darwin-amd64.tar.gz.sha256 darwin-amd64.tar.gz.sha256sum linux-amd64.tar.gz linux-amd64.tar.gz.sha256 linux-amd64.tar.gz.sha256sum linux-386.tar.gz linux-386.tar.gz.sha256 linux-386.tar.gz.sha256sum linux-arm.tar.gz linux-arm.tar.gz.sha256 linux-arm.tar.gz.sha256sum linux-arm64.tar.gz linux-arm64.tar.gz.sha256 linux-arm64.tar.gz.sha256sum linux-ppc64le.tar.gz linux-ppc64le.tar.gz.sha256 linux-ppc64le.tar.gz.sha256sum linux-s390x.tar.gz linux-s390x.tar.gz.sha256 linux-s390x.tar.gz.sha256sum windows-amd64.zip windows-amd64.zip.sha256 windows-amd64.zip.sha256sum 13 | BINNAME ?= rbac-tool 14 | 15 | GOPATH = $(shell go env GOPATH) 16 | DEP = $(GOPATH)/bin/dep 17 | GOX = $(GOPATH)/bin/gox 18 | GOIMPORTS = $(GOPATH)/bin/goimports 19 | ARCH = $(shell uname -p) 20 | 21 | UPX_VERSION := 4.0.2 22 | UPX := $(CURDIR)/rbac-tool/bin/upx 23 | 24 | GORELEASER_VERSION := 1.15.0 25 | GORELEASER := $(CURDIR)/bin/goreleaser 26 | 27 | # go option 28 | PKG := ./... 29 | TAGS := 30 | TESTS := . 31 | TESTFLAGS := 32 | LDFLAGS := -w -s 33 | GOFLAGS := 34 | SRC := $(shell find . -type f -name '*.go' -print) 35 | 36 | # Required for globs to work correctly 37 | #SHELL = /usr/bin/env bash 38 | 39 | GIT_COMMIT = $(shell git rev-parse HEAD) 40 | GIT_SHA = $(shell git rev-parse --short HEAD) 41 | GIT_TAG = $(shell git describe --tags --abbrev=0 --exact-match 2>/dev/null) 42 | GIT_DIRTY = $(shell test -n "`git status --porcelain`" && echo "dirty" || echo "clean") 43 | 44 | LDFLAGS += -X github.com/alcideio/rbac-tool/cmd.Commit=${GIT_SHA} 45 | 46 | BINARY_VERSION ?= ${GIT_TAG} 47 | ifdef VERSION 48 | BINARY_VERSION = $(VERSION) 49 | endif 50 | 51 | 52 | ## Only set Version if building a tag or VERSION is set 53 | ifneq ($(BINARY_VERSION),) 54 | LDFLAGS += -X github.com/alcideio/rbac-tool/cmd.Version=${BINARY_VERSION} 55 | endif 56 | 57 | get-bins: get-release-bins ##@build Download UPX 58 | wget https://github.com/upx/upx/releases/download/v${UPX_VERSION}/upx-${UPX_VERSION}-amd64_linux.tar.xz && \ 59 | tar xvf upx-${UPX_VERSION}-amd64_linux.tar.xz &&\ 60 | mkdir -p $(CURDIR)/bin || echo "dir already exist" &&\ 61 | cp upx-${UPX_VERSION}-amd64_linux/upx $(CURDIR)/bin/upx &&\ 62 | rm -Rf upx-${UPX_VERSION}-amd64_linux* 63 | 64 | get-release-bins: ##@build Download goreleaser 65 | mkdir -p $(CURDIR)/bin/goreleaser_install || echo "dir already exist" &&\ 66 | cd $(CURDIR)/bin &&\ 67 | $(CURDIR)/hack/goreleaser-download.sh ${GORELEASER_VERSION} $(CURDIR)/bin/goreleaser_install &&\ 68 | mv $(CURDIR)/bin/goreleaser_install/goreleaser $(CURDIR)/bin/goreleaser &&\ 69 | rm -Rf $(CURDIR)/bin/goreleaser_install 70 | 71 | 72 | 73 | .PHONY: build 74 | build: ##@build Build on local platform 75 | export CGO_ENABLED=0 && go build -o $(BINDIR)/$(BINNAME) -tags staticbinary -v -ldflags '$(LDFLAGS)' github.com/alcideio/rbac-tool 76 | 77 | .PHONY: test 78 | test: ##@Test run tests 79 | go test -v github.com/alcideio/rbac-tool/pkg/... 80 | 81 | create-kind-cluster: ##@Test creatte KIND cluster 82 | kind create cluster --image kindest/node:v1.23.13 --name rbak 83 | 84 | delete-kind-cluster: ##@Test delete KIND cluster 85 | kind delete cluster --name rbak 86 | 87 | # 88 | # How to release: 89 | # 90 | # 1. Grab GITHUB Token of alcidebuilder from 1password 91 | # 2. export GITHUB_TOKEN= 92 | # 3. git tag -a v0.4.0 -m "my new version" 93 | # 4. git push origin v0.4.0 94 | # 5. Go to to https://github.com/alcideio/rbac-tool/releases and publish the release draft 95 | # 96 | # Delete tag: git push origin --delete v0.7.0 97 | # 98 | .PHONY: gorelease 99 | gorelease: ##@build Generate All release artifacts 100 | GOPATH=~ USER=alcidebuilder $(GORELEASER) release -f $(CURDIR)/.goreleaser.yml --clean --release-notes=notes.md 101 | 102 | gorelease-snapshot: ##@build Generate All release artifacts 103 | GOPATH=~ USER=alcidebuilder GORELEASER_CURRENT_TAG=v0.0.0 $(GORELEASER) release -f $(CURDIR)/.goreleaser.yml --clean --skip-publish --snapshot 104 | 105 | HELP_FUN = \ 106 | %help; \ 107 | while(<>) { push @{$$help{$$2 // 'options'}}, [$$1, $$3] if /^(.+)\s*:.*\#\#(?:@(\w+))?\s(.*)$$/ }; \ 108 | print "Usage: make [opti@buildons] [target] ...\n\n"; \ 109 | for (sort keys %help) { \ 110 | print "$$_:\n"; \ 111 | for (sort { $$a->[0] cmp $$b->[0] } @{$$help{$$_}}) { \ 112 | $$sep = " " x (30 - length $$_->[0]); \ 113 | print " $$_->[0]$$sep$$_->[1]\n" ; \ 114 | } print "\n"; } 115 | 116 | krew-template: ##@Krew Generate Krew plugin template 117 | @docker run --rm -v $(CURDIR)/krew.yaml:/krew.yaml rajatjindal/krew-release-bot:v0.0.40 krew-release-bot template --tag $(shell git describe --tags --abbrev=0) --template-file /krew.yaml > krew-test.yaml 118 | 119 | krew-test: krew-template ##@Krew Local test of kubectl krew plugin 120 | @kubectl krew uninstall rbac-tool || true 121 | @echo "Test Mac (amd64)" 122 | KREW_OS=darwin KREW_ARCH=amd64 kubectl krew install --manifest=krew-test.yaml 123 | @kubectl krew uninstall rbac-tool || true 124 | @echo "Test Windows (amd64)" 125 | KREW_OS=windows KREW_ARCH=amd64 kubectl krew install --manifest=krew-test.yaml 126 | @echo "Test Linux (amd64)" 127 | @kubectl krew uninstall rbac-tool || true 128 | KREW_OS=linux KREW_ARCH=amd64 kubectl krew install --manifest=krew-test.yaml 129 | kubectl rbac-tool generate 130 | 131 | 132 | help: ##@Misc Show this help 133 | @perl -e '$(HELP_FUN)' $(MAKEFILE_LIST) 134 | 135 | .DEFAULT_GOAL := help 136 | -------------------------------------------------------------------------------- /cmd/analysis_cmd.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | "sort" 8 | "strings" 9 | 10 | "github.com/alcideio/rbac-tool/pkg/analysis" 11 | "github.com/alcideio/rbac-tool/pkg/kube" 12 | "github.com/alcideio/rbac-tool/pkg/rbac" 13 | "github.com/olekukonko/tablewriter" 14 | "github.com/spf13/cobra" 15 | "sigs.k8s.io/yaml" 16 | ) 17 | 18 | func NewCommandAnalysis() *cobra.Command { 19 | 20 | clusterContext := "" 21 | customConfig := "" 22 | output := "table" 23 | 24 | // Support overrides 25 | cmd := &cobra.Command{ 26 | Use: "analysis", 27 | Aliases: []string{"analyze", "analyze-cluster", "an", "assess"}, 28 | Args: cobra.ExactArgs(0), 29 | SilenceUsage: true, 30 | SilenceErrors: true, 31 | Example: "rbac-tool analyze [--config pkg/analysis/default-rules.yaml]", 32 | Short: "Analyze RBAC permissions and highlight overly permissive principals, risky permissions, etc.", 33 | Long: ` 34 | 35 | Examples: 36 | 37 | # Analyze RBAC permissions of the cluster pointed by current context 38 | rbac-tool analyze 39 | 40 | `, 41 | Hidden: false, 42 | RunE: func(c *cobra.Command, args []string) error { 43 | var err error 44 | 45 | analysisConfig := analysis.DefaultAnalysisConfig() 46 | 47 | //Override Rules (if provided) 48 | if customConfig != "" { 49 | analysisConfig, err = analysis.LoadAnalysisConfig(customConfig) 50 | if err != nil { 51 | return err 52 | } 53 | } 54 | 55 | client, err := kube.NewClient(clusterContext) 56 | if err != nil { 57 | return fmt.Errorf("Failed to create kubernetes client - %v", err) 58 | } 59 | 60 | perms, err := rbac.NewPermissionsFromCluster(client) 61 | if err != nil { 62 | return err 63 | } 64 | 65 | permsPerSubject := rbac.NewSubjectPermissions(perms) 66 | policies := rbac.NewSubjectPermissionsList(permsPerSubject) 67 | 68 | analyzer := analysis.CreateAnalyzer(analysisConfig, policies) 69 | if analyzer == nil { 70 | return fmt.Errorf("Failed to create analyzer") 71 | } 72 | 73 | report, err := analyzer.Analyze() 74 | if err != nil { 75 | return err 76 | } 77 | 78 | switch output { 79 | case "table": 80 | rows := [][]string{} 81 | 82 | for _, f := range report.Findings { 83 | 84 | row := []string{ 85 | f.Subject.Kind, 86 | f.Subject.Name, 87 | f.Subject.Namespace, 88 | f.Finding.RuleName, 89 | strings.ToUpper(f.Finding.Severity), 90 | 91 | f.Finding.Message, 92 | f.Finding.Recommendation, 93 | strings.Join(f.Finding.References, ","), 94 | } 95 | rows = append(rows, row) 96 | } 97 | 98 | sort.Slice(rows, func(i, j int) bool { 99 | if strings.Compare(rows[i][0], rows[j][0]) == 0 { 100 | return (strings.Compare(rows[i][1], rows[j][1]) < 0) 101 | } 102 | 103 | return (strings.Compare(rows[i][0], rows[j][0]) < 0) 104 | }) 105 | 106 | table := tablewriter.NewWriter(os.Stdout) 107 | table.SetHeader([]string{"TYPE", "SUBJECT", "NAMESPACE", "RULE", "SEVERITY", "INFO", "RECOMMENDATION", "REFERENCES"}) 108 | table.SetHeaderAlignment(tablewriter.ALIGN_LEFT) 109 | //table.SetAutoMergeCells(true) 110 | table.SetBorder(false) 111 | table.SetAlignment(tablewriter.ALIGN_LEFT) 112 | //table.SetAutoMergeCells(true) 113 | 114 | table.AppendBulk(rows) 115 | table.Render() 116 | 117 | return nil 118 | case "yaml": 119 | data, err := yaml.Marshal(report) 120 | if err != nil { 121 | return fmt.Errorf("Processing error - %v", err) 122 | } 123 | fmt.Fprintln(os.Stdout, string(data)) 124 | return nil 125 | 126 | case "json": 127 | data, err := json.Marshal(report) 128 | if err != nil { 129 | return fmt.Errorf("Processing error - %v", err) 130 | } 131 | 132 | fmt.Fprintln(os.Stdout, string(data)) 133 | return nil 134 | 135 | default: 136 | return fmt.Errorf("Unsupported output format") 137 | } 138 | }, 139 | } 140 | 141 | flags := cmd.Flags() 142 | flags.StringVarP(&customConfig, "config", "c", "", "Load custom analysis customConfig") 143 | 144 | flags.StringVar(&clusterContext, "cluster-context", "", "Cluster Context .use 'kubectl config get-contexts' to list available contexts") 145 | flags.StringVarP(&output, "output", "o", "yaml", "Output type: table | json | yaml") 146 | 147 | cmd.AddCommand( 148 | NewCommandGenerateAnalysisConfig(), 149 | ) 150 | 151 | return cmd 152 | } 153 | 154 | func NewCommandGenerateAnalysisConfig() *cobra.Command { 155 | return &cobra.Command{ 156 | Use: "generate", 157 | Aliases: []string{"gen"}, 158 | Hidden: true, 159 | Short: "Generate Analysis Config", 160 | RunE: func(cmd *cobra.Command, args []string) error { 161 | c, err := analysis.ExportDefaultConfig("yaml") 162 | if err != nil { 163 | return err 164 | } 165 | 166 | fmt.Println(c) 167 | return nil 168 | }, 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /cmd/auditgen_cmd.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "os" 7 | "regexp" 8 | "strings" 9 | "time" 10 | 11 | auditutil "github.com/alcideio/rbac-tool/pkg/audit" 12 | "github.com/kylelemons/godebug/pretty" 13 | "github.com/spf13/cobra" 14 | "k8s.io/apimachinery/pkg/util/errors" 15 | "k8s.io/apiserver/pkg/apis/audit" 16 | "k8s.io/apiserver/pkg/authorization/authorizer" 17 | "k8s.io/klog" 18 | ) 19 | 20 | func NewCommandAuditGen() *cobra.Command { 21 | options := &AuditGenOpts{ 22 | GeneratedPath: ".", 23 | ExpandMultipleNamespacesToClusterScoped: true, 24 | ExpandMultipleNamesToUnnamed: true, 25 | Annotations: map[string]string{ 26 | "insightcloudsec.rapid7.com/generated-by": "rbac-tool", 27 | "insightcloudsec.rapid7.com/generated": time.Now().Format(time.RFC3339), 28 | }, 29 | } 30 | 31 | cmd := &cobra.Command{ 32 | Use: "auditgen", 33 | Aliases: []string{"audit2rbac", "audit", "audit-gen", "a"}, 34 | Short: "Generate RBAC policy from Kubernetes audit events", 35 | Long: "Generate RBAC policy from Kubernetes audit events", 36 | Example: ` 37 | 38 | # Generate RBAC policies from audit.log 39 | rbac-tool auditgen -f audit.log 40 | 41 | # Generate RBAC policies fromn audit.log 42 | rbac-tool auditgen -f audit.log -ne '^system:' 43 | 44 | # Generate & Visualize 45 | rbac-tool auditgen -f testdata | rbac-tool viz -f - 46 | `, 47 | RunE: func(cmd *cobra.Command, args []string) error { 48 | 49 | if err := options.Complete(); err != nil { 50 | return err 51 | } 52 | 53 | if err := options.Validate(); err != nil { 54 | return err 55 | } 56 | 57 | if err := options.Run(); err != nil { 58 | return err 59 | } 60 | 61 | return nil 62 | }, 63 | } 64 | 65 | flags := cmd.Flags() 66 | 67 | flags.StringArrayVarP(&options.AuditSources, "filename", "f", options.AuditSources, "File, Directory, URL, or - for STDIN to read audit events from") 68 | flags.StringVarP(&options.GeneratedPath, "save", "s", "-", "Save to directory") 69 | flags.StringVarP(&options.OutputFormat, "output", "o", "yaml", "json or yaml") 70 | flags.StringVarP(&options.UserRegexFilter, "user", "u", "", "Specify whether run the lookup using a regex match") 71 | flags.BoolVarP(&options.UserFilterInverse, "not", "n", false, "Inverse the regex matching. Use to search for users that do not match '^system:.*'") 72 | 73 | flags.StringVar(&options.NamespaceRegexFilter, "namespace-filter", ".*", "Namespace regex filter, used to audit events for certain namespaces. By default - all namespaces are evaluated") 74 | 75 | flags.BoolVar(&options.ExpandMultipleNamespacesToClusterScoped, "expand-multi-namespace", options.ExpandMultipleNamespacesToClusterScoped, "Allow identical operations performed in more than one namespace to be performed in any namespace") 76 | flags.BoolVar(&options.ExpandMultipleNamesToUnnamed, "expand-multi-name", options.ExpandMultipleNamesToUnnamed, "Allow identical operations performed on more than one resource name (e.g. 'get pods pod1' and 'get pods pod2') to be allowed on any name") 77 | 78 | return cmd 79 | } 80 | 81 | type AuditGenOpts struct { 82 | // AuditSources is a list of files, URLs or - for STDIN. 83 | // Format must be JSON event.v1alpha1.audit.k8s.io, event.v1beta1.audit.k8s.io, event.v1.audit.k8s.io objects, one per line 84 | AuditSources []string 85 | 86 | // TODO: Updtate from previously generated policies 87 | // ExistingObjectFiles is a list of files or URLs. 88 | // Format must be JSON or YAML RBAC objects or List.v1 objects. 89 | // ExistingRBACObjectSources []string 90 | 91 | UserRegexFilter string 92 | UserFilterInverse bool 93 | 94 | NamespaceRegexFilter string 95 | 96 | // Namespace limits the audit events considered to the specified namespace 97 | Namespace string 98 | 99 | // JSON or YAML 100 | OutputFormat string 101 | 102 | // Directory to write generated roles to. Defaults to current directory. 103 | GeneratedPath string 104 | 105 | // Annotations to apply to generated object names. 106 | Annotations map[string]string 107 | 108 | // If the same operation is performed in multiple namespaces, expand the permission to allow it in any namespace 109 | ExpandMultipleNamespacesToClusterScoped bool 110 | // If the same operation is performed on resources with different names, expand the permission to allow it on any name 111 | ExpandMultipleNamesToUnnamed bool 112 | } 113 | 114 | func (a *AuditGenOpts) Complete() error { 115 | return nil 116 | } 117 | 118 | func (a *AuditGenOpts) Validate() error { 119 | if len(a.AuditSources) == 0 { 120 | return fmt.Errorf("--filename is required") 121 | } 122 | 123 | if len(a.GeneratedPath) == 0 { 124 | return fmt.Errorf("--output is required") 125 | } 126 | 127 | if a.UserRegexFilter != "" { 128 | _, err := regexp.Compile(a.UserRegexFilter) 129 | if err != nil { 130 | return fmt.Errorf("--user must be a valid regex - %v", err) 131 | } 132 | } 133 | 134 | if a.NamespaceRegexFilter != "" { 135 | _, err := regexp.Compile(a.NamespaceRegexFilter) 136 | if err != nil { 137 | return fmt.Errorf("--namespace-filter must be a valid regex - %v", err) 138 | } 139 | } 140 | 141 | return nil 142 | } 143 | 144 | func (a *AuditGenOpts) Run() error { 145 | errs := []error{} 146 | 147 | var userRegex *regexp.Regexp 148 | var nsRegex *regexp.Regexp 149 | var err error 150 | 151 | if a.UserRegexFilter != "" { 152 | userRegex, err = regexp.Compile(a.UserRegexFilter) 153 | } else { 154 | userRegex, err = regexp.Compile(fmt.Sprintf(`.*`)) 155 | } 156 | 157 | if err != nil { 158 | return err 159 | } 160 | 161 | if a.NamespaceRegexFilter != "" { 162 | nsRegex, err = regexp.Compile(a.NamespaceRegexFilter) 163 | } else { 164 | nsRegex, err = regexp.Compile(fmt.Sprintf(`.*`)) 165 | } 166 | 167 | if err != nil { 168 | return err 169 | } 170 | 171 | results, err := auditutil.ReadAuditEvents(a.AuditSources, 172 | func(event *audit.Event) bool { 173 | return auditutil.FilterEvent(event, userRegex, a.UserFilterInverse, nsRegex) 174 | }, 175 | ) 176 | 177 | if err != nil { 178 | errs = []error{err} 179 | } 180 | 181 | attributesByUser := map[string][]authorizer.AttributesRecord{} 182 | for result := range results { 183 | 184 | if result.Err != nil { 185 | errs = append(errs, result.Err) 186 | klog.V(7).Infof("skipping %v", result.Err) 187 | continue 188 | } 189 | 190 | auditEvent := result.Obj.(*audit.Event) 191 | klog.V(7).Infof("[%v]processing [eventId=%v]", auditEvent.User.Username, auditEvent.AuditID) 192 | 193 | attrs := auditutil.EventToAttributes(auditEvent) 194 | 195 | attributes, exist := attributesByUser[attrs.User.GetName()] 196 | if !exist { 197 | attributes = []authorizer.AttributesRecord{} 198 | } 199 | 200 | attributes = append(attributes, attrs) 201 | attributesByUser[attrs.User.GetName()] = attributes 202 | } 203 | 204 | if len(attributesByUser) == 0 { 205 | message := fmt.Sprintf("No audit events matched user %s", a.UserRegexFilter) 206 | return fmt.Errorf(message) 207 | } 208 | 209 | klog.V(7).Infof("processing %v users", len(attributesByUser)) 210 | 211 | for username, attributes := range attributesByUser { 212 | klog.V(7).Infof("[%v] processing %+v", username, pretty.Sprint(username)) 213 | 214 | opts := auditutil.DefaultGenerateOptions() 215 | opts.Annotations = a.Annotations 216 | opts.Name = fmt.Sprintf("insightcloudsec:%v", sanitizeName(username)) 217 | opts.ExpandMultipleNamespacesToClusterScoped = a.ExpandMultipleNamespacesToClusterScoped 218 | opts.ExpandMultipleNamesToUnnamed = a.ExpandMultipleNamesToUnnamed 219 | 220 | generated := auditutil.NewGenerator(auditutil.GetDiscoveryRoles(), attributes, opts).Generate() 221 | 222 | f := bufio.NewWriter(os.Stdout) 223 | defer f.Flush() 224 | 225 | for _, obj := range generated.Roles { 226 | fmt.Fprintln(f, "\n---\n") 227 | auditutil.Output(f, obj, a.OutputFormat) 228 | } 229 | for _, obj := range generated.ClusterRoles { 230 | fmt.Fprintln(f, "\n---\n") 231 | auditutil.Output(f, obj, a.OutputFormat) 232 | } 233 | for _, obj := range generated.RoleBindings { 234 | fmt.Fprintln(f, "\n---\n") 235 | auditutil.Output(f, obj, a.OutputFormat) 236 | } 237 | for _, obj := range generated.ClusterRoleBindings { 238 | fmt.Fprintln(f, "\n---\n") 239 | auditutil.Output(f, obj, a.OutputFormat) 240 | } 241 | } 242 | 243 | return errors.NewAggregate(errs) 244 | } 245 | 246 | func sanitizeName(s string) string { 247 | return strings.ToLower(string(regexp.MustCompile(`[^a-zA-Z0-9:]`).ReplaceAll([]byte(s), []byte("-")))) 248 | } 249 | -------------------------------------------------------------------------------- /cmd/generate_cmd.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "os" 7 | "strings" 8 | 9 | "github.com/spf13/cobra" 10 | 11 | rbacv1 "k8s.io/api/rbac/v1" 12 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 13 | "k8s.io/apimachinery/pkg/runtime" 14 | "k8s.io/apimachinery/pkg/runtime/schema" 15 | k8sJson "k8s.io/apimachinery/pkg/runtime/serializer/json" 16 | "k8s.io/apimachinery/pkg/util/errors" 17 | "k8s.io/apimachinery/pkg/util/sets" 18 | 19 | "github.com/alcideio/rbac-tool/pkg/kube" 20 | ) 21 | 22 | func NewCommandGenerateClusterRole() *cobra.Command { 23 | clusterContext := "" 24 | generateKind := "" 25 | allowedGroups := []string{} 26 | //expandGroups := []string{} 27 | allowedVerb := []string{} 28 | denyResources := []string{} 29 | metadataFlag := &MetadataFlag{metadata: metav1.ObjectMeta{Name: ""}} 30 | 31 | // Support overrides 32 | cmd := &cobra.Command{ 33 | Use: "generate", 34 | Aliases: []string{"gen"}, 35 | Short: "Generate Role or ClusterRole and reduce the use of wildcards", 36 | Long: ` 37 | Generate Role or ClusterRole resource while reducing the use of wildcards. 38 | 39 | rbac-tool read from the Kubernetes discovery API the available API Groups and resources, 40 | and based on the command line options, generate an explicit Role/ClusterRole that avoid wildcards 41 | 42 | Examples: 43 | 44 | # Generate a Role with read-only (get,list) excluding secrets (core group) and ingresses (extensions group) 45 | rbac-tool gen --generated-type=Role --deny-resources=secrets.,ingresses.extensions --allowed-verbs=get,list 46 | 47 | # Generate a Role with read-only (get,list) excluding secrets (core group) from core group, admissionregistration.k8s.io,storage.k8s.io,networking.k8s.io 48 | rbac-tool gen --generated-type=ClusterRole --deny-resources=secrets., --allowed-verbs=get,list --allowed-groups=,admissionregistration.k8s.io,storage.k8s.io,networking.k8s.io 49 | 50 | # Generate a Role and customize the metadata of the generated object 51 | rbac-tool gen --generated-type=Role --deny-resources=secrets.,ingresses.extensions --allowed-verbs=get,list --metadata='{"name": "my-role", "namespace":"my-namespace", "labels": {"app": "myapp"}, "annotations": {"generated-by": "rbac-tool"}}' 52 | 53 | `, 54 | Hidden: false, 55 | RunE: func(c *cobra.Command, args []string) error { 56 | kubeClient, err := kube.NewClient(clusterContext) 57 | if err != nil { 58 | return fmt.Errorf("Failed to create kubernetes client - %v", err) 59 | } 60 | 61 | computedPolicyRules, err := generateRules(generateKind, kubeClient.ServerPreferredResources, sets.NewString(denyResources...), sets.NewString(allowedGroups...), sets.NewString(allowedVerb...)) 62 | if err != nil { 63 | return err 64 | } 65 | 66 | obj, err := generateRole(generateKind, computedPolicyRules, &metadataFlag.metadata) 67 | if err != nil { 68 | return err 69 | } 70 | 71 | fmt.Fprintln(os.Stdout, obj) 72 | 73 | return nil 74 | }, 75 | } 76 | 77 | flags := cmd.Flags() 78 | 79 | flags.StringVarP(&generateKind, "generated-type", "t", "ClusterRole", "Role or ClusterRole") 80 | flags.StringVarP(&clusterContext, "cluster-context", "c", "", "Cluster.use 'kubectl config get-contexts' to list available contexts") 81 | //flags.StringSliceVarP(&expandGroups, "expand-groups", "g", []string{""}, "Comma separated list of API groups we would like to list all resource kinds rather than using wild cards '*'") 82 | flags.StringSliceVar(&allowedGroups, "allowed-groups", []string{"*"}, "Comma separated list of API groups we would like to allow '*'") 83 | flags.StringSliceVar(&allowedVerb, "allowed-verbs", []string{"*"}, "Comma separated list of verbs to include. To include all use '*'") 84 | flags.StringSliceVar(&denyResources, "deny-resources", []string{""}, "Comma separated list of resource.group - for example secret. to deny secret (core group) access") 85 | flags.Var(metadataFlag, "metadata", "Kubernetes object metadata as JSON") 86 | 87 | return cmd 88 | } 89 | 90 | func generateRole(generateKind string, rules []rbacv1.PolicyRule, metadata *metav1.ObjectMeta) (string, error) { 91 | var obj runtime.Object 92 | md := *metadata 93 | 94 | if generateKind == "ClusterRole" { 95 | if md.Name == "" { 96 | md.Name = "custom-cluster-role" 97 | } 98 | md.Namespace = "" 99 | 100 | obj = &rbacv1.ClusterRole{ 101 | TypeMeta: metav1.TypeMeta{ 102 | Kind: "ClusterRole", 103 | APIVersion: "rbac.authorization.k8s.io/v1", 104 | }, 105 | ObjectMeta: md, 106 | Rules: rules, 107 | } 108 | } else { 109 | if md.Name == "" { 110 | md.Name = "custom-role" 111 | } 112 | if md.Namespace == "" { 113 | md.Namespace = "mynamespace" 114 | } 115 | 116 | obj = &rbacv1.Role{ 117 | TypeMeta: metav1.TypeMeta{ 118 | Kind: "Role", 119 | APIVersion: "rbac.authorization.k8s.io/v1", 120 | }, 121 | ObjectMeta: md, 122 | Rules: rules, 123 | } 124 | 125 | } 126 | 127 | serializer := k8sJson.NewSerializerWithOptions(k8sJson.DefaultMetaFactory, nil, nil, k8sJson.SerializerOptions{Yaml: true, Pretty: true, Strict: true}) 128 | var writer = bytes.NewBufferString("") 129 | err := serializer.Encode(obj, writer) 130 | if err != nil { 131 | return "", err 132 | } 133 | 134 | return writer.String(), nil 135 | } 136 | 137 | func generateRules(generateKind string, apiresourceList []*metav1.APIResourceList, denyResources sets.String, includeGroups sets.String, allowedVerbs sets.String) ([]rbacv1.PolicyRule, error) { 138 | isRole := generateKind == "Role" 139 | errs := []error{} 140 | 141 | computedPolicyRules := make([]rbacv1.PolicyRule, 0) 142 | 143 | //processedGroups := sets.NewString() 144 | 145 | for _, apiGroup := range apiresourceList { 146 | 147 | // rbac rules only look at API group names, not name + version 148 | gv, err := schema.ParseGroupVersion(apiGroup.GroupVersion) 149 | if err != nil { 150 | errs = append(errs, err) 151 | continue 152 | } 153 | 154 | //Skip the API Groups for specific 155 | if !includeGroups.Has(gv.Group) && !includeGroups.Has(rbacv1.APIGroupAll) { 156 | continue 157 | } 158 | 159 | //Skip API Group versions (RBAC ignore API version) 160 | //if processedGroups.Has(gv.Group) { 161 | // continue 162 | //} 163 | 164 | //Skip API Group entirely if *.APIGroup was specified 165 | if denyResources.Has(fmt.Sprintf("*.%v", strings.ToLower(gv.Group))) { 166 | continue 167 | } 168 | 169 | //processedGroups.Insert(gv.Group) 170 | 171 | resourceList := make([]string, 0) 172 | uniqueVerbs := sets.NewString() 173 | 174 | for _, kind := range apiGroup.APIResources { 175 | 176 | if isRole && !kind.Namespaced { 177 | //When generating role - non-namespaced resources are not relevant 178 | continue 179 | } 180 | 181 | if denyResources.Has(fmt.Sprintf("%v.%v", strings.ToLower(kind.Name), strings.ToLower(gv.Group))) { 182 | continue 183 | } 184 | 185 | resourceList = append(resourceList, kind.Name) 186 | 187 | if allowedVerbs.Has(rbacv1.VerbAll) { 188 | uniqueVerbs.Insert(rbacv1.VerbAll) 189 | continue 190 | } 191 | 192 | for _, verb := range kind.Verbs { 193 | if allowedVerbs.Has(verb) || allowedVerbs.Has(rbacv1.VerbAll) { 194 | uniqueVerbs.Insert(strings.ToLower(verb)) 195 | } 196 | } 197 | } 198 | 199 | var newPolicyRule *rbacv1.PolicyRule 200 | 201 | if len(resourceList) == 0 || uniqueVerbs.Len() == 0 { 202 | continue 203 | } 204 | 205 | //if len(apiGroup.APIResources) == len(resourceList) { 206 | // resourceList = []string{"*"} 207 | //} 208 | 209 | newPolicyRule = &rbacv1.PolicyRule{ 210 | APIGroups: []string{gv.Group}, 211 | Verbs: uniqueVerbs.List(), 212 | Resources: resourceList, 213 | } 214 | 215 | computedPolicyRules = append(computedPolicyRules, *newPolicyRule) 216 | } 217 | 218 | return computedPolicyRules, errors.NewAggregate(errs) 219 | } 220 | -------------------------------------------------------------------------------- /cmd/lookup_cmd.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "regexp" 7 | "sort" 8 | "strings" 9 | 10 | "github.com/alcideio/rbac-tool/pkg/kube" 11 | "github.com/alcideio/rbac-tool/pkg/rbac" 12 | "github.com/olekukonko/tablewriter" 13 | "github.com/spf13/cobra" 14 | ) 15 | 16 | func NewCommandLookup() *cobra.Command { 17 | 18 | clusterContext := "" 19 | regex := "" 20 | inverse := false 21 | 22 | // Support overrides 23 | cmd := &cobra.Command{ 24 | Use: "lookup", 25 | Aliases: []string{"look"}, 26 | Short: "RBAC Lookup by subject (user/group/serviceaccount) name", 27 | Long: ` 28 | A Kubernetes RBAC lookup of Roles/ClusterRoles used by a given User/ServiceAccount/Group 29 | 30 | Examples: 31 | 32 | # Search All Service Accounts 33 | rbac-tool lookup 34 | 35 | # Search Service Accounts that match myname exactly 36 | rbac-tool lookup myname 37 | 38 | # Search All Service Accounts that contain myname 39 | rbac-tool lookup -e '.*myname.*' 40 | 41 | # Lookup System Accounts (all accounts that start with system: ) 42 | rbac-tool lookup -e '^system:.*' 43 | 44 | # Lookup all accounts that DO NOT start with system: ) 45 | rbac-tool lookup -ne '^system:.*' 46 | 47 | `, 48 | Hidden: false, 49 | RunE: func(c *cobra.Command, args []string) error { 50 | var re *regexp.Regexp 51 | var err error 52 | 53 | if regex == "" { 54 | if len(args) == 1 { 55 | // exact match 56 | re, err = regexp.Compile(fmt.Sprintf(`^%v$`, args[0])) 57 | } else { 58 | // search all service accounts 59 | re, err = regexp.Compile(fmt.Sprintf(`.*`)) 60 | } 61 | } else { 62 | // regex match 63 | re, err = regexp.Compile(regex) 64 | } 65 | 66 | if err != nil { 67 | return err 68 | } 69 | 70 | client, err := kube.NewClient(clusterContext) 71 | if err != nil { 72 | return fmt.Errorf("Failed to create kubernetes client - %v", err) 73 | } 74 | 75 | perms, err := rbac.NewPermissionsFromCluster(client) 76 | if err != nil { 77 | return err 78 | } 79 | 80 | table := tablewriter.NewWriter(os.Stdout) 81 | table.SetHeader([]string{"SUBJECT", "SUBJECT TYPE", "SCOPE", "NAMESPACE", "ROLE", "BINDING"}) 82 | table.SetHeaderAlignment(tablewriter.ALIGN_LEFT) 83 | table.SetBorder(false) 84 | table.SetAlignment(tablewriter.ALIGN_LEFT) 85 | 86 | rows := [][]string{} 87 | for _, bindings := range perms.RoleBindings { 88 | for _, binding := range bindings { 89 | for _, subject := range binding.Subjects { 90 | match := re.MatchString(subject.Name) 91 | 92 | // match inverse 93 | // ----------------- 94 | // true true --> skip 95 | // true false --> keep 96 | // false true --> keep 97 | // false false --> skip 98 | if match { 99 | if inverse { 100 | continue 101 | } 102 | } else { 103 | if !inverse { 104 | continue 105 | } 106 | } 107 | 108 | //Subject match 109 | roleNamespace := binding.Namespace 110 | if binding.RoleRef.Kind == "ClusterRole" { 111 | roleNamespace = "" 112 | } 113 | _, exist := perms.Roles[roleNamespace] 114 | if !exist { 115 | continue 116 | } 117 | 118 | if binding.Namespace == "" { 119 | row := []string{subject.Name, subject.Kind, "ClusterRole", "", binding.RoleRef.Name, binding.Name} 120 | rows = append(rows, row) 121 | } else if binding.Namespace != "" && roleNamespace == "" { 122 | row := []string{subject.Name, subject.Kind, "ClusterRole", binding.Namespace, binding.RoleRef.Name, binding.Name} 123 | rows = append(rows, row) 124 | } else { 125 | row := []string{subject.Name, subject.Kind, "Role", binding.Namespace, binding.RoleRef.Name, binding.Name} 126 | rows = append(rows, row) 127 | } 128 | } 129 | } 130 | } 131 | 132 | sort.Slice(rows, func(i, j int) bool { 133 | if strings.Compare(rows[i][0], rows[j][0]) == 0 { 134 | return (strings.Compare(rows[i][3], rows[j][3]) < 0) 135 | } 136 | 137 | return (strings.Compare(rows[i][0], rows[j][0]) < 0) 138 | }) 139 | 140 | table.AppendBulk(rows) 141 | table.Render() 142 | 143 | return nil 144 | }, 145 | } 146 | 147 | flags := cmd.Flags() 148 | flags.StringVar(&clusterContext, "cluster-context", "", "Cluster Context .use 'kubectl config get-contexts' to list available contexts") 149 | 150 | flags.StringVarP(®ex, "regex", "e", "", "Specify whether run the lookup using a regex match") 151 | flags.BoolVarP(&inverse, "not", "n", false, "Inverse the regex matching. Use to search for users that do not match '^system:.*'") 152 | return cmd 153 | } 154 | -------------------------------------------------------------------------------- /cmd/metadata_arg.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 7 | ) 8 | 9 | type MetadataFlag struct { 10 | metadata metav1.ObjectMeta 11 | } 12 | 13 | func (f *MetadataFlag) String() string { 14 | b, err := json.Marshal(f.metadata) 15 | if err != nil { 16 | return "failed to marshal metadata object" 17 | } 18 | return string(b) 19 | } 20 | 21 | func (f *MetadataFlag) Set(v string) error { 22 | f.metadata = metav1.ObjectMeta{} 23 | return json.Unmarshal([]byte(v), &f.metadata) 24 | } 25 | 26 | func (f *MetadataFlag) Type() string { 27 | return "json" 28 | } 29 | -------------------------------------------------------------------------------- /cmd/policyrules_cmd.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "os" 8 | "regexp" 9 | "sort" 10 | "strings" 11 | 12 | "sigs.k8s.io/yaml" 13 | 14 | "github.com/alcideio/rbac-tool/pkg/kube" 15 | "github.com/alcideio/rbac-tool/pkg/rbac" 16 | "github.com/olekukonko/tablewriter" 17 | "github.com/spf13/cobra" 18 | v1 "k8s.io/api/rbac/v1" 19 | "k8s.io/apimachinery/pkg/util/sets" 20 | ) 21 | 22 | func NewCommandPolicyRules() *cobra.Command { 23 | 24 | clusterContext := "" 25 | regex := "" 26 | inverse := false 27 | output := "table" 28 | // Support overrides 29 | cmd := &cobra.Command{ 30 | Use: "policy-rules", 31 | Aliases: []string{"rules", "rule", "policy", "pr"}, 32 | Short: "RBAC List Policy Rules For subject (user/group/serviceaccount) name", 33 | Long: ` 34 | List Kubernetes RBAC policy rules for a given User/ServiceAccount/Group 35 | 36 | Examples: 37 | 38 | # Search All Service Accounts 39 | rbac-tool policy-rules -e '.*' 40 | 41 | # Search All Service Accounts that contain myname 42 | rbac-tool policy-rules -e '.*myname.*' 43 | 44 | # Lookup System Accounts (all accounts that start with system: ) 45 | rbac-tool policy-rules -e '^system:.*' 46 | 47 | # Lookup all accounts that DO NOT start with system: ) 48 | rbac-tool policy-rules -ne '^system:.*' 49 | 50 | # Leveraging jmespath for further filtering and implementing who-can 51 | rbac-tool policy-rules -o json | jp "[? @.allowedTo[? (verb=='get' || verb=='*') && (apiGroup=='core' || apiGroup=='*') && (resource=='secrets' || resource == '*') ]].{name: name, namespace: namespace, kind: kind}" 52 | 53 | `, 54 | Hidden: false, 55 | RunE: func(c *cobra.Command, args []string) error { 56 | var re *regexp.Regexp 57 | var err error 58 | 59 | if regex != "" { 60 | re, err = regexp.Compile(regex) 61 | } else { 62 | if len(args) != 1 { 63 | re, err = regexp.Compile(fmt.Sprintf(`.*`)) 64 | } else { 65 | re, err = regexp.Compile(fmt.Sprintf(`(?mi)%v`, args[0])) 66 | } 67 | } 68 | 69 | if err != nil { 70 | return err 71 | } 72 | 73 | client, err := kube.NewClient(clusterContext) 74 | if err != nil { 75 | return fmt.Errorf("Failed to create kubernetes client - %v", err) 76 | } 77 | 78 | perms, err := rbac.NewPermissionsFromCluster(client) 79 | if err != nil { 80 | return err 81 | } 82 | 83 | policies := rbac.NewSubjectPermissions(perms) 84 | filteredPolicies := []rbac.SubjectPermissions{} 85 | for _, policy := range policies { 86 | match := re.MatchString(policy.Subject.Name) 87 | 88 | // match inverse 89 | // ----------------- 90 | // true true --> skip 91 | // true false --> keep 92 | // false true --> keep 93 | // false false --> skip 94 | if match { 95 | if inverse { 96 | continue 97 | } 98 | } else { 99 | if !inverse { 100 | continue 101 | } 102 | } 103 | 104 | filteredPolicies = append(filteredPolicies, policy) 105 | } 106 | 107 | switch output { 108 | case "table": 109 | rows := [][]string{} 110 | 111 | policies := rbac.NewSubjectPermissionsList(filteredPolicies) 112 | 113 | for _, p := range policies { 114 | 115 | var subject string 116 | if p.Subject.Kind == "ServiceAccount" { 117 | subject = fmt.Sprintf("%v/%v", p.Subject.Namespace, p.Subject.Name) 118 | } else { 119 | subject = p.Subject.Name 120 | } 121 | 122 | for _, allowedTo := range p.AllowedTo { 123 | row := []string{ 124 | p.Kind, 125 | subject, 126 | allowedTo.Verb, 127 | allowedTo.Namespace, 128 | allowedTo.APIGroup, 129 | allowedTo.Resource, 130 | strings.Join(allowedTo.ResourceNames, ","), 131 | strings.Join(allowedTo.NonResourceURLs, ","), 132 | renderOriginatedFromColumn(allowedTo.Namespace, allowedTo.OriginatedFrom), 133 | } 134 | rows = append(rows, row) 135 | } 136 | } 137 | 138 | sort.Slice(rows, func(i, j int) bool { 139 | 140 | for c := range [6]int{} { 141 | if strings.Compare(rows[i][c], rows[j][c]) == 0 { 142 | continue 143 | } 144 | return (strings.Compare(rows[i][c], rows[j][c]) < 0) 145 | } 146 | 147 | return true 148 | }) 149 | 150 | table := tablewriter.NewWriter(os.Stdout) 151 | table.SetHeader([]string{"TYPE", "SUBJECT", "VERBS", "NAMESPACE", "API GROUP", "KIND", "NAMES", "NonResourceURI", "ORIGINATED FROM"}) 152 | table.SetHeaderAlignment(tablewriter.ALIGN_LEFT) 153 | table.SetBorder(false) 154 | table.SetAlignment(tablewriter.ALIGN_LEFT) 155 | //table.SetAutoMergeCells(true) 156 | 157 | table.AppendBulk(rows) 158 | table.Render() 159 | 160 | return nil 161 | case "yaml": 162 | policies := rbac.NewSubjectPermissionsList(filteredPolicies) 163 | data, err := yaml.Marshal(&policies) 164 | if err != nil { 165 | return fmt.Errorf("Processing error - %v", err) 166 | } 167 | fmt.Fprintln(os.Stdout, string(data)) 168 | return nil 169 | 170 | case "json": 171 | policies := rbac.NewSubjectPermissionsList(filteredPolicies) 172 | 173 | data, err := json.Marshal(&policies) 174 | if err != nil { 175 | return fmt.Errorf("Processing error - %v", err) 176 | } 177 | 178 | fmt.Fprintln(os.Stdout, string(data)) 179 | return nil 180 | 181 | default: 182 | return fmt.Errorf("Unsupported output format") 183 | } 184 | }, 185 | } 186 | 187 | flags := cmd.Flags() 188 | flags.StringVar(&clusterContext, "cluster-context", "", "Cluster Context .use 'kubectl config get-contexts' to list available contexts") 189 | flags.StringVarP(&output, "output", "o", "table", "Output type: table | json | yaml") 190 | 191 | flags.StringVarP(®ex, "regex", "e", "", "Specify whether run the lookup using a regex match") 192 | flags.BoolVarP(&inverse, "not", "n", false, "Inverse the regex matching. Use to search for users that do not match '^system:.*'") 193 | return cmd 194 | } 195 | 196 | func renderOriginatedFromColumn(ns string, list []v1.RoleRef) string { 197 | roles := sets.NewString() 198 | clusterRoles := sets.NewString() 199 | s := bytes.NewBufferString("") 200 | 201 | for _, ref := range list { 202 | if ref.Kind == "ClusterRole" { 203 | clusterRoles.Insert(ref.Name) 204 | } else { 205 | roles.Insert(fmt.Sprintf("%v/%v ", ns, ref.Name)) 206 | } 207 | } 208 | 209 | if clusterRoles.Len() > 0 { 210 | s.WriteString(fmt.Sprintf("ClusterRoles>>%v", strings.Join(clusterRoles.List(), ","))) 211 | } 212 | 213 | if roles.Len() > 0 { 214 | s.WriteString(fmt.Sprintf("Roles>>%v", strings.Join(roles.List(), ","))) 215 | } 216 | 217 | return s.String() 218 | } 219 | -------------------------------------------------------------------------------- /cmd/show_permissions_cmd.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | 8 | "github.com/kylelemons/godebug/pretty" 9 | "k8s.io/klog" 10 | 11 | "github.com/spf13/cobra" 12 | 13 | rbacv1 "k8s.io/api/rbac/v1" 14 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 15 | "k8s.io/apimachinery/pkg/runtime/schema" 16 | "k8s.io/apimachinery/pkg/util/errors" 17 | "k8s.io/apimachinery/pkg/util/sets" 18 | 19 | "github.com/alcideio/rbac-tool/pkg/kube" 20 | ) 21 | 22 | func NewCommandGenerateShowPermissions() *cobra.Command { 23 | 24 | clusterContext := "" 25 | generateKind := "ClusterRole" 26 | forGroups := []string{"*"} 27 | withVerb := []string{"*"} 28 | scope := "cluster" 29 | denyVerb := []string{} 30 | denyResource := []string{} 31 | metadataFlag := &MetadataFlag{metadata: metav1.ObjectMeta{Name: ""}} 32 | 33 | // Support overrides 34 | cmd := &cobra.Command{ 35 | Use: "show", 36 | Short: "Generate ClusterRole with all available permissions from the target cluster", 37 | Long: ` 38 | Generate sample ClusterRole with all available permissions from the target cluster. 39 | 40 | rbac-tool read from the Kubernetes discovery API the available API Groups and resources, 41 | and based on the command line options, generate an explicit ClusterRole with available resource permissions. 42 | 43 | Examples: 44 | 45 | # Generate a ClusterRole with all the available permissions for core and apps api groups 46 | rbac-tool show --for-groups=,apps 47 | 48 | # Generate a ClusterRole with all the available permissions for core and apps api groups 49 | rbac-tool show --scope=namespaced --without-verbs=create,update,patch,delete,deletecollection 50 | 51 | 52 | `, 53 | Hidden: false, 54 | RunE: func(c *cobra.Command, args []string) error { 55 | if scope != "all" && scope != "cluster" && scope != "namespaced" { 56 | return fmt.Errorf("--scope must be one of: cluster, namespaced or all") 57 | } 58 | kubeClient, err := kube.NewClient(clusterContext) 59 | if err != nil { 60 | return fmt.Errorf("Failed to create kubernetes client - %v", err) 61 | } 62 | 63 | _, allResources, err := kubeClient.Client.Discovery().ServerGroupsAndResources() 64 | if err != nil { 65 | return fmt.Errorf("failed to read ServerGroupsAndResources - %v", err) 66 | } 67 | 68 | preferredResources, err := kubeClient.Client.Discovery().ServerPreferredResources() 69 | if err != nil { 70 | return fmt.Errorf("failed to read ServerPreferredResources - %v", err) 71 | } 72 | 73 | klog.V(7).Infof(">>>>> preferred Resources \n%v\n>>>>>", pretty.Sprint(preferredResources)) 74 | 75 | preferredApiGroups := sets.NewString() 76 | for _, apiGroup := range preferredResources { 77 | klog.V(5).Infof("Add preferred ApiGroups: [%v]", strings.ToLower(apiGroup.GroupVersion)) 78 | preferredApiGroups.Insert(strings.ToLower(apiGroup.GroupVersion)) 79 | } 80 | 81 | klog.V(7).Infof(">>>>> All Resources \n%v\n>>>>>", pretty.Sprint(allResources)) 82 | 83 | computedPolicyRules, err := generateRulesWithSubResources(allResources, scope, preferredApiGroups, sets.NewString(denyResource...), sets.NewString(forGroups...), sets.NewString(withVerb...), sets.NewString(denyVerb...)) 84 | if err != nil { 85 | return err 86 | } 87 | 88 | if scope == "namespaced" { 89 | generateKind = "Role" 90 | } 91 | obj, err := generateRole(generateKind, computedPolicyRules, &metadataFlag.metadata) 92 | if err != nil { 93 | return err 94 | } 95 | 96 | fmt.Fprintln(os.Stdout, obj) 97 | 98 | return nil 99 | }, 100 | } 101 | 102 | flags := cmd.Flags() 103 | 104 | flags.StringVarP(&clusterContext, "cluster-context", "c", "", "Cluster.use 'kubectl config get-contexts' to list available contexts") 105 | flags.StringVarP(&scope, "scope", "", "all", "Filter by resource scope. Valid values are: 'cluster' | 'namespaced' | 'all' ") 106 | flags.StringSliceVar(&forGroups, "for-groups", []string{"*"}, "Comma separated list of API groups we would like to show the permissions") 107 | flags.StringSliceVar(&withVerb, "with-verbs", []string{"*"}, "Comma separated list of verbs to include. To include all use '*'") 108 | flags.StringSliceVar(&denyVerb, "without-verbs", []string{""}, "Comma separated list of verbs to exclude.") 109 | flags.StringSliceVar(&denyResource, "without-resources", []string{""}, "Comma separated list of resources to exclude. Syntax: .") 110 | flags.Var(metadataFlag, "metadata", "Kubernetes object metadata as JSON") 111 | 112 | return cmd 113 | } 114 | 115 | func generateRulesWithSubResources(apiresourceList []*metav1.APIResourceList, scope string, preferredApiGroups sets.String, denyResources sets.String, includeGroups sets.String, allowedVerbs sets.String, deniedVerbs sets.String) ([]rbacv1.PolicyRule, error) { 116 | errs := []error{} 117 | 118 | computedPolicyRules := make([]rbacv1.PolicyRule, 0) 119 | 120 | processedResources := sets.NewString() 121 | 122 | for _, apiGroup := range apiresourceList { 123 | 124 | if !preferredApiGroups.Has(strings.ToLower(apiGroup.GroupVersion)) { 125 | klog.V(5).Infof("Skip ApiGroups: [%v]", strings.ToLower(apiGroup.GroupVersion)) 126 | continue 127 | } 128 | 129 | // rbac rules only look at API group names, not name + version 130 | gv, err := schema.ParseGroupVersion(apiGroup.GroupVersion) 131 | if err != nil { 132 | errs = append(errs, err) 133 | continue 134 | } 135 | 136 | //Skip the API Groups for specific 137 | if !includeGroups.Has(gv.Group) && !includeGroups.Has(rbacv1.APIGroupAll) { 138 | continue 139 | } 140 | 141 | //Skip API Group entirely if *.APIGroup was specified 142 | if denyResources.Has(fmt.Sprintf("*.%v", strings.ToLower(gv.Group))) { 143 | continue 144 | } 145 | 146 | //processedGroups.Insert(gv.Group) 147 | 148 | for _, kind := range apiGroup.APIResources { 149 | 150 | if denyResources.Has(fmt.Sprintf("%v.%v", strings.ToLower(kind.Name), strings.ToLower(gv.Group))) { 151 | continue 152 | } 153 | 154 | apiResouceGVK := schema.GroupVersionResource{Group: gv.Group, Version: kind.Version, Resource: kind.Name} 155 | 156 | if scope == "cluster" && kind.Namespaced { 157 | klog.V(5).Infof("Exclude namespaced resources: [%v]", apiResouceGVK.String()) 158 | continue 159 | } 160 | 161 | if scope == "namespaced" && !kind.Namespaced { 162 | klog.V(5).Infof("Exclude cluster scoped resources: [%v]", apiResouceGVK.String()) 163 | continue 164 | } 165 | 166 | //Skip API Group versions (RBAC ignore API version) 167 | if processedResources.Has(apiResouceGVK.String()) { 168 | klog.V(5).Infof("Skp ApiGroups: [%v]", apiResouceGVK.String()) 169 | continue 170 | } 171 | 172 | klog.V(5).Infof("Add ApiGroups: [%v]", apiResouceGVK.String()) 173 | processedResources.Insert(apiResouceGVK.String()) 174 | 175 | var newPolicyRule *rbacv1.PolicyRule 176 | var uniqueVerbs sets.String 177 | 178 | uniqueVerbs = sets.NewString() 179 | for _, verb := range kind.Verbs { 180 | if allowedVerbs.Has(verb) || allowedVerbs.Has(rbacv1.VerbAll) { 181 | if !deniedVerbs.Has(verb) { 182 | uniqueVerbs.Insert(verb) 183 | } 184 | } 185 | } 186 | 187 | if uniqueVerbs.Len() > 0 { 188 | newPolicyRule = &rbacv1.PolicyRule{ 189 | APIGroups: []string{gv.Group}, 190 | Verbs: uniqueVerbs.List(), 191 | Resources: []string{kind.Name}, 192 | } 193 | 194 | computedPolicyRules = append(computedPolicyRules, *newPolicyRule) 195 | } 196 | 197 | } 198 | } 199 | 200 | return computedPolicyRules, errors.NewAggregate(errs) 201 | } 202 | -------------------------------------------------------------------------------- /cmd/version.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "github.com/spf13/cobra" 6 | "os" 7 | ) 8 | 9 | var ( 10 | Version = "" 11 | Commit = "" 12 | ) 13 | 14 | func NewCommandVersion() *cobra.Command { 15 | return &cobra.Command{ 16 | Use: "version", 17 | Short: "Print rbac-tool version", 18 | Run: func(cmd *cobra.Command, args []string) { 19 | fmt.Fprintln(os.Stdout, "Version: "+Version+"\nCommit: "+Commit) 20 | }, 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /cmd/visualize_cmd.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "github.com/alcideio/rbac-tool/pkg/utils" 6 | "github.com/alcideio/rbac-tool/pkg/visualize" 7 | "github.com/fatih/color" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | func NewCommandVisualize() *cobra.Command { 12 | 13 | opts := &visualize.Opts{} 14 | 15 | // Support overrides 16 | cmd := &cobra.Command{ 17 | Use: "visualize", 18 | Aliases: []string{"vis", "viz"}, 19 | Short: "A RBAC visualizer", 20 | Long: ` 21 | A Kubernetes RBAC visualizer - Generate a graph as dot file format. 22 | 23 | By default 'rbac-tool viz' will connect to the local cluster (pointed by kubeconfig) 24 | Create a RBAC graph of the actively running workload on all namespaces except kube-system 25 | 26 | See run options on how to render specific namespaces, other clusters, etc. 27 | 28 | #Render Locally 29 | rbac-tool viz --outformat dot && cat rbac.dot | dot -Tpng > rbac.png && open rbac.png 30 | 31 | # Render Online 32 | https://dreampuf.github.io/GraphvizOnline 33 | 34 | Examples: 35 | 36 | # Generate RBAC Graph of a cluster pointed by the kubeconfig context 'myctx' 37 | rbac-tool viz --cluster-context myctx 38 | 39 | # Generate RBAC Graph of a cluster and create a PNG image from the graph 40 | rbac-tool viz --outformat dot --exclude-namespaces=soemns && cat rbac.dot | dot -Tpng > rbac.png && google-chrome rbac.png 41 | 42 | # Generate RBAC Graph from the output of kubectl 43 | kubectl get roles,rolebindings,clusterroles,clusterrolebindings,serviceaccounts -A -o yaml | rbac-tool viz --file - 44 | 45 | # Generate RBAC Graph for permissions used by cluster pods 46 | rbac-tool viz --include-pods-only 47 | 48 | `, 49 | Hidden: false, 50 | RunE: func(c *cobra.Command, args []string) error { 51 | 52 | if opts.Outfile == "rbac.html" && opts.Outformat == "dot" { 53 | opts.Outfile = "rbac.dot" 54 | } 55 | 56 | if err := opts.Validate(); err != nil { 57 | return err 58 | } 59 | 60 | utils.ConsolePrinter(fmt.Sprintf("Namespaces included %v", color.GreenString("'%v'", opts.IncludedNamespaces))) 61 | 62 | if len(opts.ExcludedNamespaces) > 0 { 63 | utils.ConsolePrinter(fmt.Sprintf("Namespaces excluded %v", color.HiRedString("'%v'", opts.ExcludedNamespaces))) 64 | } 65 | 66 | return visualize.CreateRBACGraph(opts) 67 | }, 68 | } 69 | 70 | flags := cmd.Flags() 71 | 72 | flags.StringVar(&opts.ClusterContext, "cluster-context", "", "Cluster Context .use 'kubectl config get-contexts' to list available contexts") 73 | flags.StringVarP(&opts.Infile, "file", "f", "", "Input File - use '-' to read from stdin") 74 | 75 | flags.StringVar(&opts.Outfile, "outfile", "rbac.html", "Output file") 76 | flags.StringVar(&opts.Outformat, "outformat", "html", "Output format: dot or html") 77 | flags.StringVar(&opts.IncludedNamespaces, "include-namespaces", "*", "Comma-delimited list of namespaces to include in the visualization") 78 | flags.StringVar(&opts.IncludeSubjectsRegex, "include-subjects", ".*", "A regular expression to limit the subjects we visualize") 79 | flags.StringVar(&opts.ExcludedNamespaces, "exclude-namespaces", "kube-system", "Comma-delimited list of namespaces to exclude from the visualization") 80 | 81 | flags.BoolVar(&opts.ShowPodsOnly, "include-pods-only", false, "Show the graph only for service accounts used by Pods") 82 | 83 | flags.BoolVar(&opts.ShowLegend, "show-legend", false, "Whether to show the legend or not (for dot format)") 84 | flags.BoolVar(&opts.ShowRules, "show-rules", true, "Whether to render RBAC access rules (e.g. \"get pods\") or not") 85 | flags.BoolVar(&opts.ShowPSP, "show-psp", false, "Show Pod Security Policies") 86 | return cmd 87 | } 88 | -------------------------------------------------------------------------------- /cmd/whoami_cmd.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/alcideio/rbac-tool/pkg/kube" 8 | "github.com/alcideio/rbac-tool/pkg/rbac" 9 | "github.com/alcideio/rbac-tool/pkg/whoami" 10 | "github.com/kylelemons/godebug/pretty" 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | type whoAmI struct { 15 | Verb string 16 | APIGroup string 17 | Kind string 18 | Name string 19 | NonResourceUrl string 20 | 21 | Rules []rbac.SubjectPolicyList 22 | } 23 | 24 | func NewCommandWhoAmI() *cobra.Command { 25 | clusterContext := "" 26 | 27 | cmd := &cobra.Command{ 28 | Use: "whoami", 29 | Aliases: []string{"who-am-i"}, 30 | Example: "rbac-tool whoami", 31 | Short: "Shows the subject for the current context with which one authenticates with the cluster", 32 | Long: `Shows the subject for the current context with which one authenticates with the cluster`, 33 | Hidden: false, 34 | RunE: func(c *cobra.Command, args []string) error { 35 | var err error 36 | 37 | kubeClient, err := kube.NewClient(clusterContext) 38 | if err != nil { 39 | return fmt.Errorf("Failed to create kubernetes client - %v", err) 40 | } 41 | 42 | userInfo, err := whoami.ExtractUserInfo(kubeClient) 43 | if err != nil { 44 | return err 45 | } 46 | 47 | fmt.Fprintln(os.Stdout, pretty.Sprint(userInfo)) 48 | 49 | return err 50 | }, 51 | } 52 | 53 | flags := cmd.Flags() 54 | flags.StringVarP(&clusterContext, "cluster-context", "c", "", "Cluster Context .use 'kubectl config get-contexts' to list available contexts") 55 | 56 | return cmd 57 | } 58 | -------------------------------------------------------------------------------- /cmd/whocan_cmd.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | "sort" 8 | "strings" 9 | 10 | "github.com/alcideio/rbac-tool/pkg/kube" 11 | "github.com/alcideio/rbac-tool/pkg/rbac" 12 | 13 | "github.com/antonmedv/expr" 14 | "github.com/olekukonko/tablewriter" 15 | "github.com/spf13/cobra" 16 | "k8s.io/klog" 17 | "sigs.k8s.io/yaml" 18 | ) 19 | 20 | type whoCanQuery struct { 21 | Verb string 22 | APIGroup string 23 | Kind string 24 | Name string 25 | NonResourceUrl string 26 | 27 | Rules []rbac.SubjectPolicyList 28 | } 29 | 30 | func NewCommandWhoCan() *cobra.Command { 31 | 32 | clusterContext := "" 33 | 34 | output := "table" 35 | // Support overrides 36 | cmd := &cobra.Command{ 37 | Use: "who-can", 38 | Aliases: []string{"who", "whocan"}, 39 | Args: cobra.ExactArgs(2), 40 | SilenceUsage: true, 41 | SilenceErrors: true, 42 | Example: "rbac-tool who-can delete deployments.apps", 43 | Short: "Shows which subjects have RBAC permissions to perform an action", 44 | Long: ` 45 | Shows which subjects have RBAC permissions to perform an action denoted by VERB on an object denoted as ( KIND | KIND/NAME | NON-RESOURCE-URL) 46 | 47 | * VERB is a logical Kubernetes API verb like 'get', 'list', 'watch', 'delete', etc. 48 | * KIND is a Kubernetes resource kind. Shortcuts and API groups will be resolved, e.g. 'po' or 'deploy'. 49 | * NAME is the name of a particular Kubernetes resource. 50 | * NON-RESOURCE-URL is a URL that starts with "/". 51 | 52 | Shows which subjects have RBAC permissions to ( KIND> | KIND/NAME | NON-RESOURCE-URL) 53 | 54 | Examples: 55 | 56 | # Who can read ConfigMap resources 57 | rbac-tool who-can get cm 58 | 59 | # Who can watch Deployments 60 | rbac-tool who-can watch deployments.apps 61 | 62 | # Who can read the Kubernetes API endpoint /apis 63 | rbac-tool who-can get /apis 64 | 65 | # Who can read a secret resource by the name some-secret 66 | rbac-tool who-can get secret/some-secret 67 | 68 | `, 69 | Hidden: false, 70 | RunE: func(c *cobra.Command, args []string) error { 71 | var err error 72 | 73 | kind := "" 74 | 75 | queryEnv := whoCanQuery{ 76 | Verb: args[0], 77 | APIGroup: "core", 78 | Kind: "*", 79 | Name: "*", 80 | NonResourceUrl: "", 81 | Rules: nil, 82 | } 83 | 84 | if len(args) == 2 { 85 | kind = args[1] 86 | } 87 | 88 | query := ` 89 | filter( 90 | Rules, 91 | {any( 92 | .AllowedTo, 93 | { .Verb in [Verb, "*"] and 94 | .Resource in [Kind, "*"] and 95 | .APIGroup in [APIGroup, "*"] and 96 | (Name == "*" or len(.ResourceNames) == 0 or Name in .ResourceNames) 97 | } 98 | )} 99 | )` 100 | 101 | if strings.HasPrefix(kind, "/") { 102 | queryEnv.NonResourceUrl = kind 103 | query = ` 104 | filter( 105 | Rules, 106 | {any( 107 | .AllowedTo, 108 | { .Verb in [Verb, "*"] and 109 | (NonResourceUrl in .NonResourceURLs or (len(.NonResourceURLs) == 1 and .NonResourceURLs[0] == "*")) 110 | } 111 | )} 112 | )` 113 | } else if strings.Contains(kind, "/") { 114 | parts := strings.Split(kind, "/") 115 | 116 | queryEnv.Kind = parts[0] 117 | queryEnv.Name = parts[1] 118 | } else { 119 | queryEnv.Kind = kind 120 | } 121 | 122 | client, err := kube.NewClient(clusterContext) 123 | if err != nil { 124 | return fmt.Errorf("Failed to create kubernetes client - %v", err) 125 | } 126 | 127 | if queryEnv.NonResourceUrl == "" { 128 | gr, err := client.Resolve(queryEnv.Verb, queryEnv.Kind, "") 129 | if err != nil { 130 | return err 131 | } 132 | 133 | queryEnv.Kind = gr.Resource 134 | if gr.Group != "" { 135 | queryEnv.APIGroup = gr.Group 136 | } 137 | } 138 | 139 | klog.V(8).Infof("query\n%v\n%#v\n", query, queryEnv) 140 | 141 | program, err := expr.Compile(query) 142 | if err != nil { 143 | return err 144 | } 145 | 146 | perms, err := rbac.NewPermissionsFromCluster(client) 147 | if err != nil { 148 | return err 149 | } 150 | 151 | permsPerSubject := rbac.NewSubjectPermissions(perms) 152 | policies := rbac.NewSubjectPermissionsList(permsPerSubject) 153 | 154 | queryEnv.Rules = policies 155 | 156 | out, err := expr.Run(program, queryEnv) 157 | if err != nil { 158 | return fmt.Errorf("Failed to run program - %v", err) 159 | } 160 | 161 | filteredPolicies := out.([]interface{}) 162 | 163 | switch output { 164 | case "table": 165 | rows := [][]string{} 166 | 167 | for _, e := range filteredPolicies { 168 | p := e.(rbac.SubjectPolicyList) 169 | row := []string{ 170 | p.Kind, 171 | p.Name, 172 | p.Namespace, 173 | } 174 | rows = append(rows, row) 175 | } 176 | 177 | sort.Slice(rows, func(i, j int) bool { 178 | if strings.Compare(rows[i][0], rows[j][0]) == 0 { 179 | return (strings.Compare(rows[i][1], rows[j][1]) < 0) 180 | } 181 | 182 | return (strings.Compare(rows[i][0], rows[j][0]) < 0) 183 | }) 184 | 185 | table := tablewriter.NewWriter(os.Stdout) 186 | table.SetHeader([]string{"TYPE", "SUBJECT", "NAMESPACE"}) 187 | table.SetHeaderAlignment(tablewriter.ALIGN_LEFT) 188 | table.SetBorder(false) 189 | table.SetAlignment(tablewriter.ALIGN_LEFT) 190 | //table.SetAutoMergeCells(true) 191 | 192 | table.AppendBulk(rows) 193 | table.Render() 194 | 195 | return nil 196 | case "yaml": 197 | data, err := yaml.Marshal(&filteredPolicies) 198 | if err != nil { 199 | return fmt.Errorf("Processing error - %v", err) 200 | } 201 | fmt.Fprintln(os.Stdout, string(data)) 202 | return nil 203 | 204 | case "json": 205 | data, err := json.Marshal(&filteredPolicies) 206 | if err != nil { 207 | return fmt.Errorf("Processing error - %v", err) 208 | } 209 | 210 | fmt.Fprintln(os.Stdout, string(data)) 211 | return nil 212 | 213 | default: 214 | return fmt.Errorf("Unsupported output format") 215 | } 216 | }, 217 | } 218 | 219 | flags := cmd.Flags() 220 | flags.StringVar(&clusterContext, "cluster-context", "", "Cluster Context .use 'kubectl config get-contexts' to list available contexts") 221 | flags.StringVarP(&output, "output", "o", "table", "Output type: table | json | yaml") 222 | 223 | return cmd 224 | } 225 | -------------------------------------------------------------------------------- /download.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -x 2 | set -e 3 | # Code generated by godownloader on 2021-07-19T11:26:48Z. DO NOT EDIT. 4 | # 5 | 6 | usage() { 7 | this=$1 8 | cat </dev/null 118 | } 119 | echoerr() { 120 | echo "$@" 1>&2 121 | } 122 | log_prefix() { 123 | echo "$0" 124 | } 125 | _logp=6 126 | log_set_priority() { 127 | _logp="$1" 128 | } 129 | log_priority() { 130 | if test -z "$1"; then 131 | echo "$_logp" 132 | return 133 | fi 134 | [ "$1" -le "$_logp" ] 135 | } 136 | log_tag() { 137 | case $1 in 138 | 0) echo "emerg" ;; 139 | 1) echo "alert" ;; 140 | 2) echo "crit" ;; 141 | 3) echo "err" ;; 142 | 4) echo "warning" ;; 143 | 5) echo "notice" ;; 144 | 6) echo "info" ;; 145 | 7) echo "debug" ;; 146 | *) echo "$1" ;; 147 | esac 148 | } 149 | log_debug() { 150 | log_priority 7 || return 0 151 | echoerr "$(log_prefix)" "$(log_tag 7)" "$@" 152 | } 153 | log_info() { 154 | log_priority 6 || return 0 155 | echoerr "$(log_prefix)" "$(log_tag 6)" "$@" 156 | } 157 | log_err() { 158 | log_priority 3 || return 0 159 | echoerr "$(log_prefix)" "$(log_tag 3)" "$@" 160 | } 161 | log_crit() { 162 | log_priority 2 || return 0 163 | echoerr "$(log_prefix)" "$(log_tag 2)" "$@" 164 | } 165 | uname_os() { 166 | os=$(uname -s | tr '[:upper:]' '[:lower:]') 167 | case "$os" in 168 | cygwin_nt*) os="windows" ;; 169 | mingw*) os="windows" ;; 170 | msys_nt*) os="windows" ;; 171 | esac 172 | echo "$os" 173 | } 174 | uname_arch() { 175 | arch=$(uname -m) 176 | case $arch in 177 | x86_64) arch="amd64" ;; 178 | x86) arch="386" ;; 179 | i686) arch="386" ;; 180 | i386) arch="386" ;; 181 | aarch64) arch="arm64" ;; 182 | armv5*) arch="armv5" ;; 183 | armv6*) arch="armv6" ;; 184 | armv7*) arch="armv7" ;; 185 | esac 186 | echo ${arch} 187 | } 188 | uname_os_check() { 189 | os=$(uname_os) 190 | case "$os" in 191 | darwin) return 0 ;; 192 | dragonfly) return 0 ;; 193 | freebsd) return 0 ;; 194 | linux) return 0 ;; 195 | android) return 0 ;; 196 | nacl) return 0 ;; 197 | netbsd) return 0 ;; 198 | openbsd) return 0 ;; 199 | plan9) return 0 ;; 200 | solaris) return 0 ;; 201 | windows) return 0 ;; 202 | esac 203 | log_crit "uname_os_check '$(uname -s)' got converted to '$os' which is not a GOOS value. Please file bug at https://github.com/client9/shlib" 204 | return 1 205 | } 206 | uname_arch_check() { 207 | arch=$(uname_arch) 208 | case "$arch" in 209 | 386) return 0 ;; 210 | amd64) return 0 ;; 211 | arm64) return 0 ;; 212 | armv5) return 0 ;; 213 | armv6) return 0 ;; 214 | armv7) return 0 ;; 215 | ppc64) return 0 ;; 216 | ppc64le) return 0 ;; 217 | mips) return 0 ;; 218 | mipsle) return 0 ;; 219 | mips64) return 0 ;; 220 | mips64le) return 0 ;; 221 | s390x) return 0 ;; 222 | amd64p32) return 0 ;; 223 | esac 224 | log_crit "uname_arch_check '$(uname -m)' got converted to '$arch' which is not a GOARCH value. Please file bug report at https://github.com/client9/shlib" 225 | return 1 226 | } 227 | untar() { 228 | tarball=$1 229 | case "${tarball}" in 230 | *.tar.gz | *.tgz) tar --no-same-owner -xzf "${tarball}" ;; 231 | *.tar) tar --no-same-owner -xf "${tarball}" ;; 232 | *.zip) unzip "${tarball}" ;; 233 | *) 234 | log_err "untar unknown archive format for ${tarball}" 235 | return 1 236 | ;; 237 | esac 238 | } 239 | http_download_curl() { 240 | local_file=$1 241 | source_url=$2 242 | header=$3 243 | if [ -z "$header" ]; then 244 | code=$(curl -w '%{http_code}' -sL -o "$local_file" "$source_url") 245 | else 246 | code=$(curl -w '%{http_code}' -sL -H "$header" -o "$local_file" "$source_url") 247 | fi 248 | if [ "$code" != "200" ]; then 249 | log_debug "http_download_curl received HTTP status $code" 250 | return 1 251 | fi 252 | return 0 253 | } 254 | http_download_wget() { 255 | local_file=$1 256 | source_url=$2 257 | header=$3 258 | if [ -z "$header" ]; then 259 | wget -q -O "$local_file" "$source_url" 260 | else 261 | wget -q --header "$header" -O "$local_file" "$source_url" 262 | fi 263 | } 264 | http_download() { 265 | log_debug "http_download $2" 266 | if is_command curl; then 267 | http_download_curl "$@" 268 | return 269 | elif is_command wget; then 270 | http_download_wget "$@" 271 | return 272 | fi 273 | log_crit "http_download unable to find wget or curl" 274 | return 1 275 | } 276 | http_copy() { 277 | tmp=$(mktemp) 278 | http_download "${tmp}" "$1" "$2" || return 1 279 | body=$(cat "$tmp") 280 | rm -f "${tmp}" 281 | echo "$body" 282 | } 283 | github_release() { 284 | owner_repo=$1 285 | version=$2 286 | test -z "$version" && version="latest" 287 | giturl="https://github.com/${owner_repo}/releases/${version}" 288 | json=$(http_copy "$giturl" "Accept:application/json") 289 | test -z "$json" && return 1 290 | version=$(echo "$json" | tr -s '\n' ' ' | sed 's/.*"tag_name":"//' | sed 's/".*//') 291 | test -z "$version" && return 1 292 | echo "$version" 293 | } 294 | hash_sha256() { 295 | TARGET=${1:-/dev/stdin} 296 | if is_command gsha256sum; then 297 | hash=$(gsha256sum "$TARGET") || return 1 298 | echo "$hash" | cut -d ' ' -f 1 299 | elif is_command sha256sum; then 300 | hash=$(sha256sum "$TARGET") || return 1 301 | echo "$hash" | cut -d ' ' -f 1 302 | elif is_command shasum; then 303 | hash=$(shasum -a 256 "$TARGET" 2>/dev/null) || return 1 304 | echo "$hash" | cut -d ' ' -f 1 305 | elif is_command openssl; then 306 | hash=$(openssl -dst openssl dgst -sha256 "$TARGET") || return 1 307 | echo "$hash" | cut -d ' ' -f a 308 | else 309 | log_crit "hash_sha256 unable to find command to compute sha-256 hash" 310 | return 1 311 | fi 312 | } 313 | hash_sha256_verify() { 314 | TARGET=$1 315 | checksums=$2 316 | if [ -z "$checksums" ]; then 317 | log_err "hash_sha256_verify checksum file not specified in arg2" 318 | return 1 319 | fi 320 | BASENAME=${TARGET##*/} 321 | want=$(grep "${BASENAME}" "${checksums}" 2>/dev/null | tr '\t' ' ' | cut -d ' ' -f 1) 322 | if [ -z "$want" ]; then 323 | log_err "hash_sha256_verify unable to find checksum for '${TARGET}' in '${checksums}'" 324 | return 1 325 | fi 326 | got=$(hash_sha256 "$TARGET") 327 | if [ "$want" != "$got" ]; then 328 | log_err "hash_sha256_verify checksum for '$TARGET' did not verify ${want} vs $got" 329 | return 1 330 | fi 331 | } 332 | cat /dev/null < k8s.io/api v0.26.15 84 | k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.26.15 85 | k8s.io/apimachinery => k8s.io/apimachinery v0.26.15 86 | k8s.io/apiserver => k8s.io/apiserver v0.26.15 87 | k8s.io/cli-runtime => k8s.io/cli-runtime v0.26.15 88 | k8s.io/client-go => k8s.io/client-go v0.26.15 89 | k8s.io/cloud-provider => k8s.io/cloud-provider v0.26.15 90 | k8s.io/cluster-bootstrap => k8s.io/cluster-bootstrap v0.26.15 91 | k8s.io/code-generator => k8s.io/code-generator v0.26.15 92 | k8s.io/component-base => k8s.io/component-base v0.26.15 93 | k8s.io/component-helpers => k8s.io/component-helpers v0.26.15 94 | k8s.io/controller-manager => k8s.io/controller-manager v0.26.15 95 | k8s.io/cri-api => k8s.io/cri-api v0.26.15 96 | k8s.io/csi-translation-lib => k8s.io/csi-translation-lib v0.26.15 97 | k8s.io/kube-aggregator => k8s.io/kube-aggregator v0.26.15 98 | k8s.io/kube-controller-manager => k8s.io/kube-controller-manager v0.26.15 99 | k8s.io/kube-proxy => k8s.io/kube-proxy v0.26.15 100 | k8s.io/kube-scheduler => k8s.io/kube-scheduler v0.26.15 101 | k8s.io/kubectl => k8s.io/kubectl v0.26.15 102 | k8s.io/kubelet => k8s.io/kubelet v0.26.15 103 | k8s.io/legacy-cloud-providers => k8s.io/legacy-cloud-providers v0.26.15 104 | k8s.io/metrics => k8s.io/metrics v0.26.15 105 | k8s.io/mount-utils => k8s.io/mount-utils v0.26.15 106 | k8s.io/node-api => k8s.io/node-api v0.26.15 107 | k8s.io/sample-apiserver => k8s.io/sample-apiserver v0.26.15 108 | k8s.io/sample-cli-plugin => k8s.io/sample-cli-plugin v0.26.15 109 | k8s.io/sample-controller => k8s.io/sample-controller v0.26.15 110 | ) 111 | -------------------------------------------------------------------------------- /hack/goreleaser-download.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Check if the version and installation directory arguments are provided 4 | if [ $# -ne 2 ]; then 5 | echo "Usage: $0 " 6 | exit 1 7 | fi 8 | 9 | # Extract the version and installation directory arguments 10 | GORELEASER_VERSION="$1" 11 | INSTALL_DIR="$2" 12 | 13 | # Specify the URL of the Goreleaser tar.gz archive for your operating system and architecture 14 | GORELEASER_URL="https://github.com/goreleaser/goreleaser/releases/download/v$GORELEASER_VERSION/goreleaser_$(uname -s)_$(uname -m).tar.gz" 15 | 16 | # Ensure the installation directory exists 17 | mkdir -p "$INSTALL_DIR" 18 | 19 | # Download Goreleaser tar.gz archive and extract it to the specified directory 20 | curl -L "$GORELEASER_URL" | tar xz -C "$INSTALL_DIR" 21 | 22 | # Make the extracted binary executable (assuming it's named "goreleaser") 23 | chmod +x "$INSTALL_DIR/goreleaser" 24 | 25 | # Display a success message 26 | echo "Goreleaser $GORELEASER_VERSION has been downloaded and installed to $INSTALL_DIR" 27 | -------------------------------------------------------------------------------- /hack/goreleaser-postbuild.sh: -------------------------------------------------------------------------------- 1 | #/bin/bash -x 2 | 3 | if [ $2 == "windows" ]; then 4 | echo "Skipping Running UPX on Windows - $1" 5 | exit 0 6 | fi 7 | 8 | if [ $1 == "rbac-tool_darwin_arm64" ]; then 9 | echo "Skipping Running UPX on Darwin arm64 - $1" 10 | exit 0 11 | fi 12 | 13 | if [ $1 == "rbac-tool_darwin_amd64" ]; then 14 | echo "Skipping Running UPX on Darwin amd64 - $1" 15 | exit 0 16 | fi 17 | 18 | echo "Running UPX - $1" 19 | find dist/$1* -type f -executable -exec ./bin/upx {} + 20 | 21 | #echo "Generate release notes footer" 22 | echo '```sh' > dist/notes-footer.md 23 | if [ $1 == "rbac-tool_linux_amd64" ]; then 24 | dist/rbac-tool_linux_amd64/rbac-tool --help >> dist/notes-footer.md 25 | fi 26 | echo '```' >> dist/notes-footer.md -------------------------------------------------------------------------------- /img/rbac-viz-html-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alcideio/rbac-tool/a0b8c036b90b8ed31de9ff1fcef854312f52c418/img/rbac-viz-html-example.png -------------------------------------------------------------------------------- /krew.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: krew.googlecontainertools.github.com/v1alpha2 2 | kind: Plugin 3 | metadata: 4 | name: rbac-tool 5 | spec: 6 | version: {{ .TagName }} 7 | platforms: 8 | - bin: rbac-tool 9 | {{addURIAndSha "https://github.com/alcideio/rbac-tool/releases/download/{{ .TagName }}/rbac-tool_{{ .TagName }}_linux_amd64.tar.gz" .TagName }} 10 | selector: 11 | matchLabels: 12 | os: linux 13 | arch: amd64 14 | 15 | - bin: rbac-tool 16 | {{addURIAndSha "https://github.com/alcideio/rbac-tool/releases/download/{{ .TagName }}/rbac-tool_{{ .TagName }}_linux_arm64.tar.gz" .TagName }} 17 | selector: 18 | matchLabels: 19 | os: linux 20 | arch: arm64 21 | 22 | - bin: rbac-tool 23 | {{addURIAndSha "https://github.com/alcideio/rbac-tool/releases/download/{{ .TagName }}/rbac-tool_{{ .TagName }}_darwin_amd64.tar.gz" .TagName }} 24 | selector: 25 | matchLabels: 26 | os: darwin 27 | arch: amd64 28 | 29 | - bin: rbac-tool 30 | {{addURIAndSha "https://github.com/alcideio/rbac-tool/releases/download/{{ .TagName }}/rbac-tool_{{ .TagName }}_darwin_arm64.tar.gz" .TagName }} 31 | selector: 32 | matchLabels: 33 | os: darwin 34 | arch: arm64 35 | 36 | - bin: rbac-tool.exe 37 | {{addURIAndSha "https://github.com/alcideio/rbac-tool/releases/download/{{ .TagName }}/rbac-tool_{{ .TagName }}_windows_amd64.tar.gz" .TagName }} 38 | selector: 39 | matchLabels: 40 | os: windows 41 | arch: amd64 42 | 43 | shortDescription: Plugin to analyze RBAC permissions and generate policies 44 | homepage: https://github.com/alcideio/rbac-tool 45 | description: | 46 | This plugin is a collection of RBAC tools to simplify analysis and configuration. 47 | You can visualize, analyze, query permissions as well as generate policies in multiple ways. 48 | 49 | Examples: 50 | # Generate HTML visualzation of your RBAC permissions 51 | kubectl rbac-tool viz 52 | 53 | # Query who can read secrets 54 | kubectl rbac-tool who-can get secret 55 | 56 | # Generate a ClusterRole policy that allows to read everything except secrets and services 57 | kubectl rbac-tool gen --deny-resources=secrets.,services. --allowed-verbs=get,list -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | goflag "flag" 6 | "fmt" 7 | "k8s.io/klog" 8 | "os" 9 | 10 | "github.com/alcideio/rbac-tool/cmd" 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | func RbacGenCmd() *cobra.Command { 15 | var rootCmd = &cobra.Command{ 16 | Use: "rbac-tool", 17 | Short: "rbac-tool", 18 | Long: `rbac-tool`, 19 | } 20 | 21 | var genBashCompletionCmd = &cobra.Command{ 22 | Use: "bash-completion", 23 | Short: "Generate bash completion. source <(rbac-tool bash-completion)", 24 | Long: "Generate bash completion. source <(rbac-tool bash-completion)", 25 | Run: func(cmd *cobra.Command, args []string) { 26 | out := new(bytes.Buffer) 27 | _ = rootCmd.GenBashCompletion(out) 28 | fmt.Println(out.String()) 29 | }, 30 | } 31 | 32 | cmds := []*cobra.Command{ 33 | cmd.NewCommandVersion(), 34 | genBashCompletionCmd, 35 | cmd.NewCommandGenerateClusterRole(), 36 | cmd.NewCommandVisualize(), 37 | cmd.NewCommandLookup(), 38 | cmd.NewCommandPolicyRules(), 39 | cmd.NewCommandAuditGen(), 40 | cmd.NewCommandWhoCan(), 41 | cmd.NewCommandAnalysis(), 42 | cmd.NewCommandGenerateShowPermissions(), 43 | cmd.NewCommandWhoAmI(), 44 | } 45 | 46 | flags := rootCmd.PersistentFlags() 47 | 48 | klog.InitFlags(nil) 49 | flags.AddGoFlagSet(goflag.CommandLine) 50 | 51 | // Hide all klog flags except for -v 52 | goflag.CommandLine.VisitAll(func(f *goflag.Flag) { 53 | if f.Name != "v" { 54 | flags.Lookup(f.Name).Hidden = true 55 | } 56 | }) 57 | 58 | rootCmd.AddCommand(cmds...) 59 | 60 | return rootCmd 61 | } 62 | 63 | func main() { 64 | rootCmd := RbacGenCmd() 65 | if err := rootCmd.Execute(); err != nil { 66 | fmt.Println(err) 67 | os.Exit(-1) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /notes.md: -------------------------------------------------------------------------------- 1 | ## insightCloudSec | insightCloudSec | RBAC TOOL 2 | 3 | A collection of Kubernetes RBAC tools to sugar coat Kubernetes RBAC complexity 4 | 5 | ## Install 6 | 7 | #### Standalone 8 | 9 | ```shell script 10 | curl https://raw.githubusercontent.com/alcideio/rbac-tool/master/download.sh | bash 11 | ``` 12 | 13 | #### kubectl plugin // krew // 14 | 15 | ```shell script 16 | $ kubectl krew install rbac-tool 17 | ``` 18 | 19 | ## Command Line Examples (Standalone) 20 | 21 | ```shell script 22 | # Show which users/groups/service accounts are allowed to read secrets in the cluster pointed by kubeconfig 23 | rbac-tool who-can get secrets 24 | 25 | # Show the subject information of the the one authenticates against the current cluster context 26 | rbac-tool whoami 27 | 28 | # Scan the cluster pointed by the kubeconfig context 'myctx' 29 | rbac-tool viz --cluster-context myctx 30 | 31 | # Scan and create a PNG image from the graph 32 | rbac-tool viz --outformat dot --exclude-namespaces=soemns && cat rbac.dot | dot -Tpng > rbac.png && google-chrome rbac.png 33 | # Render Online 34 | https://dreampuf.github.io/GraphvizOnline 35 | 36 | # Analyze cluster RBAC permissions to identify overly permissive roles and principals 37 | rbac-tool analysis -o table 38 | 39 | # Search All Service Accounts That Contains myname 40 | rbac-tool lookup -e '.*myname.*' 41 | 42 | # Lookup all accounts that DO NOT start with system: ) 43 | rbac-tool lookup -ne '^system:.*' 44 | 45 | # List policy rules for users (or all of them) 46 | rbac-tool policy-rules -e '^system:anonymous' 47 | 48 | # Generate from Audit events & Visualize 49 | rbac-tool auditgen -f testdata | rbac-tool viz -f - 50 | 51 | # Generate a `ClusterRole` policy that allows to read everything **except** *secrets* and *services* 52 | rbac-tool gen --deny-resources=secrets.,services. --allowed-verbs=get,list 53 | 54 | # Generate a ClusterRole with all the available permissions for core and apps api groups 55 | rbac-tool show --for-groups=,apps 56 | 57 | ``` 58 | 59 | ## kubectl rbac-tool ... 60 | 61 | ```shell script 62 | # Generate HTML visualzation of your RBAC permissions 63 | kubectl rbac-tool viz 64 | 65 | # Query who can read secrets 66 | kubectl rbac-tool who-can get secret 67 | 68 | # Generate a ClusterRole policy that allows to read everything except secrets and services 69 | kubectl rbac-tool gen --deny-resources=secrets.,services. --allowed-verbs=get,list 70 | 71 | # Analyze cluster RBAC permissions to identify overly permissive roles and principals 72 | kubectl rbac-tool analysis -o table 73 | 74 | # Generate a ClusterRole with all the available permissions for core and apps api groups 75 | kubectl rbac-tool show --for-groups=,apps 76 | 77 | # Show the subject information of the the one authenticates against the current cluster context 78 | kubectl rbac-tool whoami 79 | ``` 80 | -------------------------------------------------------------------------------- /pkg/analysis/default_rules.go: -------------------------------------------------------------------------------- 1 | package analysis 2 | 3 | import ( 4 | _ "embed" 5 | 6 | "sigs.k8s.io/yaml" 7 | ) 8 | 9 | //go:embed default-rules.yaml 10 | var defaultAnalysis []byte 11 | 12 | func DefaultAnalysisConfig() *AnalysisConfig { 13 | c := AnalysisConfig{} 14 | 15 | if err := yaml.Unmarshal(defaultAnalysis, &c); err != nil { 16 | return nil 17 | } 18 | 19 | return &c 20 | } 21 | 22 | func ExportDefaultConfig(format string) (string, error) { 23 | c := DefaultAnalysisConfig() 24 | 25 | return ExportAnalysisConfig(format, c) 26 | } 27 | -------------------------------------------------------------------------------- /pkg/analysis/default_rules_test.go: -------------------------------------------------------------------------------- 1 | package analysis 2 | 3 | import ( 4 | "k8s.io/apimachinery/pkg/util/sets" 5 | "strings" 6 | "testing" 7 | "time" 8 | 9 | "github.com/alcideio/rbac-tool/pkg/rbac" 10 | v1 "k8s.io/api/rbac/v1" 11 | "k8s.io/klog" 12 | ) 13 | 14 | func Test__DefultRulesUUIDs(t *testing.T) { 15 | uids := sets.NewString() 16 | config := DefaultAnalysisConfig() 17 | for i, rule := range config.Rules { 18 | if uids.Has(strings.ToLower(rule.Uuid)) { 19 | t.Fatalf("Rule '%v' - %v - duplicate UUID", i, rule.Name) 20 | t.Fail() 21 | } 22 | uids.Insert(strings.ToLower(rule.Uuid)) 23 | } 24 | } 25 | 26 | func Test__VerifyDefultRules(t *testing.T) { 27 | 28 | config := DefaultAnalysisConfig() 29 | 30 | for i, rule := range config.Rules { 31 | if r, err := newAnalysisRule(&config.Rules[i]); err != nil || r == nil { 32 | t.Fatalf("Rule '%v' - %v - failed to initialize\n%v\n", i, rule.Name, err) 33 | t.Fail() 34 | } 35 | } 36 | } 37 | 38 | func Test__Analyzer(t *testing.T) { 39 | defer klog.Flush() 40 | 41 | config := DefaultAnalysisConfig() 42 | 43 | analyzer := CreateAnalyzer( 44 | config, 45 | []rbac.SubjectPolicyList{ 46 | {Subject: v1.Subject{ 47 | Kind: "ServiceAccount", 48 | APIGroup: "", 49 | Name: "test-sa", 50 | Namespace: "test", 51 | }, AllowedTo: []rbac.NamespacedPolicyRule{ 52 | {Namespace: "test", Verb: "get", APIGroup: "*", Resource: "*", ResourceNames: nil, NonResourceURLs: nil}, 53 | }}, 54 | }, 55 | ) 56 | 57 | if analyzer == nil { 58 | t.Fail() 59 | } 60 | 61 | report, err := analyzer.Analyze() 62 | if err != nil { 63 | t.Fatalf("Analysis failed - %v", err) 64 | t.Fail() 65 | } 66 | 67 | if len(report.Findings) == 0 { 68 | t.Fatalf("Expecting findings") 69 | t.Fail() 70 | } 71 | 72 | //t.Logf("%v", pretty.Sprint(report)) 73 | } 74 | 75 | func Test__GlobalExclusion(t *testing.T) { 76 | defer klog.Flush() 77 | 78 | config := DefaultAnalysisConfig() 79 | 80 | analyzer := CreateAnalyzer( 81 | config, 82 | []rbac.SubjectPolicyList{ 83 | {Subject: v1.Subject{ 84 | Kind: "ServiceAccount", 85 | APIGroup: "", 86 | Name: "test-sa", 87 | Namespace: "kube-system", 88 | }, AllowedTo: []rbac.NamespacedPolicyRule{ 89 | {Namespace: "test", Verb: "get", APIGroup: "*", Resource: "*", ResourceNames: nil, NonResourceURLs: nil}, 90 | }}, 91 | }, 92 | ) 93 | 94 | if analyzer == nil { 95 | t.Fail() 96 | } 97 | 98 | report, err := analyzer.Analyze() 99 | if err != nil { 100 | t.Fatalf("Analysis failed - %v", err) 101 | t.Fail() 102 | } 103 | 104 | if len(report.Findings) != 0 { 105 | t.Fatalf("Expecting no findings") 106 | t.Fail() 107 | } 108 | 109 | //t.Logf("%v", pretty.Sprint(report)) 110 | } 111 | 112 | func Test__RuleExclusion(t *testing.T) { 113 | defer klog.Flush() 114 | 115 | config := DefaultAnalysisConfig() 116 | 117 | config.Rules = config.Rules[0:1] 118 | 119 | config.Rules[0].Exclusions = []Exclusion{ 120 | { 121 | Disabled: false, 122 | Comment: "Exclude test from analysis", 123 | AddedBy: "tester", 124 | LastModified: time.Now().Format(time.RFC3339), 125 | ValidBefore: 0, 126 | Expression: `subject.namespace == "test"`, 127 | }, 128 | } 129 | 130 | analyzer := CreateAnalyzer( 131 | config, 132 | []rbac.SubjectPolicyList{ 133 | {Subject: v1.Subject{ 134 | Kind: "ServiceAccount", 135 | APIGroup: "", 136 | Name: "test-sa", 137 | Namespace: "test", 138 | }, AllowedTo: []rbac.NamespacedPolicyRule{ 139 | {Namespace: "test", Verb: "get", APIGroup: "*", Resource: "*", ResourceNames: nil, NonResourceURLs: nil}, 140 | }}, 141 | }, 142 | ) 143 | 144 | if analyzer == nil { 145 | t.Fail() 146 | } 147 | 148 | report, err := analyzer.Analyze() 149 | if err != nil { 150 | t.Fatalf("Analysis failed - %v", err) 151 | t.Fail() 152 | } 153 | 154 | if len(report.Findings) != 0 { 155 | t.Fatalf("Expecting no findings") 156 | t.Fail() 157 | } 158 | 159 | //t.Logf("%v", pretty.Sprint(report)) 160 | } 161 | 162 | func Test__EmptySubjectPolicyList(t *testing.T) { 163 | defer klog.Flush() 164 | 165 | config := DefaultAnalysisConfig() 166 | 167 | analyzer := CreateAnalyzer( 168 | config, 169 | []rbac.SubjectPolicyList{ 170 | {Subject: v1.Subject{ 171 | Kind: "ServiceAccount", 172 | APIGroup: "", 173 | Name: "test-sa", 174 | Namespace: "test", 175 | }, 176 | AllowedTo: []rbac.NamespacedPolicyRule{}, 177 | }, 178 | }, 179 | ) 180 | 181 | if analyzer == nil { 182 | t.Fail() 183 | } 184 | 185 | report, err := analyzer.Analyze() 186 | if err != nil { 187 | t.Fatalf("Analysis failed - %v", err) 188 | t.Fail() 189 | } 190 | 191 | if len(report.Findings) != 0 { 192 | t.Fatalf("Expecting no findings") 193 | t.Fail() 194 | } 195 | 196 | //t.Logf("%v", pretty.Sprint(report)) 197 | } 198 | -------------------------------------------------------------------------------- /pkg/analysis/report.go: -------------------------------------------------------------------------------- 1 | package analysis 2 | 3 | import v1 "k8s.io/api/rbac/v1" 4 | 5 | type AnalysisReport struct { 6 | //The Analysis Config Info 7 | AnalysisConfigInfo AnalysisConfigInfo 8 | 9 | Stats AnalysisStats 10 | 11 | //Report Create Time 12 | CreatedOn string 13 | 14 | Findings []AnalysisReportFinding 15 | 16 | ExclusionsInfo []ExclusionInfo 17 | } 18 | 19 | type AnalysisStats struct { 20 | //Analysis Rules 21 | RuleCount int 22 | 23 | ExclusionCount int 24 | } 25 | 26 | type AnalysisReportFinding struct { 27 | Subject *v1.Subject 28 | 29 | Finding AnalysisFinding 30 | } 31 | 32 | type AnalysisFinding struct { 33 | // Finding Severity 34 | Severity string 35 | 36 | //Rule Name 37 | Message string 38 | 39 | //Rule Description 40 | Recommendation string 41 | 42 | //The Rule Name that triggered this finding 43 | RuleName string 44 | //The Rule UUID that triggered this finding 45 | RuleUuid string 46 | 47 | //Documetation & additional reading references 48 | References []string 49 | } 50 | 51 | type ExclusionInfo struct { 52 | Subject *v1.Subject 53 | 54 | //Exclusion Message 55 | Message string 56 | } 57 | -------------------------------------------------------------------------------- /pkg/analysis/types.go: -------------------------------------------------------------------------------- 1 | package analysis 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | 8 | "sigs.k8s.io/yaml" 9 | ) 10 | 11 | const ( 12 | SEVERITY_CRIT = "CRITICAL" 13 | SEVERITY_HIGH = "HIGH" 14 | SEVERITY_MED = "MEDIUM" 15 | SEVERITY_INFO = "INFO" 16 | ) 17 | 18 | //Analysis Rule 19 | type Rule struct { 20 | //Rule Name 21 | Name string 22 | //Rule Description 23 | Description string 24 | //Rule Recommendation - rendered as a Google CEL expression to customize the message 25 | Recommendation string 26 | //Rule UUID 27 | Uuid string 28 | //Rule UUID 29 | Severity string 30 | 31 | //Documetation & additional reading references 32 | References []string 33 | 34 | //A Google CEL expression analysis rule. 35 | // Input: []SubjectPolicyList 36 | // Output: Boolean 37 | AnalysisExpr string 38 | 39 | //Any Resources that we should not report about. 40 | // For example do not report on findings from kube-system namespace 41 | Exclusions []Exclusion 42 | 43 | ExclusionCount uint32 44 | } 45 | 46 | type Exclusion struct { 47 | //Is this exclusion turned off 48 | Disabled bool 49 | 50 | //Exclusion note 51 | Comment string 52 | 53 | //Who added this exclusion 54 | AddedBy string 55 | 56 | //When this exclusion had changed - 57 | LastModified string 58 | 59 | //exception active after X, where X is a timestamp of seconds since epoch 60 | ValidBefore uint64 61 | 62 | //A Google CEL expression exceptions 63 | // Input: v1.Subject 64 | // Output: Boolean 65 | Expression string 66 | } 67 | 68 | type Rules []Rule 69 | 70 | type AnalysisConfigInfo struct { 71 | //Config Name 72 | Name string 73 | //Rule Description 74 | Description string 75 | //Rule UUID 76 | Uuid string 77 | } 78 | 79 | type AnalysisConfig struct { 80 | AnalysisConfigInfo 81 | 82 | Rules []Rule 83 | 84 | GlobalExclusions []Exclusion 85 | } 86 | 87 | func ExportAnalysisConfig(format string, c *AnalysisConfig) (string, error) { 88 | switch format { 89 | case "yaml": 90 | 91 | data, err := yaml.Marshal(c) 92 | if err != nil { 93 | return "", fmt.Errorf("Processing error - %v", err) 94 | } 95 | 96 | return string(data), nil 97 | 98 | case "json": 99 | data, err := json.Marshal(c) 100 | if err != nil { 101 | return "", fmt.Errorf("Processing error - %v", err) 102 | } 103 | 104 | return string(data), nil 105 | 106 | default: 107 | return "", fmt.Errorf("Unsupported output format") 108 | } 109 | } 110 | 111 | func LoadAnalysisConfig(fname string) (*AnalysisConfig, error) { 112 | c := &AnalysisConfig{} 113 | 114 | yamlFile, err := ioutil.ReadFile(fname) 115 | if err != nil { 116 | return nil, err 117 | } 118 | err = yaml.Unmarshal(yamlFile, c) 119 | if err != nil { 120 | return nil, err 121 | } 122 | 123 | return c, nil 124 | } 125 | -------------------------------------------------------------------------------- /pkg/audit/event_filter.go: -------------------------------------------------------------------------------- 1 | package audit 2 | 3 | import ( 4 | "regexp" 5 | 6 | "k8s.io/apiserver/pkg/apis/audit" 7 | "k8s.io/klog" 8 | ) 9 | 10 | func FilterEvent(event *audit.Event, userRegex *regexp.Regexp, UserFilterInverse bool, nsRegex *regexp.Regexp) bool { 11 | eventUser := &event.User 12 | if event.ImpersonatedUser != nil { 13 | eventUser = event.ImpersonatedUser 14 | } 15 | 16 | match := userRegex.MatchString(eventUser.Username) 17 | 18 | // match inverse 19 | // ----------------- 20 | // true true --> skip 21 | // true false --> keep 22 | // false true --> keep 23 | // false false --> skip 24 | if match { 25 | if UserFilterInverse { 26 | klog.V(5).Infof("skip %v", eventUser.Username) 27 | return false 28 | } 29 | } else { 30 | if !UserFilterInverse { 31 | klog.V(5).Infof("skip %v", eventUser.Username) 32 | return false 33 | } 34 | } 35 | 36 | if event.ObjectRef != nil && event.ObjectRef.Namespace != "" { 37 | match := nsRegex.MatchString(event.ObjectRef.Namespace) 38 | if !match { 39 | klog.V(5).Infof("skip namespace %v", event.ObjectRef.Namespace) 40 | return false 41 | } 42 | } 43 | 44 | return true 45 | } 46 | -------------------------------------------------------------------------------- /pkg/audit/event_reader.go: -------------------------------------------------------------------------------- 1 | package audit 2 | 3 | import ( 4 | "bufio" 5 | "crypto/tls" 6 | "encoding/json" 7 | "fmt" 8 | "io" 9 | "net/http" 10 | "os" 11 | "path/filepath" 12 | goruntime "runtime" 13 | "strings" 14 | "sync" 15 | 16 | rbacv1 "k8s.io/api/rbac/v1" 17 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 18 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 19 | "k8s.io/apimachinery/pkg/runtime" 20 | "k8s.io/apimachinery/pkg/util/errors" 21 | "k8s.io/apimachinery/pkg/util/yaml" 22 | "k8s.io/apiserver/pkg/apis/audit" 23 | auditv1 "k8s.io/apiserver/pkg/apis/audit/v1" 24 | "k8s.io/klog" 25 | rbacv1helper "k8s.io/kubernetes/pkg/apis/rbac/v1" 26 | ) 27 | 28 | type StreamObject struct { 29 | Obj runtime.Object 30 | Err error 31 | } 32 | 33 | func ReadAuditEvents(sources []string, filters ...func(*audit.Event) bool) (<-chan *StreamObject, error) { 34 | streams, errs := openStreams(sources) 35 | 36 | klog.V(7).Infof("opening '%v' source(s) - %v", len(streams), errs) 37 | 38 | results := readStreams(streams) 39 | results = flattenEventLists(results) 40 | results = normalizeEventType(results, Scheme, Scheme) 41 | results = filterEvents(results, filters...) 42 | 43 | return results, errors.NewAggregate(errs) 44 | } 45 | 46 | func openStreams(sources []string) ([]io.ReadCloser, []error) { 47 | streams := []io.ReadCloser{} 48 | errors := []error{} 49 | 50 | klog.V(5).Infof("opening readStreams(s) - %v", sources) 51 | 52 | //FIXME 53 | client := &http.Client{Transport: &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}} 54 | for _, source := range sources { 55 | if strings.HasPrefix(source, "http://") || strings.HasPrefix(source, "https://") { 56 | req, err := http.NewRequest("GET", source, nil) 57 | if err != nil { 58 | errors = append(errors, err) 59 | continue 60 | } 61 | 62 | req.Header.Set("User-Agent", "insightcloudsec "+goruntime.GOOS+"/"+goruntime.GOARCH) 63 | 64 | resp, err := client.Do(req) 65 | if err != nil { 66 | errors = append(errors, err) 67 | } else if resp.StatusCode != http.StatusOK { 68 | resp.Body.Close() 69 | errors = append(errors, fmt.Errorf("error fetching %s: %d", source, resp.StatusCode)) 70 | } else { 71 | streams = append(streams, resp.Body) 72 | } 73 | 74 | continue 75 | } 76 | 77 | if source == "-" { 78 | streams = append(streams, os.Stdin) 79 | continue 80 | } 81 | 82 | fstat, err := os.Stat(source) 83 | 84 | if err != nil { 85 | klog.V(5).Infof("failed to stat file %v - %v", source, err) 86 | errors = append(errors, err) 87 | continue 88 | } 89 | 90 | if isDir := fstat.IsDir(); !isDir { 91 | f, err := os.Open(source) 92 | if err != nil { 93 | errors = append(errors, err) 94 | } else { 95 | streams = append(streams, f) 96 | } 97 | } 98 | 99 | err = filepath.Walk(source, func(path string, info os.FileInfo, err error) error { 100 | if info.IsDir() { 101 | klog.V(5).Infof("skip directory object %v", path) 102 | return nil 103 | } 104 | 105 | f, err := os.Open(path) 106 | if err != nil { 107 | errors = append(errors, err) 108 | } else { 109 | streams = append(streams, f) 110 | } 111 | return nil 112 | }) 113 | 114 | if err != nil { 115 | klog.V(5).Infof("failed to load from dir %v - %v", source, err) 116 | errors = append(errors, err) 117 | continue 118 | } 119 | 120 | } 121 | 122 | return streams, errors 123 | } 124 | 125 | // decoder can decode streaming json, yaml docs, single json objects, single yaml objects 126 | type decoder interface { 127 | Decode(into interface{}) error 128 | } 129 | 130 | func streamingDecoder(r io.ReadCloser) decoder { 131 | buffer := bufio.NewReaderSize(r, 4096) 132 | b, _ := buffer.Peek(1) 133 | if string(b) == "{" || string(b) == "[" { 134 | return json.NewDecoder(buffer) 135 | } else { 136 | return yaml.NewYAMLToJSONDecoder(buffer) 137 | } 138 | } 139 | 140 | func readStreams(sources []io.ReadCloser) <-chan *StreamObject { 141 | out := make(chan *StreamObject) 142 | 143 | wg := &sync.WaitGroup{} 144 | for i := range sources { 145 | wg.Add(1) 146 | go func(r io.ReadCloser) { 147 | //klog.V(5).Infof("opening readStreams %v", r) 148 | defer wg.Done() 149 | defer r.Close() 150 | d := streamingDecoder(r) 151 | for { 152 | obj := &unstructured.Unstructured{} 153 | err := d.Decode(obj) 154 | 155 | switch { 156 | case err == io.EOF: 157 | return 158 | case err != nil: 159 | klog.V(5).Infof("Fail %v", err) 160 | out <- &StreamObject{Err: err} 161 | default: 162 | //klog.V(7).Infof("Add %v", pretty.Sprint(obj)) 163 | out <- &StreamObject{Obj: obj} 164 | } 165 | } 166 | }(sources[i]) 167 | } 168 | 169 | go func() { 170 | wg.Wait() 171 | close(out) 172 | }() 173 | 174 | return out 175 | } 176 | 177 | func flattenEventLists(in <-chan *StreamObject) <-chan *StreamObject { 178 | out := make(chan *StreamObject) 179 | 180 | v1List := metav1.SchemeGroupVersion.WithKind("List") 181 | v1EventList := auditv1.SchemeGroupVersion.WithKind("EventList") 182 | 183 | go func() { 184 | defer close(out) 185 | for result := range in { 186 | if result.Err != nil { 187 | out <- result 188 | continue 189 | } 190 | 191 | objGvk := result.Obj.GetObjectKind().GroupVersionKind() 192 | 193 | switch objGvk { 194 | case v1List, v1EventList: 195 | data, err := json.Marshal(result.Obj) 196 | if err != nil { 197 | out <- &StreamObject{Err: err} 198 | continue 199 | } 200 | 201 | list := &unstructured.UnstructuredList{} 202 | if err := list.UnmarshalJSON(data); err != nil { 203 | out <- &StreamObject{Err: err} 204 | continue 205 | } 206 | 207 | for i, _ := range list.Items { 208 | //klog.V(7).Infof("Add - %v", pretty.Sprint(list.Items[i])) 209 | out <- &StreamObject{Obj: &list.Items[i]} 210 | } 211 | default: 212 | //klog.V(7).Infof("Not a list - %v", result.Obj.GetObjectKind().GroupVersionKind()) 213 | out <- result 214 | continue 215 | } 216 | } 217 | }() 218 | return out 219 | } 220 | 221 | func normalizeEventType(in <-chan *StreamObject, creator runtime.ObjectCreater, convertor runtime.ObjectConvertor) <-chan *StreamObject { 222 | out := make(chan *StreamObject) 223 | 224 | go func() { 225 | defer close(out) 226 | for result := range in { 227 | if result.Err != nil { 228 | out <- result 229 | continue 230 | } 231 | 232 | //klog.V(7).Infof("[BEFORE] normalizeEventType %v", pretty.Sprint(result.Obj)) 233 | typed, err := creator.New(result.Obj.GetObjectKind().GroupVersionKind()) 234 | if err != nil { 235 | out <- &StreamObject{Err: err} 236 | continue 237 | } 238 | 239 | unstructuredObject, ok := result.Obj.(*unstructured.Unstructured) 240 | if !ok { 241 | out <- &StreamObject{Err: fmt.Errorf("expected *unstructured.Unstructured, got %T", result.Obj)} 242 | } 243 | 244 | if err := runtime.DefaultUnstructuredConverter.FromUnstructured(unstructuredObject.Object, typed); err != nil { 245 | out <- &StreamObject{Err: err} 246 | continue 247 | } 248 | 249 | objGvk := typed.GetObjectKind().GroupVersionKind() 250 | 251 | gv := objGvk.GroupVersion() 252 | if gv.Version == "" || gv.Version == runtime.APIVersionInternal { 253 | out <- &StreamObject{Obj: typed} 254 | continue 255 | } 256 | 257 | gv.Version = runtime.APIVersionInternal 258 | converted, err := convertor.ConvertToVersion(typed, gv) 259 | if err != nil { 260 | out <- &StreamObject{Err: err} 261 | continue 262 | } 263 | 264 | //event := converted.(*audit.Event) 265 | //klog.V(7).Infof("[%v] event converetd by filter [eventId=%v]", event.User.Username, event.AuditID) 266 | out <- &StreamObject{Obj: converted} 267 | } 268 | }() 269 | return out 270 | } 271 | 272 | func filterEvents(in <-chan *StreamObject, filters ...func(*audit.Event) bool) <-chan *StreamObject { 273 | out := make(chan *StreamObject) 274 | 275 | go func() { 276 | defer close(out) 277 | for result := range in { 278 | if result.Err != nil { 279 | out <- result 280 | continue 281 | } 282 | 283 | event, ok := result.Obj.(*audit.Event) 284 | if !ok { 285 | out <- &StreamObject{Err: fmt.Errorf("expected *audit.Event, got %T", result.Obj)} 286 | continue 287 | } 288 | 289 | include := true 290 | for _, filter := range filters { 291 | include = filter(event) 292 | if !include { 293 | break 294 | } 295 | } 296 | 297 | if include { 298 | out <- result 299 | } else { 300 | klog.V(7).Infof("[%v] event dropped by filter [eventId=%v]", event.User.Username, event.AuditID) 301 | } 302 | } 303 | }() 304 | 305 | return out 306 | } 307 | 308 | func GetDiscoveryRoles() RBACObjects { 309 | return RBACObjects{ 310 | ClusterRoles: []*rbacv1.ClusterRole{ 311 | &rbacv1.ClusterRole{ 312 | ObjectMeta: metav1.ObjectMeta{Name: "system:discovery"}, 313 | Rules: []rbacv1.PolicyRule{ 314 | rbacv1helper.NewRule("get").URLs("/healthz", "/version", "/swagger*", "/openapi*", "/api*").RuleOrDie(), 315 | }, 316 | }, 317 | }, 318 | ClusterRoleBindings: []*rbacv1.ClusterRoleBinding{ 319 | &rbacv1.ClusterRoleBinding{ 320 | ObjectMeta: metav1.ObjectMeta{Name: "system:discovery"}, 321 | Subjects: []rbacv1.Subject{ 322 | {Kind: rbacv1.GroupKind, APIGroup: rbacv1.GroupName, Name: "system:authenticated"}, 323 | {Kind: rbacv1.GroupKind, APIGroup: rbacv1.GroupName, Name: "system:unauthenticated"}, 324 | }, 325 | RoleRef: rbacv1.RoleRef{APIGroup: rbacv1.GroupName, Kind: "ClusterRole", Name: "system:discovery"}, 326 | }, 327 | }, 328 | } 329 | } 330 | -------------------------------------------------------------------------------- /pkg/audit/process.go: -------------------------------------------------------------------------------- 1 | package audit 2 | 3 | import ( 4 | "context" 5 | "reflect" 6 | 7 | rbacv1 "k8s.io/api/rbac/v1" 8 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 9 | "k8s.io/apiserver/pkg/authorization/authorizer" 10 | rbacv1helper "k8s.io/kubernetes/pkg/apis/rbac/v1" 11 | "k8s.io/kubernetes/pkg/registry/rbac/validation" 12 | rbacauthorizer "k8s.io/kubernetes/plugin/pkg/auth/authorizer/rbac" 13 | ) 14 | 15 | // RBACObjects holds lists of RBAC API objects 16 | type RBACObjects struct { 17 | Roles []*rbacv1.Role 18 | RoleBindings []*rbacv1.RoleBinding 19 | ClusterRoles []*rbacv1.ClusterRole 20 | ClusterRoleBindings []*rbacv1.ClusterRoleBinding 21 | } 22 | 23 | // GenerateOptions specifies options for generating RBAC roles 24 | type GenerateOptions struct { 25 | VerbExpansions map[string][]string 26 | ExpandMultipleNamesToUnnamed bool 27 | ExpandMultipleNamespacesToClusterScoped bool 28 | 29 | Name string 30 | Annotations map[string]string 31 | } 32 | 33 | // DefaultGenerateOptions returns default generation options 34 | func DefaultGenerateOptions() GenerateOptions { 35 | return GenerateOptions{ 36 | VerbExpansions: map[string][]string{ 37 | "watch": []string{"get", "list"}, 38 | "list": []string{"get", "watch"}, 39 | "update": []string{"get", "patch"}, 40 | "patch": []string{"get", "update"}, 41 | }, 42 | ExpandMultipleNamesToUnnamed: true, 43 | ExpandMultipleNamespacesToClusterScoped: true, 44 | 45 | Name: "auditgen", 46 | Annotations: nil, 47 | } 48 | } 49 | 50 | // Generator allows generating a set of covering RBAC roles and bindings 51 | type Generator struct { 52 | Options GenerateOptions 53 | 54 | existing RBACObjects 55 | requests []authorizer.AttributesRecord 56 | 57 | generated RBACObjects 58 | generatedGetter *validation.StaticRoles 59 | 60 | clusterRole *rbacv1.ClusterRole 61 | clusterRoleBinding *rbacv1.ClusterRoleBinding 62 | namespacedRole map[string]*rbacv1.Role 63 | namespacedRoleBinding map[string]*rbacv1.RoleBinding 64 | } 65 | 66 | // NewGenerator creates a new Generator 67 | func NewGenerator(existing RBACObjects, requests []authorizer.AttributesRecord, options GenerateOptions) *Generator { 68 | _, getter := validation.NewTestRuleResolver(nil, nil, nil, nil) 69 | 70 | return &Generator{ 71 | existing: existing, 72 | requests: requests, 73 | Options: options, 74 | namespacedRole: map[string]*rbacv1.Role{}, 75 | namespacedRoleBinding: map[string]*rbacv1.RoleBinding{}, 76 | generatedGetter: getter, 77 | } 78 | } 79 | 80 | // Generate returns a set of RBAC roles and bindings that cover the specified requests 81 | func (g *Generator) Generate() *RBACObjects { 82 | _, existingGetter := validation.NewTestRuleResolver(g.existing.Roles, g.existing.RoleBindings, g.existing.ClusterRoles, g.existing.ClusterRoleBindings) 83 | existingAuthorizer := rbacauthorizer.New(existingGetter, existingGetter, existingGetter, existingGetter) 84 | 85 | generatedAuthorizer := rbacauthorizer.New(g.generatedGetter, g.generatedGetter, g.generatedGetter, g.generatedGetter) 86 | 87 | // sort requests to put broader ones first 88 | sortRequests(g.requests) 89 | 90 | for _, request := range g.requests { 91 | if decision, _, _ := existingAuthorizer.Authorize(context.Background(), request); decision == authorizer.DecisionAllow { 92 | continue 93 | } 94 | if decision, _, _ := generatedAuthorizer.Authorize(context.Background(), request); decision == authorizer.DecisionAllow { 95 | continue 96 | } 97 | 98 | if !request.ResourceRequest { 99 | clusterRole := g.ensureClusterRoleAndBinding(userToSubject(request.User)) 100 | clusterRole.Rules = append(clusterRole.Rules, rbacv1helper.NewRule(request.Verb).URLs(request.Path).RuleOrDie()) 101 | continue 102 | } 103 | 104 | requestCopy := request 105 | if g.Options.ExpandMultipleNamesToUnnamed { 106 | requestCopy.Name = "" 107 | } 108 | if g.Options.ExpandMultipleNamespacesToClusterScoped { 109 | requestCopy.Namespace = "" 110 | } 111 | requestCopy.Path = "" 112 | 113 | if (request.Namespace != "" && g.Options.ExpandMultipleNamespacesToClusterScoped) || (request.Name != "" && g.Options.ExpandMultipleNamesToUnnamed) { 114 | // search for other requests with the same verb/group/resource/subresource that differ only by name/namespace 115 | for _, a := range g.requests { 116 | differentNamespace := a.Namespace != "" && a.Namespace != request.Namespace 117 | differentName := a.Name != "" && a.Name != request.Name 118 | if !a.ResourceRequest { 119 | continue 120 | } 121 | if g.Options.ExpandMultipleNamesToUnnamed { 122 | a.Name = "" 123 | } 124 | if g.Options.ExpandMultipleNamespacesToClusterScoped { 125 | a.Namespace = "" 126 | } 127 | a.Path = "" 128 | if reflect.DeepEqual(requestCopy, a) { 129 | if g.Options.ExpandMultipleNamespacesToClusterScoped && differentNamespace { 130 | request.Namespace = "" 131 | } 132 | if g.Options.ExpandMultipleNamesToUnnamed && differentName { 133 | request.Name = "" 134 | } 135 | } 136 | } 137 | } 138 | 139 | if request.Namespace == "" { 140 | clusterRole := g.ensureClusterRoleAndBinding(userToSubject(request.User)) 141 | clusterRole.Rules = append(clusterRole.Rules, attributesToResourceRule(request, g.Options)) 142 | } else { 143 | role := g.ensureNamespacedRoleAndBinding(userToSubject(request.User), request.Namespace) 144 | role.Rules = append(role.Rules, attributesToResourceRule(request, g.Options)) 145 | } 146 | } 147 | 148 | // Compact rules 149 | for _, role := range g.generated.ClusterRoles { 150 | role.Rules = compactRules(role.Rules) 151 | } 152 | for _, role := range g.generated.Roles { 153 | role.Rules = compactRules(role.Rules) 154 | } 155 | 156 | return &g.generated 157 | } 158 | 159 | func (g *Generator) ensureClusterRoleAndBinding(subject rbacv1.Subject) *rbacv1.ClusterRole { 160 | if g.clusterRole != nil { 161 | return g.clusterRole 162 | } 163 | 164 | g.clusterRole = &rbacv1.ClusterRole{ 165 | ObjectMeta: metav1.ObjectMeta{Name: g.Options.Name, Annotations: g.Options.Annotations}, 166 | } 167 | g.clusterRoleBinding = &rbacv1.ClusterRoleBinding{ 168 | ObjectMeta: metav1.ObjectMeta{Name: g.Options.Name, Annotations: g.Options.Annotations}, 169 | RoleRef: rbacv1.RoleRef{APIGroup: rbacv1.GroupName, Kind: "ClusterRole", Name: g.clusterRole.Name}, 170 | Subjects: []rbacv1.Subject{subject}, 171 | } 172 | 173 | g.generated.ClusterRoles = append(g.generated.ClusterRoles, g.clusterRole) 174 | g.generated.ClusterRoleBindings = append(g.generated.ClusterRoleBindings, g.clusterRoleBinding) 175 | 176 | _, regeneratedGetter := validation.NewTestRuleResolver(g.generated.Roles, g.generated.RoleBindings, g.generated.ClusterRoles, g.generated.ClusterRoleBindings) 177 | *g.generatedGetter = *regeneratedGetter 178 | 179 | return g.clusterRole 180 | } 181 | 182 | func (g *Generator) ensureNamespacedRoleAndBinding(subject rbacv1.Subject, namespace string) *rbacv1.Role { 183 | if g.namespacedRole[namespace] != nil { 184 | return g.namespacedRole[namespace] 185 | } 186 | 187 | g.namespacedRole[namespace] = &rbacv1.Role{ 188 | ObjectMeta: metav1.ObjectMeta{Name: g.Options.Name, Namespace: namespace, Annotations: g.Options.Annotations}, 189 | } 190 | g.namespacedRoleBinding[namespace] = &rbacv1.RoleBinding{ 191 | ObjectMeta: metav1.ObjectMeta{Name: g.Options.Name, Namespace: namespace, Annotations: g.Options.Annotations}, 192 | RoleRef: rbacv1.RoleRef{APIGroup: rbacv1.GroupName, Kind: "Role", Name: g.namespacedRole[namespace].Name}, 193 | Subjects: []rbacv1.Subject{subject}, 194 | } 195 | 196 | g.generated.Roles = append(g.generated.Roles, g.namespacedRole[namespace]) 197 | g.generated.RoleBindings = append(g.generated.RoleBindings, g.namespacedRoleBinding[namespace]) 198 | 199 | _, regeneratedGetter := validation.NewTestRuleResolver(g.generated.Roles, g.generated.RoleBindings, g.generated.ClusterRoles, g.generated.ClusterRoleBindings) 200 | *g.generatedGetter = *regeneratedGetter 201 | 202 | return g.namespacedRole[namespace] 203 | } 204 | -------------------------------------------------------------------------------- /pkg/audit/util.go: -------------------------------------------------------------------------------- 1 | package audit 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | 7 | "net/url" 8 | "reflect" 9 | "sort" 10 | "strings" 11 | 12 | rbacv1 "k8s.io/api/rbac/v1" 13 | "k8s.io/apimachinery/pkg/runtime" 14 | "k8s.io/apimachinery/pkg/runtime/serializer" 15 | "k8s.io/apimachinery/pkg/runtime/serializer/json" 16 | "k8s.io/apimachinery/pkg/util/sets" 17 | "k8s.io/apiserver/pkg/apis/audit" 18 | auditv1 "k8s.io/apiserver/pkg/apis/audit/v1" 19 | "k8s.io/apiserver/pkg/authentication/serviceaccount" 20 | "k8s.io/apiserver/pkg/authentication/user" 21 | "k8s.io/apiserver/pkg/authorization/authorizer" 22 | validation_helper "k8s.io/component-helpers/auth/rbac/validation" 23 | rbacv1helper "k8s.io/kubernetes/pkg/apis/rbac/v1" 24 | "k8s.io/kubernetes/pkg/registry/rbac/validation" 25 | ) 26 | 27 | func userToSubject(user user.Info) rbacv1.Subject { 28 | if ns, name, err := serviceaccount.SplitUsername(user.GetName()); err == nil { 29 | return rbacv1.Subject{Name: name, Namespace: ns, Kind: "ServiceAccount"} 30 | } 31 | return rbacv1.Subject{Name: user.GetName(), Kind: "User", APIGroup: rbacv1.GroupName} 32 | } 33 | 34 | func attributesToResourceRule(request authorizer.AttributesRecord, options GenerateOptions) rbacv1.PolicyRule { 35 | verbs := append([]string{request.Verb}, options.VerbExpansions[request.Verb]...) 36 | rule := rbacv1helper.NewRule(verbs...).Groups(request.APIGroup).Resources(request.Resource).RuleOrDie() 37 | if request.Subresource != "" { 38 | rule.Resources[0] = rule.Resources[0] + "/" + request.Subresource 39 | } 40 | if request.Name != "" { 41 | rule.ResourceNames = []string{request.Name} 42 | } 43 | return rule 44 | } 45 | 46 | func compactRules(rules []rbacv1.PolicyRule) []rbacv1.PolicyRule { 47 | breakdownRules := []rbacv1.PolicyRule{} 48 | for _, rule := range rules { 49 | breakdownRules = append(breakdownRules, validation_helper.BreakdownRule(rule)...) 50 | } 51 | compactRules, err := validation.CompactRules(breakdownRules) 52 | if err != nil { 53 | return rules 54 | } 55 | // TODO: fix CompactRules to dedupe verbs 56 | for i := range compactRules { 57 | compactRules[i].Verbs = sets.NewString(compactRules[i].Verbs...).List() 58 | } 59 | 60 | accumulatingRules := []rbacv1.PolicyRule{} 61 | for _, rule := range compactRules { 62 | // Non-resource rules just accumulate 63 | if len(rule.Resources) == 0 { 64 | accumulatingRules = append(accumulatingRules, rule) 65 | continue 66 | } 67 | 68 | accumulated := false 69 | // strip resource 70 | resourcelessRule := rule 71 | resourcelessRule.Resources = nil 72 | // strip name 73 | namelessRule := rule 74 | namelessRule.ResourceNames = nil 75 | for j, accumulatingRule := range accumulatingRules { 76 | // strip name 77 | namelessAccumulatingRule := accumulatingRule 78 | namelessAccumulatingRule.ResourceNames = nil 79 | if reflect.DeepEqual(namelessRule, namelessAccumulatingRule) { 80 | combinedNames := sets.NewString(accumulatingRule.ResourceNames...) 81 | combinedNames.Insert(rule.ResourceNames...) 82 | accumulatingRule.ResourceNames = combinedNames.List() 83 | accumulatingRules[j] = accumulatingRule 84 | accumulated = true 85 | break 86 | } 87 | 88 | // strip resource 89 | resourcelessAccumulatingRule := accumulatingRule 90 | resourcelessAccumulatingRule.Resources = nil 91 | if reflect.DeepEqual(resourcelessRule, resourcelessAccumulatingRule) { 92 | combinedResources := sets.NewString(accumulatingRule.Resources...) 93 | combinedResources.Insert(rule.Resources...) 94 | accumulatingRule.Resources = combinedResources.List() 95 | accumulatingRules[j] = accumulatingRule 96 | accumulated = true 97 | break 98 | } 99 | } 100 | if !accumulated { 101 | accumulatingRules = append(accumulatingRules, rule) 102 | } 103 | } 104 | 105 | sort.SliceStable(accumulatingRules, func(i, j int) bool { 106 | // TODO: fix upstream sorting to prioritize API group 107 | if c := strings.Compare(strings.Join(accumulatingRules[i].APIGroups, ","), strings.Join(accumulatingRules[j].APIGroups, ",")); c != 0 { 108 | return c < 0 109 | } 110 | return strings.Compare(rbacv1helper.CompactString(accumulatingRules[i]), rbacv1helper.CompactString(accumulatingRules[j])) < 0 111 | }) 112 | return accumulatingRules 113 | } 114 | 115 | func sortRequests(requests []authorizer.AttributesRecord) { 116 | sort.SliceStable(requests, func(i, j int) bool { 117 | // non-resource < resource 118 | if requests[i].ResourceRequest != requests[j].ResourceRequest { 119 | return !requests[i].ResourceRequest 120 | } 121 | 122 | switch { 123 | case requests[i].ResourceRequest: 124 | // cluster-scoped < namespaced 125 | if n1, n2 := len(requests[i].Namespace) == 0, len(requests[j].Namespace) == 0; n1 != n2 { 126 | return n1 127 | } 128 | 129 | // unnamed < named 130 | if n1, n2 := len(requests[i].Name) == 0, len(requests[j].Name) == 0; n1 != n2 { 131 | return n1 132 | } 133 | 134 | // list < get 135 | if requests[i].Verb == "list" && requests[j].Verb == "get" { 136 | return true 137 | } 138 | if requests[i].Verb == "get" && requests[j].Verb == "list" { 139 | return false 140 | } 141 | 142 | // Sort by group,resource,subresource,namespace,name,verb 143 | if c := strings.Compare(requests[i].APIGroup, requests[j].APIGroup); c != 0 { 144 | return c < 0 145 | } 146 | if c := strings.Compare(requests[i].Resource, requests[j].Resource); c != 0 { 147 | return c < 0 148 | } 149 | if c := strings.Compare(requests[i].Subresource, requests[j].Subresource); c != 0 { 150 | return c < 0 151 | } 152 | if c := strings.Compare(requests[i].Namespace, requests[j].Namespace); c != 0 { 153 | return c < 0 154 | } 155 | if c := strings.Compare(requests[i].Name, requests[j].Name); c != 0 { 156 | return c < 0 157 | } 158 | if c := strings.Compare(requests[i].Verb, requests[j].Verb); c != 0 { 159 | return c < 0 160 | } 161 | 162 | case !requests[i].ResourceRequest: 163 | // Sort by verb,path 164 | if c := strings.Compare(requests[i].Verb, requests[j].Verb); c != 0 { 165 | return c < 0 166 | } 167 | if c := strings.Compare(requests[i].Path, requests[j].Path); c != 0 { 168 | return c < 0 169 | } 170 | } 171 | 172 | return false 173 | }) 174 | } 175 | 176 | var ( 177 | // Scheme knows about audit and rbac types 178 | Scheme = runtime.NewScheme() 179 | // Decoder knows how to decode audit and rbac objects 180 | Decoder runtime.Decoder 181 | ) 182 | 183 | func init() { 184 | if err := rbacv1.AddToScheme(Scheme); err != nil { 185 | panic(err) 186 | } 187 | 188 | if err := auditv1.AddToScheme(Scheme); err != nil { 189 | panic(err) 190 | } 191 | 192 | if err := audit.AddToScheme(Scheme); err != nil { 193 | panic(err) 194 | } 195 | 196 | Decoder = serializer.NewCodecFactory(Scheme).UniversalDecoder() 197 | } 198 | 199 | // Output writes the specified object to the specified writer in "yaml" or "json" format 200 | func Output(w io.Writer, obj runtime.Object, format string) error { 201 | var s *json.Serializer 202 | switch format { 203 | case "json": 204 | s = json.NewSerializerWithOptions(json.DefaultMetaFactory, Scheme, Scheme, json.SerializerOptions{false, true, false}) 205 | case "yaml": 206 | s = json.NewSerializerWithOptions(json.DefaultMetaFactory, Scheme, Scheme, json.SerializerOptions{true, false, false}) 207 | default: 208 | return fmt.Errorf("unknown format: %s", format) 209 | } 210 | 211 | codec := serializer.NewCodecFactory(Scheme).CodecForVersions(s, s, rbacv1.SchemeGroupVersion, rbacv1.SchemeGroupVersion) 212 | 213 | return codec.Encode(obj, w) 214 | } 215 | 216 | func EventToAttributes(event *audit.Event) authorizer.AttributesRecord { 217 | eventUser := &event.User 218 | if event.ImpersonatedUser != nil { 219 | eventUser = event.ImpersonatedUser 220 | } 221 | 222 | path := event.RequestURI 223 | if requestURL, err := url.ParseRequestURI(event.RequestURI); err == nil { 224 | path = requestURL.Path 225 | } 226 | 227 | attrs := authorizer.AttributesRecord{ 228 | Verb: event.Verb, 229 | Path: path, 230 | User: &user.DefaultInfo{ 231 | Name: eventUser.Username, 232 | Groups: eventUser.Groups, 233 | }, 234 | } 235 | 236 | if event.ObjectRef != nil { 237 | attrs.ResourceRequest = true 238 | attrs.Namespace = event.ObjectRef.Namespace 239 | attrs.Name = event.ObjectRef.Name 240 | attrs.Resource = event.ObjectRef.Resource 241 | attrs.Subresource = event.ObjectRef.Subresource 242 | attrs.APIGroup = event.ObjectRef.APIGroup 243 | attrs.APIVersion = event.ObjectRef.APIVersion 244 | } 245 | 246 | return attrs 247 | } 248 | -------------------------------------------------------------------------------- /pkg/kube/client.go: -------------------------------------------------------------------------------- 1 | package kube 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "regexp" 7 | "strings" 8 | 9 | authn "k8s.io/api/authentication/v1" 10 | v1 "k8s.io/api/core/v1" 11 | rbacv1 "k8s.io/api/rbac/v1" 12 | k8sserrs "k8s.io/apimachinery/pkg/api/errors" 13 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 14 | "k8s.io/apimachinery/pkg/runtime/schema" 15 | "k8s.io/apimachinery/pkg/util/errors" 16 | "k8s.io/apimachinery/pkg/util/sets" 17 | "k8s.io/apimachinery/pkg/version" 18 | clientset "k8s.io/client-go/kubernetes" 19 | _ "k8s.io/client-go/plugin/pkg/client/auth" 20 | restclient "k8s.io/client-go/rest" 21 | "k8s.io/client-go/tools/clientcmd" 22 | "k8s.io/klog" 23 | ) 24 | 25 | type KubeClient struct { 26 | Client *clientset.Clientset 27 | 28 | Config *restclient.Config 29 | 30 | // ServerPreferredResources returns the supported resources with the version preferred by the 31 | // server. 32 | ServerPreferredResources []*metav1.APIResourceList 33 | masterVersion *version.Info 34 | } 35 | 36 | func NewClient(context string) (*KubeClient, error) { 37 | loadingRules := clientcmd.NewDefaultClientConfigLoadingRules() 38 | // if you want to change the loading rules (which files in which order), you can do so here 39 | 40 | configOverrides := &clientcmd.ConfigOverrides{ 41 | CurrentContext: context, 42 | } 43 | // if you want to change override values or bind them to flags, there are methods to help you 44 | 45 | var config *restclient.Config 46 | var err error 47 | 48 | kubeConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, configOverrides) 49 | config, err = kubeConfig.ClientConfig() 50 | if err != nil { 51 | return nil, err 52 | } 53 | 54 | client, err := clientset.NewForConfig(config) 55 | if err != nil { 56 | return nil, err 57 | } 58 | 59 | preferedResource, err := client.Discovery().ServerPreferredResources() 60 | if err != nil { 61 | klog.V(3).Infof("ServerPreferredResources completed with errors %v (%v)", err, len(preferedResource)) 62 | } 63 | 64 | if preferedResource == nil { 65 | preferedResource = []*metav1.APIResourceList{} 66 | } 67 | 68 | //klog.V(8).Infof("%v\n", pretty.Sprint(preferedResource)) 69 | 70 | k8sVer, err := client.Discovery().ServerVersion() 71 | if err != nil { 72 | return nil, err 73 | } 74 | 75 | return &KubeClient{ 76 | Client: client, 77 | ServerPreferredResources: preferedResource, 78 | Config: config, 79 | masterVersion: k8sVer, 80 | }, nil 81 | } 82 | 83 | func (kubeClient *KubeClient) GetWorldPermissions() ([]rbacv1.PolicyRule, error) { 84 | errs := []error{} 85 | computedPolicyRules := make([]rbacv1.PolicyRule, 0) 86 | 87 | for _, apiResourceList := range kubeClient.ServerPreferredResources { 88 | // rbac rules only look at API group names, not name & version 89 | gv, err := schema.ParseGroupVersion(apiResourceList.GroupVersion) 90 | if err != nil { 91 | errs = append(errs, err) 92 | continue 93 | } 94 | 95 | resourceList := make([]string, 0) 96 | uniqueVerbs := sets.NewString() 97 | 98 | for _, apiResource := range apiResourceList.APIResources { 99 | resourceList = append(resourceList, apiResource.Name) 100 | for _, verb := range apiResource.Verbs { 101 | uniqueVerbs.Insert(strings.ToLower(verb)) 102 | } 103 | } 104 | 105 | newPolicyRule := &rbacv1.PolicyRule{ 106 | APIGroups: []string{gv.Group}, 107 | Verbs: uniqueVerbs.List(), 108 | Resources: resourceList, 109 | } 110 | 111 | computedPolicyRules = append(computedPolicyRules, *newPolicyRule) 112 | } 113 | 114 | return computedPolicyRules, errors.NewAggregate(errs) 115 | } 116 | 117 | func (kubeClient *KubeClient) GetResourcesAndVerbsForGroup(apiGroup string) (sets.String, sets.String, error) { 118 | errs := []error{} 119 | 120 | resources := sets.NewString() 121 | verbs := sets.NewString() 122 | 123 | for _, apiResourceList := range kubeClient.ServerPreferredResources { 124 | // rbac rules only look at API group names, not name & version 125 | gv, err := schema.ParseGroupVersion(apiResourceList.GroupVersion) 126 | if err != nil { 127 | errs = append(errs, err) 128 | continue 129 | } 130 | 131 | if strings.ToLower(gv.Group) != strings.ToLower(apiGroup) { 132 | continue 133 | } 134 | 135 | for _, apiResource := range apiResourceList.APIResources { 136 | resources.Insert(apiResource.Name) 137 | for _, verb := range apiResource.Verbs { 138 | verbs.Insert(strings.ToLower(verb)) 139 | } 140 | } 141 | } 142 | 143 | return resources, verbs, errors.NewAggregate(errs) 144 | } 145 | 146 | func (kubeClient *KubeClient) GetVerbsForResource(apiGroup string, resource string) (sets.String, error) { 147 | errs := []error{} 148 | 149 | verbs := sets.NewString() 150 | 151 | for _, apiResourceList := range kubeClient.ServerPreferredResources { 152 | // rbac rules only look at API group names, not name & version 153 | gv, err := schema.ParseGroupVersion(apiResourceList.GroupVersion) 154 | if err != nil { 155 | errs = append(errs, err) 156 | continue 157 | } 158 | 159 | if strings.ToLower(gv.Group) != strings.ToLower(apiGroup) { 160 | continue 161 | } 162 | 163 | for _, apiResource := range apiResourceList.APIResources { 164 | if apiResource.Name != resource { 165 | continue 166 | } 167 | 168 | for _, verb := range apiResource.Verbs { 169 | verbs.Insert(strings.ToLower(verb)) 170 | } 171 | } 172 | } 173 | 174 | return verbs, errors.NewAggregate(errs) 175 | } 176 | 177 | func (kubeClient *KubeClient) ListPods(namespace string) ([]v1.Pod, error) { 178 | objs, err := kubeClient.Client.CoreV1().Pods(namespace).List(context.TODO(), metav1.ListOptions{}) 179 | 180 | if err != nil { 181 | return nil, err 182 | } 183 | 184 | return objs.Items, nil 185 | } 186 | 187 | func (kubeClient *KubeClient) ListServiceAccounts(namespace string) ([]v1.ServiceAccount, error) { 188 | objs, err := kubeClient.Client.CoreV1().ServiceAccounts(namespace).List(context.TODO(), metav1.ListOptions{}) 189 | 190 | if err != nil { 191 | return nil, err 192 | } 193 | 194 | return objs.Items, nil 195 | } 196 | 197 | func (kubeClient *KubeClient) ListRoles(namespace string) ([]rbacv1.Role, error) { 198 | objs, err := kubeClient.Client.RbacV1().Roles(namespace).List(context.TODO(), metav1.ListOptions{}) 199 | 200 | if err != nil { 201 | return nil, err 202 | } 203 | 204 | return objs.Items, nil 205 | } 206 | 207 | func (kubeClient *KubeClient) ListRoleBindings(namespace string) ([]rbacv1.RoleBinding, error) { 208 | objs, err := kubeClient.Client.RbacV1().RoleBindings(namespace).List(context.TODO(), metav1.ListOptions{}) 209 | 210 | if err != nil { 211 | return nil, err 212 | } 213 | 214 | return objs.Items, nil 215 | } 216 | 217 | func (kubeClient *KubeClient) ListClusterRoles() ([]rbacv1.ClusterRole, error) { 218 | objs, err := kubeClient.Client.RbacV1().ClusterRoles().List(context.TODO(), metav1.ListOptions{}) 219 | 220 | if err != nil { 221 | return nil, err 222 | } 223 | 224 | return objs.Items, nil 225 | } 226 | 227 | func (kubeClient *KubeClient) ListClusterRoleBindings() ([]rbacv1.ClusterRoleBinding, error) { 228 | objs, err := kubeClient.Client.RbacV1().ClusterRoleBindings().List(context.TODO(), metav1.ListOptions{}) 229 | 230 | if err != nil { 231 | return nil, err 232 | } 233 | 234 | return objs.Items, nil 235 | } 236 | 237 | func (kubeClient *KubeClient) TokenReview(token string) (authn.UserInfo, error) { 238 | tokenReview, err := kubeClient.Client.AuthenticationV1().TokenReviews().Create( 239 | context.Background(), 240 | &authn.TokenReview{ 241 | Spec: authn.TokenReviewSpec{Token: token}, 242 | }, 243 | metav1.CreateOptions{}, 244 | ) 245 | 246 | if err != nil { 247 | if k8sserrs.IsForbidden(err) { 248 | //definitely bad ... but at least give some sense to the user 249 | usernameFromErrorRE := regexp.MustCompile(`^.* User "(.*)" cannot .*$`) 250 | username := usernameFromErrorRE.ReplaceAllString(err.Error(), "$1") 251 | return authn.UserInfo{Username: username}, nil 252 | } 253 | return authn.UserInfo{}, err 254 | } 255 | 256 | if tokenReview.Status.Error != "" { 257 | return authn.UserInfo{}, fmt.Errorf(tokenReview.Status.Error) 258 | } 259 | 260 | return tokenReview.Status.User, nil 261 | } 262 | 263 | func (kubeClient *KubeClient) Resolve(verb, groupresource string, subResource string) (schema.GroupResource, error) { 264 | gr := schema.ParseGroupResource(groupresource) 265 | 266 | klog.V(8).Infof("resolving %v", gr.String()) 267 | 268 | for _, apiResourceList := range kubeClient.ServerPreferredResources { 269 | // rbac rules only look at API group names, not name & version 270 | gv, err := schema.ParseGroupVersion(apiResourceList.GroupVersion) 271 | if err != nil { 272 | klog.V(8).Infof("failed to parse %v - %v", apiResourceList.GroupVersion, err) 273 | continue 274 | } 275 | 276 | if gr.Group != "" && strings.ToLower(gv.Group) != strings.ToLower(gr.Group) { 277 | klog.V(8).Infof("skip - gr=%v,gv=%v", gr.String(), gr.String()) 278 | continue 279 | } 280 | 281 | //We are looking at the correct API Group 282 | //Look at the resource kinds 283 | 284 | for _, apiResource := range apiResourceList.APIResources { 285 | 286 | possibleNames := sets.NewString(apiResource.ShortNames...) 287 | possibleNames.Insert(strings.ToLower(apiResource.Name)) 288 | possibleNames.Insert(strings.ToLower(apiResource.Kind)) 289 | 290 | if !possibleNames.Has(strings.ToLower(gr.Resource)) { 291 | klog.V(8).Infof("skip - gr=%v NOT in [%v]", gr.String(), strings.Join(possibleNames.List(), ",")) 292 | continue 293 | } 294 | 295 | r := schema.GroupResource{ 296 | Group: strings.ToLower(gv.Group), 297 | Resource: strings.ToLower(apiResource.Name), 298 | } 299 | 300 | //Special Verbs 301 | switch verb { 302 | case "bind", "escalate": 303 | //bind: https://kubernetes.io/docs/reference/access-authn-authz/rbac/#restrictions-on-role-binding-creation-or-update 304 | //escalate: https://kubernetes.io/docs/reference/access-authn-authz/rbac/#restrictions-on-role-creation-or-update 305 | if gv.Group == "rbac.authorization.k8s.io" && 306 | (possibleNames.Has("clusterroles") || possibleNames.Has("roles")) { 307 | //We have a match 308 | return r, nil 309 | } 310 | case "impersonate": 311 | if gv.Group == "" && 312 | (possibleNames.Has("users") || possibleNames.Has("groups") || possibleNames.Has("serviceaccounts")) { 313 | //We have a match 314 | return r, nil 315 | } 316 | } 317 | 318 | possibleVerbs := sets.NewString(apiResource.Verbs...) 319 | if !possibleVerbs.Has(strings.ToLower(verb)) { 320 | klog.V(8).Infof("skip - gr=%v '%v' is not in [%v]", gr.String(), verb, strings.Join(possibleVerbs.List(), ",")) 321 | return r, fmt.Errorf("The verb '%s' is not supported by %v", strings.ToLower(verb), r.String()) 322 | } 323 | 324 | //We have a match 325 | return r, nil 326 | } 327 | } 328 | 329 | return schema.GroupResource{}, fmt.Errorf("Failed find a matching API resource") 330 | } 331 | -------------------------------------------------------------------------------- /pkg/rbac/describer.go: -------------------------------------------------------------------------------- 1 | package rbac 2 | 3 | import ( 4 | "fmt" 5 | rbacv1 "k8s.io/api/rbac/v1" 6 | ) 7 | 8 | func DescribeSubject(s *rbacv1.Subject, bindingNamespace string) string { 9 | switch s.Kind { 10 | case rbacv1.ServiceAccountKind: 11 | if len(s.Namespace) > 0 { 12 | return fmt.Sprintf("%s %q", s.Kind, s.Name+"/"+s.Namespace) 13 | } 14 | return fmt.Sprintf("%s %q", s.Kind, s.Name+"/"+bindingNamespace) 15 | default: 16 | return fmt.Sprintf("%s %q", s.Kind, s.Name) 17 | } 18 | } 19 | 20 | type ClusterRoleBindingDescriber struct { 21 | binding *rbacv1.ClusterRoleBinding 22 | subject *rbacv1.Subject 23 | } 24 | 25 | func (d *ClusterRoleBindingDescriber) String() string { 26 | return fmt.Sprintf("ClusterRoleBinding %q of %s %q to %s", 27 | d.binding.Name, 28 | d.binding.RoleRef.Kind, 29 | d.binding.RoleRef.Name, 30 | DescribeSubject(d.subject, ""), 31 | ) 32 | } 33 | 34 | type RoleBindingDescriber struct { 35 | binding *rbacv1.RoleBinding 36 | subject *rbacv1.Subject 37 | } 38 | 39 | func (d *RoleBindingDescriber) String() string { 40 | return fmt.Sprintf("RoleBinding %q of %s %q to %s", 41 | d.binding.Name+"/"+d.binding.Namespace, 42 | d.binding.RoleRef.Kind, 43 | d.binding.RoleRef.Name, 44 | DescribeSubject(d.subject, d.binding.Namespace), 45 | ) 46 | } 47 | -------------------------------------------------------------------------------- /pkg/rbac/permissions.go: -------------------------------------------------------------------------------- 1 | package rbac 2 | 3 | import ( 4 | v1 "k8s.io/api/core/v1" 5 | rbacv1 "k8s.io/api/rbac/v1" 6 | "k8s.io/apimachinery/pkg/runtime" 7 | "k8s.io/klog" 8 | 9 | "github.com/alcideio/rbac-tool/pkg/kube" 10 | ) 11 | 12 | type Permissions struct { 13 | ServiceAccounts map[string]map[string]v1.ServiceAccount 14 | 15 | // Roles & RoleBinding maps captures Cluster & ClusterRoleBinding in namespace "" 16 | // - ClusterRoles are stored in Roles[""] 17 | // - ClusterRoleBindings are stored in RoleBindings[""] 18 | Roles map[string]map[string]rbacv1.Role 19 | RoleBindings map[string]map[string]rbacv1.RoleBinding 20 | } 21 | 22 | func (p *Permissions) populateServiceAccounts(sas []v1.ServiceAccount) { 23 | for _, sa := range sas { 24 | 25 | if p.ServiceAccounts[sa.Namespace] == nil { 26 | p.ServiceAccounts[sa.Namespace] = make(map[string]v1.ServiceAccount) 27 | } 28 | 29 | p.ServiceAccounts[sa.Namespace][sa.Name] = sa 30 | 31 | klog.V(6).Infof("ServiceAccount %v/%v", sa.Namespace, sa.Name) 32 | } 33 | } 34 | 35 | func (p *Permissions) populateRoles(roles []rbacv1.Role) { 36 | for _, role := range roles { 37 | 38 | if p.Roles[role.Namespace] == nil { 39 | p.Roles[role.Namespace] = make(map[string]rbacv1.Role) 40 | } 41 | 42 | p.Roles[role.Namespace][role.Name] = role 43 | klog.V(6).Infof("Role %v/%v", role.Namespace, role.Name) 44 | } 45 | } 46 | 47 | func (p *Permissions) populateClusterRoles(clusterRoles []rbacv1.ClusterRole) { 48 | for _, role := range clusterRoles { 49 | if p.Roles[""] == nil { 50 | p.Roles[""] = make(map[string]rbacv1.Role) 51 | } 52 | 53 | aRole := rbacv1.Role{ 54 | ObjectMeta: role.ObjectMeta, 55 | Rules: role.Rules, 56 | } 57 | 58 | p.Roles[role.Namespace][role.Name] = aRole 59 | klog.V(6).Infof("ClusterRole %v", role.Name) 60 | } 61 | } 62 | 63 | func (p *Permissions) populateRoleBindings(bindings []rbacv1.RoleBinding) { 64 | for _, binding := range bindings { 65 | if p.RoleBindings[binding.Namespace] == nil { 66 | p.RoleBindings[binding.Namespace] = make(map[string]rbacv1.RoleBinding) 67 | } 68 | 69 | p.RoleBindings[binding.Namespace][binding.Name] = binding 70 | klog.V(6).Infof("RoleBinding %v/%v", binding.Namespace, binding.Name) 71 | } 72 | } 73 | 74 | func (p *Permissions) populateClusterRoleBindings(bindings []rbacv1.ClusterRoleBinding) { 75 | for _, binding := range bindings { 76 | if p.RoleBindings[""] == nil { 77 | p.RoleBindings[""] = make(map[string]rbacv1.RoleBinding) 78 | } 79 | 80 | aBindinig := rbacv1.RoleBinding{ 81 | ObjectMeta: binding.ObjectMeta, 82 | Subjects: binding.Subjects, 83 | RoleRef: binding.RoleRef, 84 | } 85 | 86 | p.RoleBindings[""][binding.Name] = aBindinig 87 | klog.V(6).Infof("ClusterRoleBinding %v", aBindinig.Name) 88 | } 89 | } 90 | 91 | func NewPermissionsFromCluster(client *kube.KubeClient) (*Permissions, error) { 92 | permissions := &Permissions{} 93 | 94 | permissions.ServiceAccounts = make(map[string]map[string]v1.ServiceAccount) 95 | permissions.Roles = make(map[string]map[string]rbacv1.Role) 96 | permissions.RoleBindings = make(map[string]map[string]rbacv1.RoleBinding) 97 | 98 | sas, err := client.ListServiceAccounts(v1.NamespaceAll) 99 | if err != nil { 100 | return nil, err 101 | } 102 | 103 | permissions.populateServiceAccounts(sas) 104 | 105 | roles, err := client.ListRoles(v1.NamespaceAll) 106 | if err != nil { 107 | return nil, err 108 | } 109 | permissions.populateRoles(roles) 110 | 111 | clusterRoles, err := client.ListClusterRoles() 112 | if err != nil { 113 | return nil, err 114 | } 115 | permissions.populateClusterRoles(clusterRoles) 116 | 117 | bindings, err := client.ListRoleBindings(v1.NamespaceAll) 118 | if err != nil { 119 | return nil, err 120 | } 121 | permissions.populateRoleBindings(bindings) 122 | 123 | clusterBindings, err := client.ListClusterRoleBindings() 124 | if err != nil { 125 | return nil, err 126 | } 127 | permissions.populateClusterRoleBindings(clusterBindings) 128 | 129 | return permissions, nil 130 | } 131 | 132 | func NewPermissionsFromResourceList(objs []runtime.Object) (*Permissions, error) { 133 | permissions := &Permissions{} 134 | 135 | permissions.ServiceAccounts = make(map[string]map[string]v1.ServiceAccount) 136 | permissions.Roles = make(map[string]map[string]rbacv1.Role) 137 | permissions.RoleBindings = make(map[string]map[string]rbacv1.RoleBinding) 138 | 139 | sas := []v1.ServiceAccount{} 140 | roles := []rbacv1.Role{} 141 | clusterRoles := []rbacv1.ClusterRole{} 142 | bindings := []rbacv1.RoleBinding{} 143 | clusterBindings := []rbacv1.ClusterRoleBinding{} 144 | 145 | for _, obj := range objs { 146 | 147 | switch o := obj.(type) { 148 | case *v1.ServiceAccount: 149 | sas = append(sas, *o) 150 | case *rbacv1.Role: 151 | roles = append(roles, *o) 152 | case *rbacv1.ClusterRole: 153 | clusterRoles = append(clusterRoles, *o) 154 | case *rbacv1.RoleBinding: 155 | bindings = append(bindings, *o) 156 | case *rbacv1.ClusterRoleBinding: 157 | clusterBindings = append(clusterBindings, *o) 158 | 159 | default: 160 | klog.V(6).Infof("Skipping type %v", obj.GetObjectKind().GroupVersionKind().String()) 161 | } 162 | } 163 | 164 | permissions.populateServiceAccounts(sas) 165 | permissions.populateRoles(roles) 166 | permissions.populateClusterRoles(clusterRoles) 167 | permissions.populateRoleBindings(bindings) 168 | permissions.populateClusterRoleBindings(clusterBindings) 169 | 170 | return permissions, nil 171 | } 172 | -------------------------------------------------------------------------------- /pkg/rbac/static.go: -------------------------------------------------------------------------------- 1 | package rbac 2 | 3 | import ( 4 | "errors" 5 | rbacv1 "k8s.io/api/rbac/v1" 6 | ) 7 | 8 | // StaticRoles is a rule resolver that resolves from lists of role objects. 9 | type StaticRoles struct { 10 | roles []*rbacv1.Role 11 | roleBindings []*rbacv1.RoleBinding 12 | clusterRoles []*rbacv1.ClusterRole 13 | clusterRoleBindings []*rbacv1.ClusterRoleBinding 14 | } 15 | 16 | func (r *StaticRoles) GetRole(namespace, name string) (*rbacv1.Role, error) { 17 | if len(namespace) == 0 { 18 | return nil, errors.New("must provide namespace when getting role") 19 | } 20 | for _, role := range r.roles { 21 | if role.Namespace == namespace && role.Name == name { 22 | return role, nil 23 | } 24 | } 25 | return nil, errors.New("role not found") 26 | } 27 | 28 | func (r *StaticRoles) GetClusterRole(name string) (*rbacv1.ClusterRole, error) { 29 | for _, clusterRole := range r.clusterRoles { 30 | if clusterRole.Name == name { 31 | return clusterRole, nil 32 | } 33 | } 34 | return nil, errors.New("clusterrole not found") 35 | } 36 | 37 | func (r *StaticRoles) ListRoleBindings(namespace string) ([]*rbacv1.RoleBinding, error) { 38 | if len(namespace) == 0 { 39 | return nil, errors.New("must provide namespace when listing role bindings") 40 | } 41 | 42 | roleBindingList := []*rbacv1.RoleBinding{} 43 | for _, roleBinding := range r.roleBindings { 44 | if roleBinding.Namespace != namespace { 45 | continue 46 | } 47 | 48 | roleBindingList = append(roleBindingList, roleBinding) 49 | } 50 | return roleBindingList, nil 51 | } 52 | 53 | func (r *StaticRoles) ListClusterRoleBindings() ([]*rbacv1.ClusterRoleBinding, error) { 54 | return r.clusterRoleBindings, nil 55 | } 56 | -------------------------------------------------------------------------------- /pkg/rbac/subject_permissions.go: -------------------------------------------------------------------------------- 1 | package rbac 2 | 3 | import ( 4 | "sort" 5 | "strings" 6 | 7 | "github.com/kylelemons/godebug/pretty" 8 | v1 "k8s.io/api/rbac/v1" 9 | "k8s.io/klog" 10 | ) 11 | 12 | type PolicyRule struct { 13 | v1.PolicyRule 14 | 15 | //Specify the Roles or ClusterRoles this rule originated from 16 | OriginatedFrom []v1.RoleRef 17 | } 18 | 19 | type SubjectPermissions struct { 20 | Subject v1.Subject 21 | 22 | //Rules Per Namespace ... "" means cluster-wide 23 | Rules map[string][]PolicyRule 24 | } 25 | 26 | func NewSubjectPermissions(perms *Permissions) []SubjectPermissions { 27 | subjects := map[string]*SubjectPermissions{} 28 | 29 | for _, bindings := range perms.RoleBindings { 30 | for _, binding := range bindings { 31 | for _, subject := range binding.Subjects { 32 | var exist bool 33 | var subPerms *SubjectPermissions 34 | 35 | ns := binding.Namespace 36 | if strings.ToLower(binding.RoleRef.Kind) == "clusterrole" { 37 | ns = "" 38 | } 39 | 40 | if subject.Namespace == "" && subject.Kind == v1.ServiceAccountKind && binding.Namespace != "" { 41 | //If for some reason the namespace is abscent from the subject for ServiceAccount - fill it 42 | subject.Namespace = binding.Namespace 43 | } 44 | 45 | roles, exist := perms.Roles[ns] 46 | if !exist { 47 | klog.V(6).Infof("[%v] %+v didn't find roles for namespace '%v'", subject.String(), binding, ns) 48 | continue 49 | } 50 | 51 | role, exist := roles[binding.RoleRef.Name] 52 | if !exist { 53 | klog.V(6).Infof("[%v] %+v didn't find role '%v' in '%v'", subject.String(), binding, binding.RoleRef.Name, ns) 54 | continue 55 | } 56 | 57 | sub := subject.String() 58 | subPerms, exist = subjects[sub] 59 | 60 | if !exist { 61 | subPerms = &SubjectPermissions{ 62 | Subject: subject, 63 | Rules: map[string][]PolicyRule{}, 64 | } 65 | 66 | klog.V(6).Infof("[%v] %+v -- CREATE --", subject.String(), subject) 67 | } 68 | 69 | rules, exist := subPerms.Rules[binding.Namespace] 70 | if !exist { 71 | rules = []PolicyRule{} 72 | } 73 | 74 | roleRules := make([]PolicyRule, len(role.Rules)) 75 | for i, _ := range role.Rules { 76 | roleRules[i].PolicyRule = role.Rules[i] 77 | roleRules[i].OriginatedFrom = []v1.RoleRef{binding.RoleRef} 78 | } 79 | 80 | klog.V(6).Infof("[%v] %+v -- UPDATE -- %v %v %+v", subject.String(), subject, len(rules), len(role.Rules), roleRules) 81 | 82 | rules = append(rules, roleRules...) 83 | subPerms.Rules[binding.Namespace] = rules 84 | subjects[sub] = subPerms 85 | } 86 | } 87 | } 88 | 89 | res := []SubjectPermissions{} 90 | for _, v := range subjects { 91 | res = append(res, *v) 92 | } 93 | 94 | klog.V(10).Infof("%v", pretty.Sprint(res)) 95 | 96 | return res 97 | } 98 | 99 | func ReplaceToWildCard(l []string) { 100 | for i, _ := range l { 101 | if l[i] == "" { 102 | l[i] = "*" 103 | } 104 | } 105 | } 106 | 107 | func ReplaceToCore(l []string) { 108 | for i, _ := range l { 109 | if l[i] == "" { 110 | l[i] = "core" 111 | } 112 | } 113 | } 114 | 115 | type NamespacedPolicyRule struct { 116 | Namespace string `json:"namespace,omitempty"` 117 | 118 | // Verbs is a list of Verbs that apply to ALL the ResourceKinds and AttributeRestrictions contained in this rule. VerbAll represents all kinds. 119 | Verb string `json:"verb"` 120 | 121 | // The name of the APIGroup that contains the resources. 122 | APIGroup string `json:"apiGroup,omitempty"` 123 | 124 | // Resources is a list of resources this rule applies to. ResourceAll represents all resources. 125 | Resource string `json:"resource,omitempty"` 126 | 127 | // ResourceNames is an optional white list of names that the rule applies to. An empty set means that everything is allowed. 128 | ResourceNames []string `json:"resourceNames,omitempty"` 129 | 130 | // NonResourceURLs is a set of partial urls that a user should have access to. *s are allowed, but only as the full, final step in the path 131 | // Since non-resource URLs are not namespaced, this field is only applicable for ClusterRoles referenced from a ClusterRoleBinding. 132 | NonResourceURLs []string `json:"nonResourceURLs,omitempty"` 133 | 134 | //The Role/ClusterRole rule references 135 | OriginatedFrom []v1.RoleRef `json:"originatedFrom,omitempty"` 136 | } 137 | 138 | type SubjectPolicyList struct { 139 | v1.Subject 140 | 141 | AllowedTo []NamespacedPolicyRule `json:"allowedTo,omitempty"` 142 | } 143 | 144 | func NewSubjectPermissionsList(policies []SubjectPermissions) []SubjectPolicyList { 145 | subjectPolicyList := []SubjectPolicyList{} 146 | 147 | for _, p := range policies { 148 | nsrules := []NamespacedPolicyRule{} 149 | for namespace, rules := range p.Rules { 150 | if namespace == "" { 151 | namespace = "*" 152 | } 153 | 154 | for _, rule := range rules { 155 | //Normalize the strings 156 | ReplaceToCore(rule.APIGroups) 157 | ReplaceToWildCard(rule.Resources) 158 | ReplaceToWildCard(rule.ResourceNames) 159 | ReplaceToWildCard(rule.Verbs) 160 | ReplaceToWildCard(rule.NonResourceURLs) 161 | 162 | sort.Strings(rule.APIGroups) 163 | sort.Strings(rule.Resources) 164 | sort.Strings(rule.ResourceNames) 165 | sort.Strings(rule.Verbs) 166 | sort.Strings(rule.NonResourceURLs) 167 | 168 | for _, verb := range rule.Verbs { 169 | 170 | if len(rule.NonResourceURLs) == 0 { 171 | // The common case ... let's flatten the rule 172 | for _, apiGroup := range rule.APIGroups { 173 | for _, resource := range rule.Resources { 174 | subjectPolicy := NamespacedPolicyRule{ 175 | Namespace: namespace, 176 | Verb: verb, 177 | APIGroup: apiGroup, 178 | Resource: resource, 179 | ResourceNames: rule.ResourceNames, 180 | NonResourceURLs: rule.NonResourceURLs, 181 | OriginatedFrom: rule.OriginatedFrom, 182 | } 183 | 184 | nsrules = append(nsrules, subjectPolicy) 185 | } 186 | 187 | } 188 | } else { 189 | // NonResourceURL ... not namespaced 190 | subjectPolicy := NamespacedPolicyRule{ 191 | Namespace: namespace, 192 | Verb: verb, 193 | NonResourceURLs: rule.NonResourceURLs, 194 | OriginatedFrom: rule.OriginatedFrom, 195 | } 196 | 197 | nsrules = append(nsrules, subjectPolicy) 198 | } 199 | } 200 | } 201 | } 202 | subjectPolicyList = append( 203 | subjectPolicyList, SubjectPolicyList{ 204 | Subject: p.Subject, 205 | AllowedTo: nsrules, 206 | }) 207 | } 208 | 209 | return subjectPolicyList 210 | } 211 | -------------------------------------------------------------------------------- /pkg/utils/console_printer.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/fatih/color" 8 | ) 9 | 10 | var ( 11 | rbacToolPrefix = color.New(color.FgBlue).SprintFunc() 12 | lineMsg = color.New(color.FgHiWhite).SprintFunc() 13 | ) 14 | 15 | func ConsolePrinter(msg string) { 16 | fmt.Fprintln(os.Stderr, rbacToolPrefix("[RAPID7-INSIGHTCLOUDSEC]"), lineMsg(msg)) 17 | } 18 | -------------------------------------------------------------------------------- /pkg/utils/namespaces.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | v1 "k8s.io/api/core/v1" 5 | "k8s.io/apimachinery/pkg/util/sets" 6 | "strings" 7 | ) 8 | 9 | func GetNamespaceSets(nsInclude string, nsExclude string) (sets.String, sets.String) { 10 | NamespaceInclude := sets.NewString() 11 | NamespaceExclude := sets.NewString() 12 | 13 | if nsInclude == "" { 14 | NamespaceInclude.Insert("*") 15 | } else { 16 | inclusionList := strings.TrimSpace(nsInclude) 17 | inclusion := strings.Split(inclusionList, ",") 18 | 19 | for _, inc := range inclusion { 20 | NamespaceInclude.Insert(strings.ToLower(inc)) 21 | } 22 | } 23 | 24 | if nsExclude != "" { 25 | exclusionList := strings.TrimSpace(nsExclude) 26 | exclusion := strings.Split(exclusionList, ",") 27 | 28 | for _, exc := range exclusion { 29 | NamespaceExclude.Insert(strings.ToLower(exc)) 30 | } 31 | } 32 | 33 | return NamespaceInclude, NamespaceExclude 34 | } 35 | 36 | func IsNamespaceIncluded(namespace string, nsInclude sets.String, nsExclude sets.String) bool { 37 | 38 | if nsExclude.Len() > 0 && isInNamespace(namespace, nsExclude) { 39 | return false 40 | } 41 | 42 | if nsInclude.Len() > 0 && isInNamespace(namespace, nsInclude) { 43 | return true 44 | } 45 | 46 | return false 47 | } 48 | 49 | func isInNamespace(namespace string, namespaceSet sets.String) (valid bool) { 50 | if namespaceSet.Has(v1.NamespaceAll) || namespaceSet.Has("*") || namespaceSet.Has(strings.ToLower(namespace)) { 51 | return true 52 | } 53 | 54 | return false 55 | } 56 | -------------------------------------------------------------------------------- /pkg/utils/object_reader.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "io/ioutil" 8 | "os" 9 | "reflect" 10 | "strings" 11 | 12 | v1 "k8s.io/api/core/v1" 13 | "k8s.io/apimachinery/pkg/runtime" 14 | "k8s.io/apimachinery/pkg/util/errors" 15 | "k8s.io/client-go/kubernetes/scheme" 16 | "k8s.io/klog" 17 | ) 18 | 19 | func ReadYamlManifest(r io.Reader) ([]runtime.Object, error) { 20 | decoded := []runtime.Object{} 21 | 22 | buf, err := ioutil.ReadAll(r) 23 | 24 | if err != nil { 25 | return nil, err 26 | } 27 | 28 | //klog.V(6).Infof("ReadAll '%v'", string(buf)) 29 | 30 | bufSlice := bytes.Split(buf, []byte("\n---")) 31 | decoder := scheme.Codecs.UniversalDeserializer() 32 | 33 | for _, b := range bufSlice { 34 | obj, _, err := decoder.Decode(b, nil, nil) 35 | 36 | if err != nil { 37 | klog.V(6).Infof("failed to decode - %v - '%v'", err, string(b)) 38 | continue 39 | } 40 | 41 | if obj == nil { 42 | klog.V(6).Infof("failed to decode - %v", string(b)) 43 | continue 44 | } 45 | 46 | decoded = append(decoded, obj) 47 | } 48 | 49 | return decoded, nil 50 | } 51 | 52 | func ReadObjectList(r io.Reader) ([]runtime.Object, error) { 53 | buf, err := ioutil.ReadAll(r) 54 | 55 | if err != nil { 56 | return nil, err 57 | } 58 | 59 | decoder := scheme.Codecs.UniversalDeserializer() 60 | obj, gvk, err := decoder.Decode(buf, nil, nil) 61 | if err != nil { 62 | return nil, err 63 | } 64 | 65 | if obj == nil { 66 | return nil, fmt.Errorf("Failed to decode") 67 | } 68 | 69 | switch o := obj.(type) { 70 | case *v1.List: 71 | if gvk.GroupKind().Kind != "List" { 72 | return nil, fmt.Errorf("Expected List Object Kind") 73 | } 74 | 75 | return convert(o.Items) 76 | default: 77 | return nil, fmt.Errorf("Failed to cast loaded object - '%v'", reflect.TypeOf(obj)) 78 | } 79 | } 80 | 81 | func ReadObjectsFromFile(filename string) ([]runtime.Object, error) { 82 | objs := []runtime.Object{} 83 | 84 | var tee io.Reader 85 | var buf bytes.Buffer 86 | 87 | if filename != "-" { 88 | f, err := os.Open(filename) 89 | if err != nil { 90 | return nil, err 91 | } 92 | defer f.Close() 93 | tee = io.TeeReader(f, &buf) 94 | } else { 95 | tee = io.TeeReader(os.Stdin, &buf) 96 | } 97 | 98 | if l, err := ReadObjectList(tee); err == nil { 99 | klog.V(6).Infof("Loaded from Object List %v resources", len(l)) 100 | objs = l 101 | } else { 102 | klog.V(6).Infof("Couldn't read Object List (%v) from %v ... trying to load as YAML", err, filename) 103 | if l, err := ReadYamlManifest(strings.NewReader(buf.String())); err == nil { 104 | klog.V(6).Infof("Loaded from YAML %v resources %v", filename, len(l)) 105 | objs = l 106 | } else { 107 | return nil, fmt.Errorf("Failed to read kubernetes resources") 108 | } 109 | } 110 | 111 | return objs, nil 112 | } 113 | 114 | func convert(objs []runtime.RawExtension) ([]runtime.Object, error) { 115 | errs := []error{} 116 | decoded := []runtime.Object{} 117 | 118 | decoder := scheme.Codecs.UniversalDeserializer() 119 | 120 | for _, raw := range objs { 121 | obj, gvk, err := decoder.Decode(raw.Raw, nil, nil) 122 | if err != nil { 123 | errs = append(errs, err) 124 | continue 125 | } 126 | 127 | if obj == nil { 128 | errs = append(errs, fmt.Errorf("Object %+v decoded into nil", gvk)) 129 | continue 130 | } 131 | 132 | decoded = append(decoded, obj) 133 | } 134 | 135 | return decoded, errors.NewAggregate(errs) 136 | } 137 | -------------------------------------------------------------------------------- /pkg/utils/struct_to_map.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "github.com/fatih/structs" 6 | "reflect" 7 | ) 8 | 9 | type Map map[string]string 10 | 11 | func StructToMap(v interface{}) map[string]string { 12 | 13 | map_v := structs.Map(v) 14 | flat_v := Flatten(map_v) 15 | 16 | return (map[string]string)(flat_v) 17 | } 18 | 19 | // Flatten takes a structure and turns into a flat map[string]string. 20 | // 21 | // Within the "thing" parameter, only primitive values are allowed. Structs are 22 | // not supported. Therefore, it can only be slices, maps, primitives, and 23 | // any combination of those together. 24 | // 25 | // See the tests for examples of what inputs are turned into. 26 | func Flatten(thing map[string]interface{}) Map { 27 | result := make(map[string]string) 28 | 29 | for k, raw := range thing { 30 | flatten(result, k, reflect.ValueOf(raw)) 31 | } 32 | 33 | return Map(result) 34 | } 35 | 36 | func flatten(result map[string]string, prefix string, v reflect.Value) { 37 | if v.Kind() == reflect.Interface || v.Kind() == reflect.Ptr { 38 | if v.IsNil() { 39 | return 40 | } 41 | 42 | v = v.Elem() 43 | flatten(result, prefix, v) 44 | return 45 | } 46 | 47 | switch v.Kind() { 48 | case reflect.Bool: 49 | if v.Bool() { 50 | result[prefix] = "true" 51 | } else { 52 | result[prefix] = "false" 53 | } 54 | case reflect.Int, reflect.Int32, reflect.Int64: 55 | result[prefix] = fmt.Sprintf("%d", v.Int()) 56 | case reflect.Uint, reflect.Uint32, reflect.Uint64: 57 | result[prefix] = fmt.Sprintf("%d", v.Uint()) 58 | case reflect.Map: 59 | flattenMap(result, prefix, v) 60 | case reflect.Slice: 61 | flattenSlice(result, prefix, v) 62 | case reflect.String: 63 | result[prefix] = v.String() 64 | default: 65 | panic(fmt.Sprintf("Unknown: %s", v)) 66 | } 67 | } 68 | 69 | func flattenMap(result map[string]string, prefix string, v reflect.Value) { 70 | for _, k := range v.MapKeys() { 71 | if k.Kind() == reflect.Interface { 72 | k = k.Elem() 73 | } 74 | 75 | if k.Kind() != reflect.String { 76 | panic(fmt.Sprintf("%s: map key is not string: %s", prefix, k)) 77 | } 78 | 79 | flatten(result, fmt.Sprintf("%s.%s", prefix, k.String()), v.MapIndex(k)) 80 | } 81 | } 82 | 83 | func flattenSlice(result map[string]string, prefix string, v reflect.Value) { 84 | //prefix = prefix + "." 85 | 86 | //result[prefix+"#"] = fmt.Sprintf("%d", v.Len()) 87 | for i := 0; i < v.Len(); i++ { 88 | flatten(result, fmt.Sprintf("%s[%d]", prefix, i), v.Index(i)) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /pkg/utils/writefile.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "syscall" 8 | ) 9 | 10 | // FileExists checks if specified file exists. 11 | func FileExists(filename string) (bool, error) { 12 | if _, err := os.Stat(filename); os.IsNotExist(err) { 13 | return false, nil 14 | } else if err != nil { 15 | return false, err 16 | } 17 | return true, nil 18 | } 19 | 20 | func WriteFile(outfile string, data string) error { 21 | if outfile == "-" { 22 | fmt.Println(data) 23 | return nil 24 | } 25 | 26 | if exist, err := FileExists(outfile); err == nil && exist { 27 | syscall.Unlink(outfile) 28 | } 29 | 30 | return ioutil.WriteFile(outfile, []byte(data), 0644) 31 | } 32 | -------------------------------------------------------------------------------- /pkg/visualize/dotgraph.go: -------------------------------------------------------------------------------- 1 | package visualize 2 | 3 | import ( 4 | "fmt" 5 | "github.com/emicklei/dot" 6 | "html" 7 | "strings" 8 | ) 9 | 10 | const ( 11 | fontName = "Poppins 100 normal" 12 | 13 | redOutline = "#e33a1f" 14 | 15 | serviceAccountColor = "#1b60db" 16 | serviceAccountOutline = "#01040a" 17 | serviceAccountText = "white" 18 | 19 | roleColor = "#17b87e" 20 | roleColorOutline = "#01080a" 21 | roleColorText = "white" 22 | 23 | roleBindingColor = "#00994c" 24 | roleBindingColorOutline = "#01080a" 25 | roleBindingColorText = "white" 26 | 27 | clusterRoleColor = "#006666" 28 | clusterRoleColorOutline = "#01080a" 29 | clusterRoleColorText = "#f4f4f4" 30 | 31 | clusterRoleBindingColor = "#006633" 32 | clusterRoleBindingColorOutline = "#01080a" 33 | clusterRoleBindingColorText = "#f4f4f4" 34 | 35 | pspColor = "#ffbf00" 36 | pspColorOutline = "#01080a" 37 | pspColorText = "black" 38 | ) 39 | 40 | func newGraph() *dot.Graph { 41 | g := dot.NewGraph(dot.Directed) 42 | 43 | g.Attr("fontsize", "12.00") 44 | g.Attr("fontname", fontName) 45 | // global rank instead of per-subgraph (ensures access rules are always in the same place (at bottom)) 46 | g.Attr("newrank", "true") 47 | return g 48 | } 49 | 50 | func newNamespaceSubgraph(g *dot.Graph, namespace string) *dot.Graph { 51 | if namespace == "" { 52 | return g 53 | } 54 | 55 | gns := g.Subgraph(namespace, dot.ClusterOption{}) 56 | gns.Attr("style", "rounded,dashed") 57 | 58 | return gns 59 | } 60 | 61 | func newSubjectNode0(g *dot.Graph, kind, name string, exists, highlight bool) dot.Node { 62 | return g.Node(kind+"-"+name). 63 | Box(). 64 | Attr("label", formatLabel(fmt.Sprintf("%s\n(%s)", name, kind), highlight)). 65 | Attr("style", iff(exists, "filled", "dotted")). 66 | Attr("color", iff(exists, serviceAccountOutline, redOutline)). 67 | Attr("penwidth", iff(highlight || !exists, "2.0", "1.0")). 68 | Attr("margin", "0.22,0.11"). 69 | Attr("fillcolor", serviceAccountColor). 70 | Attr("fontcolor", iff(exists, serviceAccountText, "#030303")). 71 | Attr("fontname", fontName) 72 | } 73 | 74 | func newRoleBindingNode(g *dot.Graph, name string, highlight bool) dot.Node { 75 | return g.Node("rb-"+name). 76 | Attr("label", formatLabel(name, highlight)). 77 | Attr("shape", "oval"). 78 | Attr("style", "filled"). 79 | Attr("penwidth", iff(highlight, "2.0", "1.0")). 80 | Attr("fillcolor", roleBindingColor). 81 | Attr("color", roleBindingColorOutline). 82 | Attr("fontcolor", roleBindingColorText). 83 | Attr("fontname", fontName) 84 | } 85 | 86 | func newRoleNode(g *dot.Graph, namespace, name string, exists, highlight bool) dot.Node { 87 | node := g.Node("r-"+namespace+"/"+name). 88 | Attr("label", formatLabel(name, highlight)). 89 | Attr("shape", "oval"). 90 | Attr("style", iff(exists, "filled", "dotted")). 91 | Attr("color", iff(exists, roleColorOutline, redOutline)). 92 | Attr("penwidth", iff(highlight || !exists, "2.0", "1.0")). 93 | Attr("fillcolor", roleColor). 94 | Attr("fontcolor", iff(exists, roleColorText, "#030303")). 95 | Attr("fontname", fontName) 96 | g.Root().AddToSameRank("Roles", node) 97 | return node 98 | } 99 | 100 | func newClusterRoleBindingNode(g *dot.Graph, name string, highlight bool) dot.Node { 101 | return g.Node("crb-"+name). 102 | Attr("label", formatLabel(name, highlight)). 103 | Attr("shape", "oval"). 104 | Attr("style", "filled"). 105 | Attr("penwidth", iff(highlight, "2.0", "1.0")). 106 | Attr("fillcolor", clusterRoleBindingColor). 107 | Attr("color", clusterRoleBindingColorOutline). 108 | Attr("fontcolor", clusterRoleBindingColorText). 109 | Attr("fontname", fontName) 110 | } 111 | 112 | func newClusterRoleNode(g *dot.Graph, bindingNamespace, roleName string, exists, highlight bool) dot.Node { 113 | node := g.Node("cr-"+bindingNamespace+"/"+roleName). 114 | Attr("label", formatLabel(roleName, highlight)). 115 | Attr("shape", "oval"). 116 | Attr("style", iff(exists, iff(bindingNamespace == "", "filled", "filled,dashed"), "dotted")). 117 | Attr("color", iff(exists, clusterRoleColorOutline, redOutline)). 118 | Attr("penwidth", iff(highlight || !exists, "2.0", "1.0")). 119 | Attr("fillcolor", clusterRoleColor). 120 | Attr("fontcolor", iff(exists, clusterRoleColorText, "#030303")). 121 | Attr("fontname", fontName) 122 | g.Root().AddToSameRank("Roles", node) 123 | return node 124 | } 125 | 126 | func newRulesNode0(g *dot.Graph, namespace, roleName, rulesHTML string, highlight bool) dot.Node { 127 | return g.Node("rules-"+namespace+"/"+roleName). 128 | Attr("label", dot.HTML(rulesHTML)). 129 | Attr("shape", "note"). 130 | Attr("fillcolor", "#DCDCDC"). 131 | Attr("penwidth", iff(highlight, "2.0", "1.0")). 132 | Attr("fontsize", "10") 133 | } 134 | 135 | func pspNodeId(pspName string) string { 136 | return "psp-" + strings.ToLower(pspName) 137 | } 138 | 139 | func newPSPNode(g *dot.Graph, bindingNamespace, pspName string, exists, highlight bool) dot.Node { 140 | node := g.Node(pspNodeId(pspName)). 141 | Attr("label", formatLabel(pspName, highlight)). 142 | Attr("shape", "note"). 143 | Attr("style", iff(exists, iff(bindingNamespace == "", "filled", "filled,dashed"), "dotted")). 144 | Attr("color", iff(exists, pspColorOutline, redOutline)). 145 | Attr("penwidth", iff(highlight || !exists, "2.0", "1.0")). 146 | Attr("fillcolor", pspColor). 147 | Attr("fontcolor", iff(exists, pspColorText, "#030303")). 148 | Attr("fontname", fontName) 149 | g.Root().AddToSameRank("PSPs", node) 150 | return node 151 | } 152 | 153 | func newPSPRulesNode(g *dot.Graph, pspName, rulesHTML string, highlight bool) dot.Node { 154 | return g.Node("psp-rules-"+pspName). 155 | Attr("label", dot.HTML(rulesHTML)). 156 | Attr("shape", "note"). 157 | Attr("fillcolor", "#DCDCDC"). 158 | Attr("penwidth", iff(highlight, "2.0", "1.0")). 159 | Attr("fontsize", "10") 160 | } 161 | 162 | func formatLabel(label string, highlight bool) interface{} { 163 | if highlight { 164 | return dot.HTML("" + html.EscapeString(label) + "") 165 | } else { 166 | return label 167 | } 168 | } 169 | 170 | func newSubjectToBindingEdge(subjectNode dot.Node, bindingNode dot.Node) dot.Edge { 171 | return edge(subjectNode, bindingNode).Attr("dir", "back") 172 | } 173 | 174 | func newBindingToRoleEdge(bindingNode dot.Node, roleNode dot.Node) dot.Edge { 175 | return edge(bindingNode, roleNode) 176 | } 177 | 178 | func newRoleToRulesEdge(roleNode dot.Node, rulesNode dot.Node) dot.Edge { 179 | return edge(roleNode, rulesNode) 180 | } 181 | 182 | // edge creates a new edge between two nodes, but only if the edge doesn't exist yet 183 | func edge(from dot.Node, to dot.Node) dot.Edge { 184 | existingEdges := from.EdgesTo(to) 185 | if len(existingEdges) == 0 { 186 | return from.Edge(to) 187 | } else { 188 | return existingEdges[0] 189 | } 190 | } 191 | 192 | func iff(condition bool, string1, string2 string) string { 193 | if condition { 194 | return string1 195 | } else { 196 | return string2 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /pkg/visualize/output.go: -------------------------------------------------------------------------------- 1 | package visualize 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "github.com/Masterminds/sprig" 7 | "github.com/alcideio/rbac-tool/pkg/utils" 8 | "github.com/emicklei/dot" 9 | "k8s.io/klog" 10 | "text/template" 11 | ) 12 | 13 | func GenerateOutput(filename string, format string, g *dot.Graph, legend *dot.Graph, opts *Opts) error { 14 | 15 | switch format { 16 | 17 | case "html": 18 | report := HtmlReport{ 19 | Graph: g, 20 | Legend: legend, 21 | opts: opts, 22 | } 23 | 24 | out, err := report.Generate() 25 | if err != nil { 26 | return err 27 | } 28 | 29 | return utils.WriteFile(filename, out) 30 | case "dot": 31 | fallthrough 32 | default: 33 | return utils.WriteFile(filename, g.String()) 34 | } 35 | 36 | } 37 | 38 | type HtmlReport struct { 39 | Graph *dot.Graph 40 | Legend *dot.Graph 41 | opts *Opts 42 | counter int64 43 | } 44 | 45 | func (r *HtmlReport) generateHeader() string { 46 | 47 | data := ` 48 | 110 | ` 111 | 112 | tmpl, err := r.newTemplateEngine("header", data) 113 | if err != nil { 114 | klog.Errorf("Failed to generate category chart - %v", err) 115 | return "" 116 | } 117 | 118 | buf := new(bytes.Buffer) 119 | 120 | err = tmpl.Execute(buf, r) 121 | if err != nil { 122 | klog.Errorf("Failed to execute category chart - %v", err) 123 | return "" 124 | } 125 | 126 | return buf.String() 127 | } 128 | 129 | func (r *HtmlReport) generateFooter() string { 130 | 131 | data := ` 132 |
133 |
134 |
135 |
136 |
137 | 138 | 139 |

Brought to You by Rapid7 InsightCloudSec Kubernetes Obsession

140 |
141 |
142 |
143 |
144 |
145 |
146 | ` 147 | 148 | tmpl, err := r.newTemplateEngine("footer", data) 149 | if err != nil { 150 | klog.Errorf("Failed to generate footer - %v", err) 151 | return "" 152 | } 153 | 154 | buf := new(bytes.Buffer) 155 | 156 | err = tmpl.Execute(buf, r) 157 | if err != nil { 158 | klog.Errorf("Failed to execute footer - %v", err) 159 | return "" 160 | } 161 | 162 | return buf.String() 163 | } 164 | 165 | func (r *HtmlReport) generateBody() string { 166 | data := ` 167 |
168 |
169 |
170 |
171 | 172 |
    173 |
  • {{ generateGraph .Legend "legend" "100%" }}
  • 174 |
175 |
176 | Legend 177 |
178 |
179 |
180 |
181 | {{ generateGraph .Graph "rbacgraph" "auto" }} 182 |
183 |
184 |
185 |
186 |
187 | ` 188 | funcs := sprig.TxtFuncMap() 189 | 190 | funcs["uniqueCounter"] = r.uniqueCounter 191 | funcs["generateGraph"] = r.generateGraph 192 | 193 | tmpl, err := r.newTemplateEngine("body", data) 194 | if err != nil { 195 | klog.Errorf("Failed to generate body - %v", err) 196 | return "" 197 | } 198 | 199 | buf := new(bytes.Buffer) 200 | 201 | err = tmpl.Funcs(funcs).Execute(buf, r) 202 | if err != nil { 203 | klog.Errorf("Failed to execute category chart - %v", err) 204 | return "" 205 | } 206 | 207 | return buf.String() 208 | 209 | } 210 | 211 | func (r *HtmlReport) generateGraph(graph *dot.Graph, divId string, widthStyle string) string { 212 | fmtHtmlCode := ` 213 | 219 |
220 |
221 | 222 | 250 | ` 251 | 252 | return fmtHtmlCode 253 | } 254 | 255 | func (r *HtmlReport) uniqueCounter() string { 256 | r.counter++ 257 | 258 | return fmt.Sprint(r.counter) 259 | } 260 | 261 | func (r *HtmlReport) newTemplateEngine(name string, data string) (*template.Template, error) { 262 | funcs := sprig.TxtFuncMap() 263 | 264 | funcs["uniqueCounter"] = r.uniqueCounter 265 | funcs["generateHeader"] = r.generateHeader 266 | funcs["generateBody"] = r.generateBody 267 | funcs["generateFooter"] = r.generateFooter 268 | funcs["generateGraph"] = r.generateGraph 269 | 270 | return template.New(name).Funcs(funcs).Parse(data) 271 | } 272 | 273 | func (r *HtmlReport) Generate() (out string, err error) { 274 | 275 | html := ` 276 | 277 | 278 | 279 | 280 | 281 | 282 | [Rapid7 | InsightCloudSec] Kubernetes RBAC Power Toys 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | {{ generateHeader }} 305 | 306 | {{ generateBody }} 307 | 308 | {{ generateFooter }} 309 | 310 | 311 | 312 | ` 313 | 314 | tmpl, err := r.newTemplateEngine("full-report", html) 315 | if err != nil { 316 | return "", err 317 | } 318 | 319 | buf := new(bytes.Buffer) 320 | 321 | err = tmpl.Execute(buf, r) 322 | if err != nil { 323 | return "", err 324 | } 325 | 326 | return buf.String(), nil 327 | } 328 | -------------------------------------------------------------------------------- /pkg/visualize/types.go: -------------------------------------------------------------------------------- 1 | package visualize 2 | 3 | import ( 4 | "fmt" 5 | "github.com/alcideio/rbac-tool/pkg/rbac" 6 | v1 "k8s.io/api/core/v1" 7 | "k8s.io/apimachinery/pkg/util/sets" 8 | ) 9 | 10 | type Opts struct { 11 | //Input source - cluster or input file/stdin 12 | ClusterContext string 13 | Infile string 14 | 15 | //Show Actuall use by Pods 16 | ShowPodsOnly bool 17 | 18 | Outfile string 19 | Outformat string 20 | ShowRules bool 21 | ShowLegend bool 22 | ShowPSP bool 23 | 24 | IncludedNamespaces string 25 | ExcludedNamespaces string 26 | 27 | IncludeSubjectsRegex string 28 | } 29 | 30 | func (o *Opts) Validate() error { 31 | if o.Infile != "" && o.ClusterContext != "" { 32 | return fmt.Errorf("Either use input file or specify cluster context") 33 | } 34 | 35 | return nil 36 | } 37 | 38 | type Permissions struct { 39 | rbac.Permissions 40 | 41 | ServiceAccountsUsed sets.String 42 | Pods map[string]map[string]v1.Pod //map[namespace]map[name]Pod 43 | } 44 | -------------------------------------------------------------------------------- /pkg/whoami/whoami.go: -------------------------------------------------------------------------------- 1 | package whoami 2 | 3 | import ( 4 | "context" 5 | "crypto/x509" 6 | "fmt" 7 | "net/http" 8 | "os" 9 | "strings" 10 | 11 | "github.com/alcideio/rbac-tool/pkg/kube" 12 | "github.com/kylelemons/godebug/pretty" 13 | v1 "k8s.io/api/authentication/v1" 14 | authz "k8s.io/api/authorization/v1" 15 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 16 | clientset "k8s.io/client-go/kubernetes" 17 | "k8s.io/client-go/transport" 18 | "k8s.io/klog" 19 | ) 20 | 21 | // tokenExtractor helps to retrieve token 22 | type tokenExtractor struct { 23 | rountTripper http.RoundTripper 24 | token string 25 | } 26 | 27 | // RoundTrip gets token 28 | func (t *tokenExtractor) RoundTrip(req *http.Request) (*http.Response, error) { 29 | header := req.Header.Get("authorization") 30 | 31 | if strings.HasPrefix(header, "Bearer ") { 32 | t.token = strings.ReplaceAll(header, "Bearer ", "") 33 | klog.V(5).Infof("extracted token successfully") 34 | } else { 35 | klog.V(5).Infof("could not extract token from header") 36 | } 37 | 38 | return t.rountTripper.RoundTrip(req) 39 | } 40 | 41 | func extractToken(client *kube.KubeClient) (string, error) { 42 | config := client.Config 43 | tokenExtractor := &tokenExtractor{} 44 | config.Wrap(func(rt http.RoundTripper) http.RoundTripper { 45 | tokenExtractor.rountTripper = rt 46 | return tokenExtractor 47 | }) 48 | 49 | k8sClient, err := clientset.NewForConfig(config) 50 | if err != nil { 51 | return "", err 52 | } 53 | 54 | _, err = k8sClient.AuthorizationV1().SelfSubjectAccessReviews().Create( 55 | context.Background(), 56 | &authz.SelfSubjectAccessReview{ 57 | Spec: authz.SelfSubjectAccessReviewSpec{ 58 | ResourceAttributes: &authz.ResourceAttributes{ 59 | Namespace: "", 60 | }, 61 | }, 62 | }, 63 | metav1.CreateOptions{}, 64 | ) 65 | 66 | return tokenExtractor.token, nil 67 | } 68 | 69 | func ExtractUserInfo(client *kube.KubeClient) (*v1.UserInfo, error) { 70 | var token string 71 | userInfo := &v1.UserInfo{} 72 | 73 | config := client.Config 74 | 75 | c, err := config.TransportConfig() 76 | if err != nil { 77 | return nil, err 78 | } 79 | 80 | klog.V(9).Infof("Config:\n%v\n", pretty.Sprint(config)) 81 | 82 | // Token based authentication has preference over basic auth and certificate auth 83 | if c.HasTokenAuth() { 84 | if config.BearerTokenFile != "" { 85 | klog.V(5).Infof("extracting token from file '%v'", config.BearerTokenFile) 86 | d, err := os.ReadFile(config.BearerTokenFile) 87 | if err != nil { 88 | return nil, err 89 | } 90 | 91 | token = string(d) 92 | } 93 | 94 | if config.BearerToken != "" { 95 | klog.V(5).Infof("extracting token bearer") 96 | token = config.BearerToken 97 | } 98 | } 99 | 100 | if token == "" { 101 | klog.V(5).Infof("extracting token actively") 102 | t, err := extractToken(client) 103 | if err != nil { 104 | return nil, err 105 | } 106 | 107 | token = t 108 | } 109 | 110 | user, err := client.TokenReview(token) 111 | 112 | if err != nil { 113 | klog.V(5).Infof("TokenReview failed to '%v'", err) 114 | 115 | if c.HasCertAuth() { 116 | cert, err := getClientCertificate(c) 117 | if err != nil { 118 | return nil, err 119 | } 120 | klog.V(5).Infof("extracting from cert '%v'", pretty.Sprint(cert.Subject)) 121 | userInfo.Username = cert.Subject.CommonName 122 | userInfo.Groups = cert.Subject.Organization 123 | return userInfo, nil 124 | } 125 | 126 | if c.HasBasicAuth() { 127 | klog.V(5).Infof("extracting from basic auth") 128 | userInfo.Username = config.Username 129 | return userInfo, nil 130 | } 131 | 132 | return nil, fmt.Errorf("Failed to extract user information") 133 | } 134 | 135 | klog.V(5).Infof("TokenReview extraction completed ok '%v'", user) 136 | return &user, nil 137 | } 138 | 139 | func getClientCertificate(c *transport.Config) (*x509.Certificate, error) { 140 | tlsConfig, err := transport.TLSConfigFor(c) 141 | if err != nil { 142 | return nil, err 143 | } 144 | // GetClientCertificate has been set in transport.TLSConfigFor, 145 | // so it is not nil 146 | cert, err := tlsConfig.GetClientCertificate(nil) 147 | if err != nil { 148 | return nil, err 149 | } 150 | if cert.Leaf != nil { 151 | return cert.Leaf, nil 152 | } 153 | return x509.ParseCertificate(cert.Certificate[0]) 154 | } 155 | -------------------------------------------------------------------------------- /rbac-tool.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alcideio/rbac-tool/a0b8c036b90b8ed31de9ff1fcef854312f52c418/rbac-tool.png -------------------------------------------------------------------------------- /testdata/auditgen/testdata-01.json: -------------------------------------------------------------------------------- 1 | {"kind":"Event","apiVersion":"audit.k8s.io/v1","level":"Metadata","auditID":"c38408b4-23f7-4942-bcd7-d9019ae2e8ea","stage":"ResponseComplete","requestURI":"/api/v1/namespaces/ns1/pods","verb":"create","user":{"username":"admin","uid":"admin","groups":["system:masters","system:authenticated"]},"sourceIPs":["98.207.36.92"],"userAgent":"kubectl/v1.17.4 (darwin/amd64) kubernetes/8d8aa39","objectRef":{"resource":"pods","namespace":"ns1","name":"pod-1","apiVersion":"v1"},"responseStatus":{"metadata":{},"code":201},"requestReceivedTimestamp":"2020-04-08T16:32:36.125094Z","stageTimestamp":"2020-04-08T16:32:36.136498Z","annotations":{"authorization.k8s.io/decision":"allow","authorization.k8s.io/reason":""}} 2 | {"kind":"Event","apiVersion":"audit.k8s.io/v1","level":"Request","auditID":"1a7c78f0-bc92-44ff-bb50-87f61241db73","stage":"ResponseComplete","requestURI":"/api/v1/namespaces/ns2/configmaps","verb":"create","user":{"username":"admin","uid":"admin","groups":["system:masters","system:authenticated"]},"sourceIPs":["98.207.36.92"],"userAgent":"kubectl/v1.17.4 (darwin/amd64) kubernetes/8d8aa39","objectRef":{"resource":"configmaps","namespace":"ns2","name":"cm-2","apiVersion":"v1"},"responseStatus":{"metadata":{},"code":201},"requestObject":{"kind":"ConfigMap","apiVersion":"v1","metadata":{"name":"cm-2","namespace":"ns2","creationTimestamp":null,"annotations":{"kubectl.kubernetes.io/last-applied-configuration":"{\"apiVersion\":\"v1\",\"data\":{\"secret\":\"\\\"xxx\\\"\"},\"kind\":\"ConfigMap\",\"metadata\":{\"annotations\":{},\"name\":\"cm-2\",\"namespace\":\"ns2\"}}\n"}},"data":{"secret":"\"xxx\""}},"requestReceivedTimestamp":"2020-04-08T16:32:36.399597Z","stageTimestamp":"2020-04-08T16:32:36.403499Z","annotations":{"authorization.k8s.io/decision":"allow","authorization.k8s.io/reason":""}} 3 | {"kind":"Event","apiVersion":"audit.k8s.io/v1","level":"RequestResponse","auditID":"b46bced3-5c91-4159-9657-f7b4322b884e","stage":"ResponseComplete","requestURI":"/api/v1/namespaces/ns3/secrets","verb":"create","user":{"username":"admin","uid":"admin","groups":["system:masters","system:authenticated"]},"sourceIPs":["98.207.36.92"],"userAgent":"kubectl/v1.17.4 (darwin/amd64) kubernetes/8d8aa39","objectRef":{"resource":"secrets","namespace":"ns3","name":"secret-3","apiVersion":"v1"},"responseStatus":{"metadata":{},"code":201},"requestObject":{"kind":"Secret","apiVersion":"v1","metadata":{"name":"secret-3","namespace":"ns3","creationTimestamp":null,"annotations":{"kubectl.kubernetes.io/last-applied-configuration":"{\"apiVersion\":\"v1\",\"data\":{\"secret\":\"eXl5Cg==\"},\"kind\":\"Secret\",\"metadata\":{\"annotations\":{},\"name\":\"secret-3\",\"namespace\":\"ns3\"},\"type\":\"Opaque\"}\n"}},"data":{"secret":"eXl5Cg=="},"type":"Opaque"},"responseObject":{"kind":"Secret","apiVersion":"v1","metadata":{"name":"secret-3","namespace":"ns3","selfLink":"/api/v1/namespaces/ns3/secrets/secret-3","uid":"8e4f4dd6-79b6-11ea-a056-0a39f00d8287","resourceVersion":"134185","creationTimestamp":"2020-04-08T16:32:36Z","annotations":{"kubectl.kubernetes.io/last-applied-configuration":"{\"apiVersion\":\"v1\",\"data\":{\"secret\":\"eXl5Cg==\"},\"kind\":\"Secret\",\"metadata\":{\"annotations\":{},\"name\":\"secret-3\",\"namespace\":\"ns3\"},\"type\":\"Opaque\"}\n"}},"data":{"secret":"eXl5Cg=="},"type":"Opaque"},"requestReceivedTimestamp":"2020-04-08T16:32:36.678782Z","stageTimestamp":"2020-04-08T16:32:36.682243Z","annotations":{"authorization.k8s.io/decision":"allow","authorization.k8s.io/reason":""}} -------------------------------------------------------------------------------- /testdata/auditgen/testdata-02.json: -------------------------------------------------------------------------------- 1 | {"kind":"Event","apiVersion":"audit.k8s.io/v1","level":"Metadata","auditID":"c38408b4-23f7-4942-bcd7-d9019ae2e8ea","stage":"ResponseComplete","requestURI":"/api/v1/namespaces/ns1/pods","verb":"create","user":{"username":"diana","uid":"diana","groups":["system:authenticated"]},"sourceIPs":["98.207.36.92"],"userAgent":"kubectl/v1.17.4 (darwin/amd64) kubernetes/8d8aa39","objectRef":{"resource":"pods","namespace":"alcide","name":"pod-1","apiVersion":"v1"},"responseStatus":{"metadata":{},"code":201},"requestReceivedTimestamp":"2020-04-08T16:32:36.125094Z","stageTimestamp":"2020-04-08T16:32:36.136498Z","annotations":{"authorization.k8s.io/decision":"allow","authorization.k8s.io/reason":""}} 2 | {"kind":"Event","apiVersion":"audit.k8s.io/v1","level":"Request","auditID":"1a7c78f0-bc92-44ff-bb50-87f61241db73","stage":"ResponseComplete","requestURI":"/api/v1/namespaces/ns2/configmaps","verb":"create","user":{"username":"diana","uid":"diana","groups":["system:authenticated"]},"sourceIPs":["98.207.36.92"],"userAgent":"kubectl/v1.17.4 (darwin/amd64) kubernetes/8d8aa39","objectRef":{"resource":"configmaps","namespace":"alcide","name":"cm-2","apiVersion":"v1"},"responseStatus":{"metadata":{},"code":201},"requestObject":{"kind":"ConfigMap","apiVersion":"v1","metadata":{"name":"cm-2","namespace":"ns2","creationTimestamp":null,"annotations":{"kubectl.kubernetes.io/last-applied-configuration":"{\"apiVersion\":\"v1\",\"data\":{\"secret\":\"\\\"xxx\\\"\"},\"kind\":\"ConfigMap\",\"metadata\":{\"annotations\":{},\"name\":\"cm-2\",\"namespace\":\"ns2\"}}\n"}},"data":{"secret":"\"xxx\""}},"requestReceivedTimestamp":"2020-04-08T16:32:36.399597Z","stageTimestamp":"2020-04-08T16:32:36.403499Z","annotations":{"authorization.k8s.io/decision":"allow","authorization.k8s.io/reason":""}} 3 | {"kind":"Event","apiVersion":"audit.k8s.io/v1","level":"RequestResponse","auditID":"b46bced3-5c91-4159-9657-f7b4322b884e","stage":"ResponseComplete","requestURI":"/api/v1/namespaces/ns3/secrets","verb":"create","user":{"username":"diana","uid":"diana","groups":["system:authenticated"]},"sourceIPs":["98.207.36.92"],"userAgent":"kubectl/v1.17.4 (darwin/amd64) kubernetes/8d8aa39","objectRef":{"resource":"secrets","namespace":"alcide","name":"secret-3","apiVersion":"v1"},"responseStatus":{"metadata":{},"code":201},"requestObject":{"kind":"Secret","apiVersion":"v1","metadata":{"name":"secret-3","namespace":"ns3","creationTimestamp":null,"annotations":{"kubectl.kubernetes.io/last-applied-configuration":"{\"apiVersion\":\"v1\",\"data\":{\"secret\":\"eXl5Cg==\"},\"kind\":\"Secret\",\"metadata\":{\"annotations\":{},\"name\":\"secret-3\",\"namespace\":\"ns3\"},\"type\":\"Opaque\"}\n"}},"data":{"secret":"eXl5Cg=="},"type":"Opaque"},"responseObject":{"kind":"Secret","apiVersion":"v1","metadata":{"name":"secret-3","namespace":"ns3","selfLink":"/api/v1/namespaces/ns3/secrets/secret-3","uid":"8e4f4dd6-79b6-11ea-a056-0a39f00d8287","resourceVersion":"134185","creationTimestamp":"2020-04-08T16:32:36Z","annotations":{"kubectl.kubernetes.io/last-applied-configuration":"{\"apiVersion\":\"v1\",\"data\":{\"secret\":\"eXl5Cg==\"},\"kind\":\"Secret\",\"metadata\":{\"annotations\":{},\"name\":\"secret-3\",\"namespace\":\"ns3\"},\"type\":\"Opaque\"}\n"}},"data":{"secret":"eXl5Cg=="},"type":"Opaque"},"requestReceivedTimestamp":"2020-04-08T16:32:36.678782Z","stageTimestamp":"2020-04-08T16:32:36.682243Z","annotations":{"authorization.k8s.io/decision":"allow","authorization.k8s.io/reason":""}} -------------------------------------------------------------------------------- /testdata/policyrules/multiple-role-bindings.yaml: -------------------------------------------------------------------------------- 1 | # 2 | # Install: 3 | # kubectl apply -f testdata/policyrules/multiple-role-bindings.yaml 4 | # 5 | # Run: 6 | # bin/rbac-tool policy-rules -e the-test-user | grep the-test-user 7 | # 8 | # Expect: 9 | # 10 | # ServiceAccount | the-test-user | get | policyrules | core | * | | | Roles>>policyrules/some-rules 11 | # ServiceAccount | the-test-user | get | policyrules | core | * | | | Roles>>policyrules/more-rules 12 | # ServiceAccount | the-test-user | get | policyrules | core | secrets | some-secret | | Roles>>policyrules/some-rules 13 | # ServiceAccount | the-test-user | get | policyrules | core | secrets | | | Roles>>policyrules/more-rules 14 | # ServiceAccount | the-test-user | list | policyrules | core | secrets | some-secret | | Roles>>policyrules/some-rules 15 | # ServiceAccount | the-test-user | watch | policyrules | core | secrets | some-secret | | Roles>>policyrules/some-rules 16 | # 17 | apiVersion: rbac.authorization.k8s.io/v1 18 | kind: Role 19 | metadata: 20 | namespace: policyrules 21 | name: some-rules 22 | rules: 23 | - apiGroups: [""] # "" indicates the core API group 24 | resources: ["secrets"] 25 | resourceNames: ["some-secret"] 26 | verbs: ["get", "watch", "list"] 27 | - apiGroups: [""] # "" indicates the core API group 28 | resources: ["*"] 29 | verbs: ["get"] 30 | 31 | --- 32 | apiVersion: rbac.authorization.k8s.io/v1 33 | kind: Role 34 | metadata: 35 | namespace: policyrules 36 | name: more-rules 37 | rules: 38 | - apiGroups: [""] # "" indicates the core API group 39 | resources: ["secrets"] 40 | verbs: ["get"] 41 | - apiGroups: [""] # "" indicates the core API group 42 | resources: ["*"] 43 | verbs: ["get"] 44 | 45 | --- 46 | apiVersion: rbac.authorization.k8s.io/v1 47 | # This role binding allows "jane" to read pods in the "default" namespace. 48 | # You need to already have a Role named "pod-reader" in that namespace. 49 | kind: RoleBinding 50 | metadata: 51 | name: some-rules-binding 52 | namespace: policyrules 53 | subjects: 54 | - kind: ServiceAccount 55 | name: the-test-user # "name" is case sensitive 56 | namespace: policyrules 57 | roleRef: 58 | # "roleRef" specifies the binding to a Role / ClusterRole 59 | kind: Role #this must be Role or ClusterRole 60 | name: some-rules # this must match the name of the Role or ClusterRole you wish to bind to 61 | apiGroup: rbac.authorization.k8s.io 62 | 63 | --- 64 | apiVersion: rbac.authorization.k8s.io/v1 65 | # This role binding allows "jane" to read pods in the "default" namespace. 66 | # You need to already have a Role named "pod-reader" in that namespace. 67 | kind: RoleBinding 68 | metadata: 69 | name: more-rules-binding 70 | namespace: policyrules 71 | subjects: 72 | - kind: ServiceAccount 73 | name: the-test-user # "name" is case sensitive 74 | namespace: policyrules 75 | roleRef: 76 | # "roleRef" specifies the binding to a Role / ClusterRole 77 | kind: Role #this must be Role or ClusterRole 78 | name: more-rules # this must match the name of the Role or ClusterRole you wish to bind to 79 | apiGroup: rbac.authorization.k8s.io -------------------------------------------------------------------------------- /testdata/viz/multiple-rolebindings-in-ns-missing-service-account.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Namespace 4 | metadata: 5 | name: mynamespace 6 | #--- 7 | 8 | #apiVersion: v1 9 | #kind: ServiceAccount 10 | #metadata: 11 | # name: example-user 12 | # namespace: mynamespace 13 | 14 | --- 15 | kind: Role 16 | apiVersion: rbac.authorization.k8s.io/v1 17 | metadata: 18 | namespace: mynamespace 19 | name: example-role-1 20 | rules: 21 | - apiGroups: [""] 22 | resources: ["pods"] 23 | verbs: ["get", "watch", "list"] 24 | 25 | --- 26 | kind: Role 27 | apiVersion: rbac.authorization.k8s.io/v1 28 | metadata: 29 | namespace: mynamespace 30 | name: example-role-2 31 | rules: 32 | - apiGroups: [""] 33 | resources: ["services"] 34 | verbs: ["get", "watch", "list"] 35 | 36 | --- 37 | kind: RoleBinding 38 | apiVersion: rbac.authorization.k8s.io/v1 39 | metadata: 40 | name: example-rolebinding-1 41 | namespace: mynamespace 42 | subjects: 43 | - kind: ServiceAccount 44 | name: example-user 45 | roleRef: 46 | kind: Role 47 | name: example-role-1 48 | apiGroup: rbac.authorization.k8s.io 49 | 50 | --- 51 | kind: RoleBinding 52 | apiVersion: rbac.authorization.k8s.io/v1 53 | metadata: 54 | name: example-rolebinding-2 55 | namespace: mynamespace 56 | subjects: 57 | - kind: ServiceAccount 58 | name: example-user 59 | roleRef: 60 | kind: Role 61 | name: example-role-2 62 | apiGroup: rbac.authorization.k8s.io -------------------------------------------------------------------------------- /testdata/viz/multiple-rolebindings-in-ns.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Namespace 4 | metadata: 5 | name: mynamespace 6 | --- 7 | apiVersion: v1 8 | kind: ServiceAccount 9 | metadata: 10 | name: example-user 11 | namespace: mynamespace 12 | 13 | --- 14 | kind: Role 15 | apiVersion: rbac.authorization.k8s.io/v1 16 | metadata: 17 | namespace: mynamespace 18 | name: example-role-1 19 | rules: 20 | - apiGroups: [""] 21 | resources: ["pods"] 22 | verbs: ["get", "watch", "list"] 23 | 24 | --- 25 | kind: Role 26 | apiVersion: rbac.authorization.k8s.io/v1 27 | metadata: 28 | namespace: mynamespace 29 | name: example-role-2 30 | rules: 31 | - apiGroups: [""] 32 | resources: ["services"] 33 | verbs: ["get", "watch", "list"] 34 | 35 | --- 36 | kind: RoleBinding 37 | apiVersion: rbac.authorization.k8s.io/v1 38 | metadata: 39 | name: example-rolebinding-1 40 | namespace: mynamespace 41 | subjects: 42 | - kind: ServiceAccount 43 | name: example-user 44 | roleRef: 45 | kind: Role 46 | name: example-role-1 47 | apiGroup: rbac.authorization.k8s.io 48 | 49 | --- 50 | kind: RoleBinding 51 | apiVersion: rbac.authorization.k8s.io/v1 52 | metadata: 53 | name: example-rolebinding-2 54 | namespace: mynamespace 55 | subjects: 56 | - kind: ServiceAccount 57 | name: example-user 58 | roleRef: 59 | kind: Role 60 | name: example-role-2 61 | apiGroup: rbac.authorization.k8s.io -------------------------------------------------------------------------------- /testdata/viz/rbac-with-psp.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Namespace 4 | metadata: 5 | name: mynamespace 6 | --- 7 | apiVersion: v1 8 | kind: ServiceAccount 9 | metadata: 10 | name: example-user 11 | namespace: mynamespace 12 | 13 | --- 14 | kind: Role 15 | apiVersion: rbac.authorization.k8s.io/v1 16 | metadata: 17 | namespace: mynamespace 18 | name: example-role-1 19 | rules: 20 | - apiGroups: [""] 21 | resources: ["pods"] 22 | verbs: ["get", "watch", "list"] 23 | 24 | --- 25 | kind: Role 26 | apiVersion: rbac.authorization.k8s.io/v1 27 | metadata: 28 | namespace: mynamespace 29 | name: example-role-2 30 | rules: 31 | - apiGroups: [""] 32 | resources: ["services"] 33 | verbs: ["get", "watch", "list"] 34 | 35 | --- 36 | kind: RoleBinding 37 | apiVersion: rbac.authorization.k8s.io/v1 38 | metadata: 39 | name: example-rolebinding-1 40 | namespace: mynamespace 41 | subjects: 42 | - kind: ServiceAccount 43 | name: example-user 44 | roleRef: 45 | kind: Role 46 | name: example-role-1 47 | apiGroup: rbac.authorization.k8s.io 48 | 49 | --- 50 | kind: RoleBinding 51 | apiVersion: rbac.authorization.k8s.io/v1 52 | metadata: 53 | name: example-rolebinding-2 54 | namespace: mynamespace 55 | subjects: 56 | - kind: ServiceAccount 57 | name: example-user 58 | roleRef: 59 | kind: Role 60 | name: example-role-2 61 | apiGroup: rbac.authorization.k8s.io 62 | --- 63 | kind: RoleBinding 64 | apiVersion: rbac.authorization.k8s.io/v1 65 | metadata: 66 | name: example-psp-bind 67 | namespace: mynamespace 68 | subjects: 69 | - kind: ServiceAccount 70 | name: example-user 71 | roleRef: 72 | kind: ClusterRole 73 | name: priviliged-psp-role 74 | apiGroup: rbac.authorization.k8s.io 75 | --- 76 | apiVersion: rbac.authorization.k8s.io/v1 77 | kind: ClusterRole 78 | metadata: 79 | name: priviliged-psp-role 80 | rules: 81 | - apiGroups: ['policy'] 82 | resources: ['podsecuritypolicies'] 83 | verbs: ['use'] 84 | resourceNames: 85 | - mypsp 86 | --- 87 | kind: RoleBinding 88 | apiVersion: rbac.authorization.k8s.io/v1 89 | metadata: 90 | name: all-psps-bind 91 | namespace: mynamespace 92 | subjects: 93 | - kind: ServiceAccount 94 | name: example-user 95 | roleRef: 96 | kind: ClusterRole 97 | name: all-psps 98 | apiGroup: rbac.authorization.k8s.io 99 | --- 100 | apiVersion: rbac.authorization.k8s.io/v1 101 | kind: ClusterRole 102 | metadata: 103 | name: all-psps 104 | rules: 105 | - apiGroups: ['policy'] 106 | resources: ['podsecuritypolicies'] 107 | verbs: ['use'] 108 | resourceNames: 109 | - '*' 110 | --- 111 | apiVersion: policy/v1beta1 112 | kind: PodSecurityPolicy 113 | metadata: 114 | name: mypsp 115 | annotations: 116 | seccomp.security.alpha.kubernetes.io/allowedProfileNames: '*' 117 | spec: 118 | privileged: true 119 | allowPrivilegeEscalation: true 120 | allowedCapabilities: 121 | - '*' 122 | volumes: 123 | - '*' 124 | hostNetwork: true 125 | hostPorts: 126 | - min: 0 127 | max: 65535 128 | hostIPC: true 129 | hostPID: true 130 | runAsUser: 131 | rule: 'RunAsAny' 132 | seLinux: 133 | rule: 'RunAsAny' 134 | supplementalGroups: 135 | rule: 'RunAsAny' 136 | fsGroup: 137 | rule: 'RunAsAny' 138 | 139 | --- 140 | apiVersion: policy/v1beta1 141 | kind: PodSecurityPolicy 142 | metadata: 143 | name: another-psp 144 | annotations: 145 | seccomp.security.alpha.kubernetes.io/allowedProfileNames: '*' 146 | spec: 147 | privileged: false 148 | allowPrivilegeEscalation: true 149 | volumes: 150 | - emptyDir 151 | runAsUser: 152 | rule: 'MustRunAsNonRoot' 153 | seLinux: 154 | rule: 'RunAsAny' 155 | supplementalGroups: 156 | rule: 'RunAsAny' 157 | fsGroup: 158 | rule: 'RunAsAny' 159 | 160 | -------------------------------------------------------------------------------- /testdata/whocan/clusterrole-aggregate.yaml: -------------------------------------------------------------------------------- 1 | # 2 | # Install: 3 | # kubectl apply -f testdata/whocan/clusterrole-aggregate.yaml 4 | # 5 | # Run: 6 | # bin/rbac-tool who-can get pod | grep test-aggregate 7 | # bin/rbac-tool who-can create pod | grep test-aggregate 8 | # 9 | # Expect: 10 | # 11 | # Group | test-aggregate-group | 12 | # ServiceAccount | test-aggregate-sa | test 13 | # User | test-aggregate-user | 14 | # 15 | apiVersion: rbac.authorization.k8s.io/v1 16 | kind: ClusterRole 17 | metadata: 18 | name: test-aggregate 19 | aggregationRule: 20 | clusterRoleSelectors: 21 | - matchLabels: 22 | rbac.example.com/aggregate-test: "true" 23 | rules: [] # The control plane automatically fills in the rules 24 | 25 | --- 26 | 27 | apiVersion: rbac.authorization.k8s.io/v1 28 | kind: ClusterRole 29 | metadata: 30 | name: test-clusterrole-reader 31 | labels: 32 | rbac.example.com/aggregate-test: "true" 33 | # When you create the "monitoring-endpoints" ClusterRole, 34 | # the rules below will be added to the "monitoring" ClusterRole. 35 | rules: 36 | - apiGroups: [""] 37 | resources: ["pods"] 38 | verbs: ["get", "list", "watch"] 39 | 40 | --- 41 | 42 | apiVersion: rbac.authorization.k8s.io/v1 43 | kind: ClusterRole 44 | metadata: 45 | name: test-clusterrole-create 46 | labels: 47 | rbac.example.com/aggregate-test: "true" 48 | # When you create the "monitoring-endpoints" ClusterRole, 49 | # the rules below will be added to the "monitoring" ClusterRole. 50 | rules: 51 | - apiGroups: [""] 52 | resources: ["pods"] 53 | verbs: ["create"] 54 | 55 | --- 56 | apiVersion: rbac.authorization.k8s.io/v1 57 | # This role binding allows "jane" to read pods in the "default" namespace. 58 | # You need to already have a Role named "pod-reader" in that namespace. 59 | kind: ClusterRoleBinding 60 | metadata: 61 | name: aggregate-pod-creator 62 | subjects: 63 | # You can specify more than one "subject" 64 | - kind: User 65 | name: test-aggregate-user # "name" is case sensitive 66 | apiGroup: rbac.authorization.k8s.io 67 | - kind: Group 68 | name: test-aggregate-group # "name" is case sensitive 69 | apiGroup: rbac.authorization.k8s.io 70 | - kind: ServiceAccount 71 | name: test-aggregate-sa # "name" is case sensitive 72 | namespace: test 73 | 74 | 75 | roleRef: 76 | # "roleRef" specifies the binding to a Role / ClusterRole 77 | kind: ClusterRole #this must be Role or ClusterRole 78 | name: test-aggregate # this must match the name of the Role or ClusterRole you wish to bind to 79 | apiGroup: rbac.authorization.k8s.io -------------------------------------------------------------------------------- /testdata/whocan/gatewat-api-operator.yaml: -------------------------------------------------------------------------------- 1 | # 2 | # Install: 3 | # kubectl apply -f testdata/whocan/gatewat-api-operator.yaml 4 | # 5 | # Run: 6 | # bin/rbac-tool who-can get gateways | grep gateway-network-operator 7 | # 8 | # Expect: 9 | # 10 | # ServiceAccount | gateway-network-operator-sa | test 11 | # User | gateway-network-operator-user | 12 | # 13 | apiVersion: rbac.authorization.k8s.io/v1 14 | kind: ClusterRole 15 | metadata: 16 | name: gateway-network-operator-role 17 | rules: 18 | - apiGroups: ["gateway.networking.k8s.io"] 19 | resources: ["*"] 20 | verbs: ["create", "update", "delete", "patch"] 21 | 22 | --- 23 | apiVersion: rbac.authorization.k8s.io/v1 24 | # This role binding allows "jane" to read pods in the "default" namespace. 25 | # You need to already have a Role named "pod-reader" in that namespace. 26 | kind: RoleBinding 27 | metadata: 28 | name: gateway-network-operator 29 | namespace: test 30 | subjects: 31 | # You can specify more than one "subject" 32 | - kind: User 33 | name: gateway-network-operator-user # "name" is case sensitive 34 | apiGroup: rbac.authorization.k8s.io # You can specify more than one "subject" 35 | - kind: ServiceAccount 36 | name: gateway-network-operator-sa # "name" is case sensitive 37 | namespace: test 38 | roleRef: 39 | # "roleRef" specifies the binding to a Role / ClusterRole 40 | kind: ClusterRole #this must be Role or ClusterRole 41 | name: gateway-network-operator-role # this must match the name of the Role or ClusterRole you wish to bind to 42 | apiGroup: rbac.authorization.k8s.io 43 | --- 44 | apiVersion: v1 45 | kind: ServiceAccount 46 | metadata: 47 | name: gateway-network-operator-sa 48 | namespace: test -------------------------------------------------------------------------------- /testdata/whocan/impersonator.yaml: -------------------------------------------------------------------------------- 1 | # 2 | # Install: 3 | # kubectl apply -f testdata/whocan/impersonator.yaml 4 | # 5 | # Run: 6 | # bin/rbac-tool who-can impersonate serviceaccounts | grep impersonator 7 | 8 | # 9 | # Expect: 10 | # 11 | # User | test-impersonator-user | | 12 | # 13 | apiVersion: rbac.authorization.k8s.io/v1 14 | kind: ClusterRole 15 | metadata: 16 | name: impersonator-role 17 | rules: 18 | - apiGroups: [""] 19 | resources: ["users", "groups", "serviceaccounts"] 20 | verbs: ["impersonate"] 21 | 22 | --- 23 | apiVersion: rbac.authorization.k8s.io/v1 24 | # This role binding allows "jane" to read pods in the "default" namespace. 25 | # You need to already have a Role named "pod-reader" in that namespace. 26 | kind: RoleBinding 27 | metadata: 28 | name: impersonator 29 | namespace: test 30 | subjects: 31 | # You can specify more than one "subject" 32 | - kind: User 33 | name: test-impersonator-user # "name" is case sensitive 34 | apiGroup: rbac.authorization.k8s.io 35 | 36 | 37 | roleRef: 38 | # "roleRef" specifies the binding to a Role / ClusterRole 39 | kind: ClusterRole #this must be Role or ClusterRole 40 | name: impersonator-role # this must match the name of the Role or ClusterRole you wish to bind to 41 | apiGroup: rbac.authorization.k8s.io 42 | -------------------------------------------------------------------------------- /testdata/whocan/nonresourceurl-reader.yaml: -------------------------------------------------------------------------------- 1 | # 2 | # Install: 3 | # kubectl apply -f testdata/whocan/nonresourceurl-reader.yaml 4 | # 5 | # Run: 6 | # bin/rbac-tool who-can get /test-reader 7 | # 8 | # Expect: 9 | # 10 | # TYPE | SUBJECT | NAMESPACE 11 | # +-------+----------------------------+-----------+ 12 | # Group | system:masters | 13 | # User | test-nonresourceurl-reader | 14 | # 15 | apiVersion: rbac.authorization.k8s.io/v1 16 | kind: ClusterRole 17 | metadata: 18 | name: nonresourceurl-reader 19 | rules: 20 | - verbs: ["get", "watch", "list"] 21 | nonResourceURLs: 22 | - /test-reader 23 | 24 | --- 25 | apiVersion: rbac.authorization.k8s.io/v1 26 | # This role binding allows "jane" to read pods in the "default" namespace. 27 | # You need to already have a Role named "pod-reader" in that namespace. 28 | kind: RoleBinding 29 | metadata: 30 | name: read-nonresourceurl 31 | namespace: test 32 | subjects: 33 | # You can specify more than one "subject" 34 | - kind: User 35 | name: test-nonresourceurl-reader # "name" is case sensitive 36 | apiGroup: rbac.authorization.k8s.io 37 | roleRef: 38 | # "roleRef" specifies the binding to a Role / ClusterRole 39 | kind: ClusterRole #this must be Role or ClusterRole 40 | name: nonresourceurl-reader # this must match the name of the Role or ClusterRole you wish to bind to 41 | apiGroup: rbac.authorization.k8s.io -------------------------------------------------------------------------------- /testdata/whocan/pod-creator.yaml: -------------------------------------------------------------------------------- 1 | # 2 | # Install: 3 | # kubectl apply -f testdata/whocan/pod-creator.yaml 4 | # 5 | # Run: 6 | # bin/rbac-tool who-can create pods | grep pod-creator 7 | 8 | # 9 | # Expect: 10 | # 11 | # Group | test-pod-creator-group | 12 | # ServiceAccount | test-pod-creator-sa | test 13 | # User | test-pod-creator-user | 14 | # 15 | apiVersion: rbac.authorization.k8s.io/v1 16 | kind: Role 17 | metadata: 18 | namespace: test 19 | name: pod-creator 20 | rules: 21 | - apiGroups: [""] # "" indicates the core API group 22 | resources: ["pods"] 23 | verbs: ["create", "update"] 24 | 25 | --- 26 | apiVersion: rbac.authorization.k8s.io/v1 27 | # This role binding allows "jane" to read pods in the "default" namespace. 28 | # You need to already have a Role named "pod-reader" in that namespace. 29 | kind: RoleBinding 30 | metadata: 31 | name: pod-creator 32 | namespace: test 33 | subjects: 34 | # You can specify more than one "subject" 35 | - kind: User 36 | name: test-pod-creator-user # "name" is case sensitive 37 | apiGroup: rbac.authorization.k8s.io 38 | - kind: Group 39 | name: test-pod-creator-group # "name" is case sensitive 40 | apiGroup: rbac.authorization.k8s.io 41 | - kind: ServiceAccount 42 | name: test-pod-creator-sa # "name" is case sensitive 43 | namespace: test 44 | 45 | 46 | roleRef: 47 | # "roleRef" specifies the binding to a Role / ClusterRole 48 | kind: Role #this must be Role or ClusterRole 49 | name: pod-creator # this must match the name of the Role or ClusterRole you wish to bind to 50 | apiGroup: rbac.authorization.k8s.io -------------------------------------------------------------------------------- /testdata/whocan/secret-reader.yaml: -------------------------------------------------------------------------------- 1 | # 2 | # Install: 3 | # kubectl apply -f testdata/whocan/secret-reader.yaml 4 | # 5 | # Run: 6 | # bin/rbac-tool who-can get secrets | grep test-secret-reader 7 | # 8 | # Expect: 9 | # 10 | # ServiceAccount | test-secret-reader-sa | test 11 | # User | test-secret-reader | 12 | # 13 | apiVersion: rbac.authorization.k8s.io/v1 14 | kind: ClusterRole 15 | metadata: 16 | name: test-secret-reader 17 | rules: 18 | - apiGroups: [""] # "" indicates the core API group 19 | resources: ["secrets"] 20 | verbs: ["get", "watch", "list"] 21 | 22 | --- 23 | apiVersion: rbac.authorization.k8s.io/v1 24 | # This role binding allows "jane" to read pods in the "default" namespace. 25 | # You need to already have a Role named "pod-reader" in that namespace. 26 | kind: RoleBinding 27 | metadata: 28 | name: read-secrets 29 | namespace: test 30 | subjects: 31 | # You can specify more than one "subject" 32 | - kind: User 33 | name: test-secret-reader # "name" is case sensitive 34 | apiGroup: rbac.authorization.k8s.io 35 | - kind: ServiceAccount 36 | name: test-secret-reader-sa # "name" is case sensitive 37 | namespace: test 38 | roleRef: 39 | # "roleRef" specifies the binding to a Role / ClusterRole 40 | kind: ClusterRole #this must be Role or ClusterRole 41 | name: test-secret-reader # this must match the name of the Role or ClusterRole you wish to bind to 42 | apiGroup: rbac.authorization.k8s.io -------------------------------------------------------------------------------- /testdata/whocan/specific-resource-reader.yaml: -------------------------------------------------------------------------------- 1 | # 2 | # Install: 3 | # kubectl apply -f testdata/whocan/specific-resource-reader.yaml 4 | # 5 | # Run: 6 | # bin/rbac-tool who-can get secrets/some-secret | grep test-specific-secret-reader 7 | # 8 | # Expect: 9 | # 10 | # ServiceAccount | test-specific-secret-reader-sa | test 11 | # User | test-specific-secret-reader | 12 | # 13 | apiVersion: rbac.authorization.k8s.io/v1 14 | kind: Role 15 | metadata: 16 | namespace: test 17 | name: specific-secret-reader 18 | rules: 19 | - apiGroups: [""] # "" indicates the core API group 20 | resources: ["secrets"] 21 | resourceNames: ["some-secret"] 22 | verbs: ["get", "watch", "list"] 23 | 24 | --- 25 | apiVersion: rbac.authorization.k8s.io/v1 26 | # This role binding allows "jane" to read pods in the "default" namespace. 27 | # You need to already have a Role named "pod-reader" in that namespace. 28 | kind: RoleBinding 29 | metadata: 30 | name: read-specific-secrets 31 | namespace: test 32 | subjects: 33 | # You can specify more than one "subject" 34 | - kind: User 35 | name: test-specific-secret-reader # "name" is case sensitive 36 | apiGroup: rbac.authorization.k8s.io 37 | - kind: ServiceAccount 38 | name: test-specific-secret-reader-sa # "name" is case sensitive 39 | namespace: test 40 | roleRef: 41 | # "roleRef" specifies the binding to a Role / ClusterRole 42 | kind: Role #this must be Role or ClusterRole 43 | name: specific-secret-reader # this must match the name of the Role or ClusterRole you wish to bind to 44 | apiGroup: rbac.authorization.k8s.io --------------------------------------------------------------------------------