├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── access-graph.go ├── export.go ├── iam.go ├── interaction.go ├── k8s.go ├── k8s_types.go ├── main.go └── site ├── docs ├── getting-started.md ├── img │ ├── cclfb.png │ ├── iam-concept.png │ ├── iam-example.png │ ├── iam-rbac-example.png │ ├── rbac-concept.png │ ├── rbac-example.png │ ├── w_iam_policies.png │ ├── w_iam_roles.png │ ├── w_iam_user.png │ ├── w_k8s_pods.png │ ├── w_k8s_sa_secret.png │ ├── w_startup.png │ ├── w_toplevelmenu.png │ └── w_tracing.png ├── index.md └── terminology.md ├── mkdocs.yml └── theme └── assets └── images └── favicon.png /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | /.DS_Store 14 | .DS_Store 15 | /site/site 16 | /release 17 | rbiam*.json 18 | rbiam*.dot 19 | -------------------------------------------------------------------------------- /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 | rbiam_version:= v0.3 2 | 3 | .PHONY: build 4 | 5 | build: 6 | GOOS=darwin GOARCH=amd64 go build -ldflags "-X main.Version=${rbiam_version}" -o release/rbiam-macos . 7 | GOOS=linux GOARCH=amd64 go build -ldflags "-X main.Version=${rbiam_version}" -o release/rbiam-linux . 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rbIAM 2 | 3 | A unified AWS IAM & Kubernetes RBAC access control exploration tool. 4 | 5 | 6 | -------------------------------------------------------------------------------- /access-graph.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/aws/aws-sdk-go-v2/aws" 8 | "github.com/aws/aws-sdk-go-v2/service/iam" 9 | "github.com/aws/aws-sdk-go-v2/service/sts" 10 | ) 11 | 12 | // AccessGraph represents the combined IAM and RBAC access control regime 13 | // found in Kubernetes on AWS. It includes IAM users, roles and AWS service 14 | // with temporary credentials (STS) as well as Kubernetes service accounts 15 | // and users. See also: 16 | // https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_identifiers.html 17 | // https://kubernetes.io/docs/reference/access-authn-authz/service-accounts-admin/ 18 | type AccessGraph struct { 19 | // Caller is the caller of an AWS service (STS). 20 | Caller *sts.GetCallerIdentityOutput 21 | // User is the AWS user (IAM). 22 | User *iam.User 23 | // KubeConfig is the Kubernetes client configuration. 24 | KubeConfig *Config 25 | // Roles is the collection of all IAM roles pertinent to user/caller. 26 | Roles map[string]iam.Role 27 | // Policies is the collection of all IAM policies pertinent to user/caller. 28 | Policies map[string]iam.Policy 29 | // ServiceAccounts is the collection of all service accounts in the 30 | // Kubernetes cluster. 31 | ServiceAccounts map[string]ServiceAccount 32 | // Secrets is the collection of all secrets in the Kubernetes cluster. 33 | Secrets map[string]Secret 34 | // Pods is the collection of all pods in the Kubernetes cluster. 35 | Pods map[string]Pod 36 | } 37 | 38 | // NewAccessGraph a new access graph for the currently authenticated AWS user, 39 | // retrieving IAM-related as well as Kubernetes-related info. We try to be as 40 | // graceful as possbile here but if the IAM queries fail, there's no point in 41 | // continuing and we exit early. 42 | func NewAccessGraph(cfg aws.Config) *AccessGraph { 43 | ag := &AccessGraph{} 44 | err := ag.user(cfg) 45 | if err != nil { 46 | fmt.Printf("Can't get user: %v", err.Error()) 47 | os.Exit(2) 48 | } 49 | err = ag.callerIdentity(cfg) 50 | if err != nil { 51 | fmt.Printf("Can't get caller identity: %v", err.Error()) 52 | os.Exit(2) 53 | } 54 | err = ag.roles(cfg) 55 | if err != nil { 56 | fmt.Printf("Can't get roles: %v", err.Error()) 57 | os.Exit(2) 58 | } 59 | err = ag.policies(cfg) 60 | if err != nil { 61 | fmt.Printf("Can't get policies: %v", err.Error()) 62 | os.Exit(2) 63 | } 64 | err = ag.kubeIdentity() 65 | if err != nil { 66 | fmt.Printf("Can't get Kubernetes identity: %v", err.Error()) 67 | } 68 | err = ag.kubeServiceAccounts() 69 | if err != nil { 70 | fmt.Printf("Can't get Kubernetes service accounts: %v", err.Error()) 71 | } 72 | err = ag.kubeSecrets() 73 | if err != nil { 74 | fmt.Printf("Can't get Kubernetes secrets: %v", err.Error()) 75 | } 76 | err = ag.kubePods() 77 | if err != nil { 78 | fmt.Printf("Can't get Kubernetes pods: %v", err.Error()) 79 | } 80 | return ag 81 | } 82 | 83 | // String provides a textual rendering of the access graph 84 | func (ag *AccessGraph) String() string { 85 | return fmt.Sprintf( 86 | "User: %v\n"+ 87 | "STS caller identity: %v\n"+ 88 | "EKS roles: %v\n"+ 89 | "EKS policies: %v\n"+ 90 | "Kube context: %+v\n"+ 91 | "Kube service accounts: %+v\n"+ 92 | "Kube secrets: %+v\n"+ 93 | "Kube pods: %+v\n", 94 | ag.User, 95 | ag.Caller, 96 | ag.Roles, 97 | ag.Policies, 98 | ag.KubeConfig.CurrentContext, 99 | ag.ServiceAccounts, 100 | ag.Secrets, 101 | ag.Pods, 102 | ) 103 | } 104 | -------------------------------------------------------------------------------- /export.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "strings" 8 | "time" 9 | 10 | "github.com/emicklei/dot" 11 | ) 12 | 13 | // dump exports the entire access graph. 14 | func dump(ag *AccessGraph) error { 15 | b, err := json.Marshal(ag) 16 | if err != nil { 17 | return err 18 | } 19 | filename := fmt.Sprintf("rbiam-dump-%v.json", time.Now().Unix()) 20 | err = ioutil.WriteFile(filename, b, 0644) 21 | return err 22 | } 23 | 24 | // load imports access graph from filename 25 | func load(filename string) (*AccessGraph, error) { 26 | ag := &AccessGraph{} 27 | b, err := ioutil.ReadFile(filename) 28 | if err != nil { 29 | return ag, err 30 | } 31 | err = json.Unmarshal(b, ag) 32 | return ag, err 33 | } 34 | 35 | // exportRaw exports the trace as a raw dump in JSON format into a file 36 | // in the current working directory with a name of 'rbiam-trace-NNNNNNNNNN' with 37 | // the NNNNNNNNNN being the Unix timestamp of the creation time, for example: 38 | // rbiam-trace-1564315687.json 39 | func exportRaw(trace []string, ag *AccessGraph) (string, error) { 40 | dump := "" 41 | for _, item := range trace { 42 | itype, ikey := extractTK(item) 43 | switch itype { 44 | case "IAM role": 45 | b, err := json.Marshal(ag.Roles[ikey]) 46 | if err != nil { 47 | return "", err 48 | } 49 | dump = fmt.Sprintf("%v\n%v", dump, string(b)) 50 | case "IAM policy": 51 | b, err := json.Marshal(ag.Policies[ikey]) 52 | if err != nil { 53 | return "", err 54 | } 55 | dump = fmt.Sprintf("%v\n%v", dump, string(b)) 56 | case "Kubernetes service account": 57 | b, err := json.Marshal(ag.ServiceAccounts[ikey]) 58 | if err != nil { 59 | return "", err 60 | } 61 | dump = fmt.Sprintf("%v\n%v", dump, string(b)) 62 | case "Kubernetes secret": 63 | b, err := json.Marshal(ag.Secrets[ikey]) 64 | if err != nil { 65 | return "", err 66 | } 67 | dump = fmt.Sprintf("%v\n%v", dump, string(b)) 68 | case "Kubernetes pod": 69 | b, err := json.Marshal(ag.Pods[ikey]) 70 | if err != nil { 71 | return "", err 72 | } 73 | dump = fmt.Sprintf("%v\n%v", dump, string(b)) 74 | } 75 | } 76 | 77 | filename := fmt.Sprintf("rbiam-trace-%v.json", time.Now().Unix()) 78 | err := ioutil.WriteFile(filename, []byte(dump), 0644) 79 | if err != nil { 80 | return "", err 81 | } 82 | return filename, nil 83 | } 84 | 85 | // exportGraph exports the trace as a graph in DOT format into a file 86 | // in the current working directory with a name of 'rbiam-trace-NNNNNNNNNN' with 87 | // the NNNNNNNNNN being the Unix timestamp of the creation time, for example: 88 | // rbiam-trace-1564315687.dot 89 | func exportGraph(trace []string, ag *AccessGraph) (string, error) { 90 | g := dot.NewGraph(dot.Directed) 91 | // make sure the legend is at the bottom: 92 | g.Attr("newrank", "true") 93 | // legend: 94 | legend := g.Subgraph("LEGEND", dot.ClusterOption{}) 95 | lsa := formatAsServiceAccount(legend.Node("Kubernetes service account")) 96 | lsecret := formatAsSecret(legend.Node("Kubernetes secret")) 97 | lpod := formatAsPod(legend.Node("Kubernetes pod")) 98 | lrole := formatAsRole(legend.Node("IAM role")) 99 | lpolicy := formatAsPolicy(legend.Node("IAM policy")) 100 | legend.Edge(lpod, lsa, "uses").Attr("fontname", "Helvetica") 101 | legend.Edge(lsa, lsecret, "has").Attr("fontname", "Helvetica") 102 | legend.Edge(lrole, lpolicy, "has").Attr("fontname", "Helvetica") 103 | legend.Edge(lpod, lrole, "assumes").Attr("fontname", "Helvetica") 104 | 105 | // first let's draw the nodes and remember the 106 | // graph entry points for traversals to later draw 107 | // the edges starting with Kubernetes pods and IAM roles: 108 | pods := make(map[string]dot.Node) 109 | sas := make(map[string]dot.Node) 110 | secrets := make(map[string]dot.Node) 111 | roles := make(map[string]dot.Node) 112 | policies := make(map[string]dot.Node) 113 | for _, item := range trace { 114 | itype, ikey := extractTK(item) 115 | switch itype { 116 | case "IAM role": 117 | roles[ikey] = formatAsRole(g.Node(ikey)) 118 | case "IAM policy": 119 | policies[ikey] = formatAsPolicy(g.Node(ikey)) 120 | case "Kubernetes service account": 121 | sas[ikey] = formatAsServiceAccount(g.Node(ikey)) 122 | case "Kubernetes secret": 123 | secrets[ikey] = formatAsSecret(g.Node(ikey)) 124 | case "Kubernetes pod": 125 | pods[ikey] = formatAsPod(g.Node(ikey)) 126 | } 127 | } 128 | 129 | // next, we draw the edges: 130 | // pods -> service accounts 131 | for podname, node := range pods { 132 | for _, item := range trace { 133 | itype, ikey := extractTK(item) 134 | if itype == "Kubernetes service account" { 135 | podsa := namespaceit(ag.Pods[podname].Namespace, ag.Pods[podname].Spec.ServiceAccountName) 136 | if podsa == ikey { 137 | g.Edge(node, sas[ikey]) 138 | } 139 | } 140 | } 141 | } 142 | // service accounts -> secrets 143 | for saname, node := range sas { 144 | for _, item := range trace { 145 | itype, ikey := extractTK(item) 146 | if itype == "Kubernetes secret" { 147 | // for now we simply take the first secret of the service account, should really iterate over all and check each: 148 | sasecrect := namespaceit(ag.ServiceAccounts[saname].Namespace, ag.ServiceAccounts[saname].Secrets[0].Name) 149 | if sasecrect == ikey { 150 | g.Edge(node, secrets[ikey]) 151 | } 152 | } 153 | } 154 | } 155 | // pods -> IAM roles 156 | for podname, node := range pods { 157 | for _, item := range trace { 158 | itype, ikey := extractTK(item) 159 | if itype == "IAM role" { 160 | // for IRP-enabled pods: 161 | for _, container := range ag.Pods[podname].Spec.Containers { 162 | for _, envar := range container.Env { 163 | if envar.Name == "AWS_ROLE_ARN" && envar.Value == ikey { 164 | g.Edge(node, roles[ikey]) 165 | } 166 | } 167 | } 168 | // for traditional, node-level IAM role assignment: 169 | // iterate over EC2 instances and select the ones where the 170 | // pods' hostIP matches, then take the EC2 NodeInstanceRole 171 | } 172 | } 173 | } 174 | 175 | // IAM roles -> IAM policies 176 | // https://godoc.org/github.com/aws/aws-sdk-go-v2/service/iam#Client.ListAttachedRolePoliciesRequest 177 | 178 | // now we can write out the graph into a file in DOT format: 179 | filename := fmt.Sprintf("rbiam-trace-%v.dot", time.Now().Unix()) 180 | err := ioutil.WriteFile(filename, []byte(g.String()), 0644) 181 | if err != nil { 182 | return "", err 183 | } 184 | return filename, nil 185 | } 186 | 187 | // extractTK takes a history item in the form [TYPE] KEY 188 | // and return t as the TYPE and k as the KEY, for example: 189 | // [Kubernetes service account] default:s3-echoer -> 190 | // t == Kubernetes service account 191 | // k == default:s3-echoer 192 | func extractTK(item string) (t, k string) { 193 | t = strings.TrimPrefix(strings.Split(item, "]")[0], "[") 194 | k = strings.TrimSpace(strings.Split(item, "]")[1]) 195 | return 196 | } 197 | 198 | func formatAsRole(n dot.Node) dot.Node { 199 | return n.Attr("style", "filled").Attr("fillcolor", "#FD8564").Attr("fontcolor", "#000000").Attr("fontname", "Helvetica") 200 | } 201 | 202 | func formatAsPolicy(n dot.Node) dot.Node { 203 | return n.Attr("style", "filled").Attr("fillcolor", "#D9A7F1").Attr("fontcolor", "#000000").Attr("fontname", "Helvetica") 204 | } 205 | 206 | func formatAsServiceAccount(n dot.Node) dot.Node { 207 | return n.Attr("style", "filled").Attr("fillcolor", "#1BFF9F").Attr("fontcolor", "#000000").Attr("fontname", "Helvetica") 208 | } 209 | 210 | func formatAsSecret(n dot.Node) dot.Node { 211 | return n.Attr("style", "filled").Attr("fillcolor", "#F9ED49").Attr("fontcolor", "#000000").Attr("fontname", "Helvetica") 212 | } 213 | 214 | func formatAsPod(n dot.Node) dot.Node { 215 | return n.Attr("style", "filled").Attr("fillcolor", "#4260FA").Attr("fontcolor", "#f0f0f0").Attr("fontname", "Helvetica") 216 | } 217 | -------------------------------------------------------------------------------- /iam.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/url" 7 | 8 | "github.com/aws/aws-sdk-go-v2/aws" 9 | "github.com/aws/aws-sdk-go-v2/service/iam" 10 | "github.com/aws/aws-sdk-go-v2/service/sts" 11 | ) 12 | 13 | // user queries IAM to retrieve info on the user issuing the request. 14 | func (ag *AccessGraph) user(cfg aws.Config) error { 15 | svc := iam.New(cfg) 16 | req := svc.GetUserRequest(&iam.GetUserInput{}) 17 | res, err := req.Send(context.Background()) 18 | if err != nil { 19 | return err 20 | } 21 | ag.User = res.User 22 | return nil 23 | } 24 | 25 | // formatCaller provides a textual rendering of the combined IAM user and caller information. 26 | func formatCaller(ag *AccessGraph) string { 27 | user := ag.User 28 | caller := ag.Caller 29 | return fmt.Sprintf( 30 | " Account ID: %v\n"+ 31 | " User name: %v\n"+ 32 | " User ID: %v\n"+ 33 | " Caller ID: %v\n"+ 34 | " Path: %v\n"+ 35 | " Created at: %v\n"+ 36 | " Tags: %v\n", 37 | *caller.Account, 38 | *user.UserName, 39 | *user.UserId, 40 | *caller.UserId, 41 | *user.Path, 42 | user.CreateDate, 43 | user.Tags, 44 | ) 45 | } 46 | 47 | // callerIdentity queries STS to retrieve the identity of the caller. 48 | func (ag *AccessGraph) callerIdentity(cfg aws.Config) error { 49 | svc := sts.New(cfg) 50 | req := svc.GetCallerIdentityRequest(&sts.GetCallerIdentityInput{}) 51 | res, err := req.Send(context.Background()) 52 | if err != nil { 53 | return err 54 | } 55 | ag.Caller = res 56 | return nil 57 | } 58 | 59 | // roles queries IAM for roles in use, related to EKS. 60 | // This is done simply by checking if the role ARN contains EKS or eks. 61 | func (ag *AccessGraph) roles(cfg aws.Config) error { 62 | svc := iam.New(cfg) 63 | req := svc.ListRolesRequest(&iam.ListRolesInput{}) 64 | res, err := req.Send(context.TODO()) 65 | if err != nil { 66 | return err 67 | } 68 | ag.Roles = make(map[string]iam.Role) 69 | for _, role := range res.Roles { 70 | rolearn := *role.Arn 71 | ag.Roles[rolearn] = role 72 | } 73 | return nil 74 | } 75 | 76 | // formatRole provides a textual rendering of a role 77 | func formatRole(role *iam.Role) string { 78 | arpd := "" 79 | u, err := url.QueryUnescape(*role.AssumeRolePolicyDocument) 80 | if err == nil { 81 | arpd = u 82 | } 83 | return fmt.Sprintf( 84 | " Name: %v\n"+ 85 | " ID: %v\n"+ 86 | " Path: %v\n"+ 87 | " Assume role by: %v\n"+ 88 | " Maximum session duration: %v sec\n"+ 89 | " Created at: %v\n"+ 90 | " Tags: %v\n", 91 | *role.RoleName, 92 | *role.RoleId, 93 | *role.Path, 94 | arpd, 95 | *role.MaxSessionDuration, 96 | role.CreateDate, 97 | role.Tags, 98 | ) 99 | } 100 | 101 | // policies queries IAM for attached policies 102 | func (ag *AccessGraph) policies(cfg aws.Config) error { 103 | svc := iam.New(cfg) 104 | req := svc.ListPoliciesRequest(&iam.ListPoliciesInput{OnlyAttached: aws.Bool(true)}) 105 | res, err := req.Send(context.TODO()) 106 | if err != nil { 107 | return err 108 | } 109 | ag.Policies = make(map[string]iam.Policy) 110 | for _, policy := range res.Policies { 111 | policyarn := *policy.Arn 112 | ag.Policies[policyarn] = policy 113 | } 114 | return nil 115 | } 116 | 117 | // formatPolicy provides a textual rendering of a policy. 118 | func formatPolicy(policy *iam.Policy) string { 119 | return fmt.Sprintf( 120 | " Name: %v\n"+ 121 | " ID: %v\n"+ 122 | " Path: %v\n"+ 123 | " Number of entities the policy is attached: %v\n"+ 124 | " Created at: %v\n"+ 125 | " Updated at: %v\n", 126 | *policy.PolicyName, 127 | *policy.PolicyId, 128 | *policy.Path, 129 | *policy.AttachmentCount, 130 | *policy.CreateDate, 131 | *policy.UpdateDate, 132 | ) 133 | } 134 | -------------------------------------------------------------------------------- /interaction.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/c-bata/go-prompt" 5 | ) 6 | 7 | // toplevel represents the top level choices in the interaction. 8 | func toplevel(d prompt.Document) []prompt.Suggest { 9 | s := []prompt.Suggest{ 10 | {Text: "iam-user", Description: "Describe calling AWS IAM user"}, 11 | {Text: "iam-roles", Description: "Select an AWS IAM role to explore"}, 12 | {Text: "iam-policies", Description: "Select an AWS IAM policy to explore"}, 13 | {Text: "k8s-sa", Description: "Select an Kubernetes service account to explore"}, 14 | {Text: "k8s-secrets", Description: "Select a Kubernetes secret to explore"}, 15 | {Text: "k8s-pods", Description: "Select a Kubernetes pod to explore"}, 16 | {Text: "history", Description: "Show the history of selected items"}, 17 | {Text: "sync", Description: "Synchronize the local state with IAM and Kubernetes"}, 18 | {Text: "trace", Description: "Start tracing"}, 19 | {Text: "export-raw", Description: "Stop tracing and export trace to JSON dump in current working directory"}, 20 | {Text: "export-graph", Description: "Stop tracing and export trace as DOT file in current working directory"}, 21 | {Text: "dump", Description: "Export access graph as a JSON dump in current working directory"}, 22 | {Text: "help", Description: "Explain how it works and show available commands"}, 23 | {Text: "quit", Description: "Terminate the interactive session and quit"}, 24 | } 25 | return prompt.FilterHasPrefix(s, d.GetWordBeforeCursor(), true) 26 | } 27 | 28 | // selectRole allows user to select an IAM role by ARN. 29 | func selectRole(d prompt.Document) []prompt.Suggest { 30 | s := []prompt.Suggest{} 31 | for rolearn := range ag.Roles { 32 | s = append(s, prompt.Suggest{Text: rolearn}) 33 | } 34 | return prompt.FilterContains(s, d.GetWordBeforeCursor(), true) 35 | } 36 | 37 | // selectPolicy allows user to select an IAM policy by ARN. 38 | func selectPolicy(d prompt.Document) []prompt.Suggest { 39 | s := []prompt.Suggest{} 40 | for policyarn := range ag.Policies { 41 | s = append(s, prompt.Suggest{Text: policyarn}) 42 | } 43 | return prompt.FilterContains(s, d.GetWordBeforeCursor(), true) 44 | } 45 | 46 | // selectSA allows user to select an Kubernetes service account. 47 | func selectSA(d prompt.Document) []prompt.Suggest { 48 | s := []prompt.Suggest{} 49 | for saname := range ag.ServiceAccounts { 50 | s = append(s, prompt.Suggest{Text: saname}) 51 | } 52 | return prompt.FilterContains(s, d.GetWordBeforeCursor(), true) 53 | } 54 | 55 | // selectSecret allows user to select an Kubernetes secret. 56 | func selectSecret(d prompt.Document) []prompt.Suggest { 57 | s := []prompt.Suggest{} 58 | for secname := range ag.Secrets { 59 | s = append(s, prompt.Suggest{Text: secname}) 60 | } 61 | return prompt.FilterContains(s, d.GetWordBeforeCursor(), true) 62 | } 63 | 64 | // selectPod allows user to select a Kubernetes pod. 65 | func selectPod(d prompt.Document) []prompt.Suggest { 66 | s := []prompt.Suggest{} 67 | for podname := range ag.Pods { 68 | s = append(s, prompt.Suggest{Text: podname}) 69 | } 70 | return prompt.FilterContains(s, d.GetWordBeforeCursor(), true) 71 | } 72 | -------------------------------------------------------------------------------- /k8s.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/mhausenblas/kubecuddler" 9 | ) 10 | 11 | // namespaceit joins Kubernetes namespace and name. 12 | func namespaceit(ns, name string) string { 13 | return fmt.Sprintf("%v:%v", ns, name) 14 | } 15 | 16 | // kubeIdentity queries the local Kube config to retrieve the configuration. 17 | func (ag *AccessGraph) kubeIdentity() error { 18 | res, err := kubecuddler.Kubectl(false, false, "", "config", "view", "--minify", "--output", "json") 19 | if err != nil { 20 | return err 21 | } 22 | sr := strings.NewReader(res) 23 | decoder := json.NewDecoder(sr) 24 | kconf := &Config{} 25 | err = decoder.Decode(kconf) 26 | if err != nil { 27 | return err 28 | } 29 | ag.KubeConfig = kconf 30 | return nil 31 | } 32 | 33 | // kubeServiceAccounts retrieves the service accounts in the cluster. 34 | func (ag *AccessGraph) kubeServiceAccounts() error { 35 | res, err := kubecuddler.Kubectl(false, false, "", "get", "sa", "--all-namespaces", "--output", "json") 36 | if err != nil { 37 | return err 38 | } 39 | sr := strings.NewReader(res) 40 | decoder := json.NewDecoder(sr) 41 | sal := ServiceAccountList{} 42 | err = decoder.Decode(&sal) 43 | if err != nil { 44 | return err 45 | } 46 | ag.ServiceAccounts = make(map[string]ServiceAccount) 47 | for _, sa := range sal.Items { 48 | ag.ServiceAccounts[namespaceit(sa.Namespace, sa.Name)] = sa 49 | } 50 | return nil 51 | } 52 | 53 | // formatSA provides a textual rendering of the service account. 54 | func formatSA(sa *ServiceAccount) string { 55 | var secrets strings.Builder 56 | for _, sec := range sa.Secrets { 57 | secrets.WriteString(sec.Name + " ") 58 | } 59 | return fmt.Sprintf( 60 | " Namespace: %v\n"+ 61 | " Name: %v\n"+ 62 | " Secrets: %v\n", 63 | sa.Namespace, 64 | sa.Name, 65 | secrets.String(), 66 | ) 67 | } 68 | 69 | // kubeSecrets retrieves the secrets in the cluster. 70 | func (ag *AccessGraph) kubeSecrets() error { 71 | res, err := kubecuddler.Kubectl(false, false, "", "get", "secrets", "--all-namespaces", "--output", "json") 72 | if err != nil { 73 | return err 74 | } 75 | sr := strings.NewReader(res) 76 | decoder := json.NewDecoder(sr) 77 | secl := SecretList{} 78 | err = decoder.Decode(&secl) 79 | if err != nil { 80 | return err 81 | } 82 | ag.Secrets = make(map[string]Secret) 83 | for _, secret := range secl.Items { 84 | ag.Secrets[namespaceit(secret.Namespace, secret.Name)] = secret 85 | } 86 | return nil 87 | } 88 | 89 | // formatSecret provides a textual rendering of the secret. 90 | func formatSecret(secret *Secret) string { 91 | strdatamap := "" 92 | // case of string data present: 93 | if len(secret.StringData) != 0 { 94 | for k, v := range secret.StringData { 95 | strdatamap += fmt.Sprintf("%v: %v ", k, v) 96 | } 97 | } 98 | datamap := "" 99 | // case of data present: 100 | if len(secret.Data) != 0 { 101 | for k, v := range secret.Data { 102 | datamap += fmt.Sprintf("\n %v: %v ", k, string(v)) 103 | } 104 | } 105 | return fmt.Sprintf( 106 | " Namespace: %v\n"+ 107 | " Name: %v\n"+ 108 | " Type: %v\n"+ 109 | " String data: %v\n"+ 110 | " Data: %v\n", 111 | secret.Namespace, 112 | secret.Name, 113 | secret.Type, 114 | strdatamap, 115 | datamap, 116 | ) 117 | } 118 | 119 | // kubePods retrieves the pods in the cluster. 120 | func (ag *AccessGraph) kubePods() error { 121 | res, err := kubecuddler.Kubectl(false, false, "", "get", "pods", "--all-namespaces", "--output", "json") 122 | if err != nil { 123 | return err 124 | } 125 | sr := strings.NewReader(res) 126 | decoder := json.NewDecoder(sr) 127 | podl := PodList{} 128 | err = decoder.Decode(&podl) 129 | if err != nil { 130 | return err 131 | } 132 | ag.Pods = make(map[string]Pod) 133 | for _, pod := range podl.Items { 134 | ag.Pods[namespaceit(pod.Namespace, pod.Name)] = pod 135 | } 136 | return nil 137 | } 138 | 139 | // formatPod provides a textual rendering of the pod. 140 | func formatPod(pod *Pod) string { 141 | strcontainers := "" 142 | for _, container := range pod.Spec.Containers { 143 | strcontainers += fmt.Sprintf( 144 | " Name: %v\n"+ 145 | " Image pull policy: %v\n"+ 146 | " Image: %v\n"+ 147 | " Command: %v\n"+ 148 | " Args: %v\n"+ 149 | " Env: %v", 150 | container.Name, 151 | container.ImagePullPolicy, 152 | container.Image, 153 | container.Command, 154 | container.Args, 155 | container.Env, 156 | ) 157 | } 158 | return fmt.Sprintf( 159 | " Namespace: %v\n"+ 160 | " Name: %v\n"+ 161 | " Service account: %v\n"+ 162 | " Image pull secrets: %v\n"+ 163 | " Volumes: %v\n"+ 164 | " Containers:\n %v\n"+ 165 | " Host IP: %v\n"+ 166 | " Pod IP: %v\n"+ 167 | " Phase: %v\n", 168 | pod.Namespace, 169 | pod.Name, 170 | pod.Spec.ServiceAccountName, 171 | pod.Spec.ImagePullSecrets, 172 | pod.Spec.Volumes, 173 | strcontainers, 174 | pod.Status.HostIP, 175 | pod.Status.PodIP, 176 | pod.Status.Phase, 177 | ) 178 | } 179 | -------------------------------------------------------------------------------- /k8s_types.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | //////////////////////////////////////////////////////////////////////////////// 4 | // https://github.com/kubernetes/apimachinery/blob/master/pkg/apis/meta/v1/types.go 5 | 6 | // ObjectMeta is metadata that all persisted resources must have. 7 | type ObjectMeta struct { 8 | Name string `json:"name,omitempty"` 9 | Namespace string `json:"namespace,omitempty"` 10 | Labels map[string]string `json:"labels,omitempty"` 11 | Annotations map[string]string `json:"annotations,omitempty"` 12 | ClusterName string `json:"clusterName,omitempty"` 13 | } 14 | 15 | // ServiceAccountList is a list of service accounts. 16 | type ServiceAccountList struct { 17 | Items []ServiceAccount `json:"items"` 18 | } 19 | 20 | // SecretList is a list of secrets. 21 | type SecretList struct { 22 | Items []Secret `json:"items"` 23 | } 24 | 25 | // PodList is a list of pods. 26 | type PodList struct { 27 | Items []Pod `json:"items"` 28 | } 29 | 30 | //////////////////////////////////////////////////////////////////////////////// 31 | // https://github.com/kubernetes/kubernetes/blob/master/staging/src/k8s.io/api/core/v1/types.go 32 | 33 | // ObjectReference for inspecting the referred object. 34 | type ObjectReference struct { 35 | Kind string `json:"kind,omitempty"` 36 | Namespace string `json:"namespace,omitempty"` 37 | Name string `json:"name,omitempty"` 38 | FieldPath string `json:"fieldPath,omitempty"` 39 | } 40 | 41 | // LocalObjectReference for locating the referenced object inside a namespace. 42 | type LocalObjectReference struct { 43 | Name string `json:"name,omitempty"` 44 | } 45 | 46 | // ServiceAccount represents a Kubernetes service account. 47 | type ServiceAccount struct { 48 | ObjectMeta `json:"metadata,omitempty"` 49 | Secrets []ObjectReference `json:"secrets,omitempty"` 50 | ImagePullSecrets []LocalObjectReference `json:"imagePullSecrets,omitempty"` 51 | AutomountServiceAccountToken *bool `json:"automountServiceAccountToken,omitempty"` 52 | } 53 | 54 | // Secret represents a Kubernetes secrect holding sensitive data. 55 | type Secret struct { 56 | ObjectMeta `json:"metadata,omitempty"` 57 | Data map[string][]byte `json:"data,omitempty" ` 58 | StringData map[string]string `json:"stringData,omitempty"` 59 | Type SecretType `json:"type,omitempty"` 60 | } 61 | 62 | // SecretType is a custom data type facilitating programmatic handling of 63 | // secret data. 64 | type SecretType string 65 | 66 | // Pod is a collection of containers that can run on a host. 67 | type Pod struct { 68 | ObjectMeta `json:"metadata,omitempty"` 69 | Spec PodSpec `json:"spec,omitempty"` 70 | Status PodStatus `json:"status,omitempty"` 71 | } 72 | 73 | // PodSpec is a description of a pod. 74 | type PodSpec struct { 75 | Volumes []Volume `json:"volumes,omitempty"` 76 | InitContainers []Container `json:"initContainers,omitempty"` 77 | Containers []Container `json:"containers"` 78 | ServiceAccountName string `json:"serviceAccountName,omitempty"` 79 | ImagePullSecrets []LocalObjectReference `json:"imagePullSecrets,omitempty"` 80 | } 81 | 82 | // Volume represents a named volume in a pod. 83 | type Volume struct { 84 | Name string `json:"name"` 85 | } 86 | 87 | // Container is an application container running within a pod. 88 | type Container struct { 89 | Name string `json:"name"` 90 | Image string `json:"image,omitempty"` 91 | Command []string `json:"command,omitempty"` 92 | Args []string `json:"args,omitempty"` 93 | Env []EnvVar `json:"env,omitempty"` 94 | ImagePullPolicy PullPolicy `json:"imagePullPolicy,omitempty"` 95 | } 96 | 97 | // EnvVar represents an environment variable present in a Container. 98 | type EnvVar struct { 99 | Name string `json:"name" protobuf:"bytes,1,opt,name=name"` 100 | Value string `json:"value,omitempty" protobuf:"bytes,2,opt,name=value"` 101 | } 102 | 103 | // PullPolicy describes a policy for if/when to pull a container image. 104 | type PullPolicy string 105 | 106 | // PodStatus represents information about the status of a pod. 107 | type PodStatus struct { 108 | Phase PodPhase `json:"phase,omitempty"` 109 | Conditions []PodCondition `json:"conditions,omitempty"` 110 | Message string `json:"message,omitempty"` 111 | NominatedNodeName string `json:"nominatedNodeName,omitempty"` 112 | HostIP string `json:"hostIP,omitempty" protobuf:"bytes,5,opt,name=hostIP"` 113 | PodIP string `json:"podIP,omitempty" protobuf:"bytes,6,opt,name=podIP"` 114 | } 115 | 116 | // PodPhase is a label for the condition of a pod at the current time. 117 | type PodPhase string 118 | 119 | // PodCondition contains details for the current condition of this pod. 120 | type PodCondition struct { 121 | Type PodConditionType `json:"type"` 122 | Status ConditionStatus `json:"status"` 123 | Reason string `json:"reason,omitempty"` 124 | } 125 | 126 | // PodConditionType is a valid value for PodCondition.Type 127 | type PodConditionType string 128 | 129 | // ConditionStatus provides the status. 130 | type ConditionStatus string 131 | 132 | //////////////////////////////////////////////////////////////////////////////// 133 | // https://github.com/kubernetes/client-go/blob/master/tools/clientcmd/api/v1/types.go 134 | 135 | // Config is a lean v1.Config variant, representing a Kubernetes client config. 136 | type Config struct { 137 | // Clusters is a map of referencable names to cluster configs 138 | Clusters []NamedCluster `json:"clusters"` 139 | // AuthInfos is a map of referencable names to user configs 140 | AuthInfos []NamedAuthInfo `json:"users"` 141 | // Contexts is a map of referencable names to context configs 142 | Contexts []NamedContext `json:"contexts"` 143 | // CurrentContext is the name of the context that you would like to use by default 144 | CurrentContext string `json:"current-context"` 145 | } 146 | 147 | // Cluster contains information about how to communicate with a cluster 148 | type Cluster struct { 149 | // Server is the address of the kubernetes cluster (https://hostname:port) 150 | Server string `json:"server"` 151 | // InsecureSkipTLSVerify skips the validity check for the server's certificate 152 | InsecureSkipTLSVerify bool `json:"insecure-skip-tls-verify,omitempty"` 153 | // CertificateAuthority is the path to a cert file for the certificate authority 154 | CertificateAuthority string `json:"certificate-authority,omitempty"` 155 | // CertificateAuthorityData contains PEM-encoded certificate authority certificates 156 | CertificateAuthorityData []byte `json:"certificate-authority-data,omitempty"` 157 | } 158 | 159 | // AuthInfo contains information that describes identity information 160 | type AuthInfo struct { 161 | // ClientCertificate is the path to a client cert file for TLS 162 | ClientCertificate string `json:"client-certificate,omitempty"` 163 | // ClientCertificateData contains PEM-encoded data from a client cert file for TLS 164 | ClientCertificateData []byte `json:"client-certificate-data,omitempty"` 165 | // ClientKey is the path to a client key file for TLS. 166 | ClientKey string `json:"client-key,omitempty"` 167 | // ClientKeyData contains PEM-encoded data from a client key file for TLS 168 | ClientKeyData []byte `json:"client-key-data,omitempty"` 169 | // Token is the bearer token for authentication 170 | Token string `json:"token,omitempty"` 171 | // TokenFile is a pointer to a file that contains a bearer token. 172 | // If both Token and TokenFile are present, Token takes precedence. 173 | TokenFile string `json:"tokenFile,omitempty"` 174 | // Impersonate is the username to impersonate. The name matches the flag. 175 | Impersonate string `json:"as,omitempty"` 176 | // ImpersonateGroups is the groups to impersonate 177 | ImpersonateGroups []string `json:"as-groups,omitempty"` 178 | // ImpersonateUserExtra contains additional information for impersonated user 179 | ImpersonateUserExtra map[string][]string `json:"as-user-extra,omitempty"` 180 | // Username is the username for basic authentication 181 | Username string `json:"username,omitempty"` 182 | // Password is the password for basic authentication 183 | Password string `json:"password,omitempty"` 184 | // AuthProvider specifies a custom authentication plugin 185 | AuthProvider *AuthProviderConfig `json:"auth-provider,omitempty"` 186 | // Exec specifies a custom exec-based authentication plugin 187 | Exec *ExecConfig `json:"exec,omitempty"` 188 | } 189 | 190 | // Context is a tuple of references to a cluster: 191 | // - how do I communicate with a kubernetes cluster 192 | // - a user, that is, how do I identify myself 193 | // - namespace, that is, what subset of resources do I want to work with 194 | type Context struct { 195 | // Cluster is the name of the cluster for this context 196 | Cluster string `json:"cluster"` 197 | // AuthInfo is the name of the authInfo for this context 198 | AuthInfo string `json:"user"` 199 | // Namespace is the default namespace to use on unspecified requests 200 | Namespace string `json:"namespace,omitempty"` 201 | } 202 | 203 | // NamedCluster relates nicknames to cluster information 204 | type NamedCluster struct { 205 | // Name is the nickname for this Cluster 206 | Name string `json:"name"` 207 | // Cluster holds the cluster information 208 | Cluster Cluster `json:"cluster"` 209 | } 210 | 211 | // NamedContext relates nicknames to context information 212 | type NamedContext struct { 213 | // Name is the nickname for this Context 214 | Name string `json:"name"` 215 | // Context holds the context information 216 | Context Context `json:"context"` 217 | } 218 | 219 | // NamedAuthInfo relates nicknames to auth information 220 | type NamedAuthInfo struct { 221 | // Name is the nickname for this AuthInfo 222 | Name string `json:"name"` 223 | // AuthInfo holds the auth information 224 | AuthInfo AuthInfo `json:"user"` 225 | } 226 | 227 | // AuthProviderConfig holds the configuration for a specified auth provider 228 | type AuthProviderConfig struct { 229 | Name string `json:"name"` 230 | Config map[string]string `json:"config"` 231 | } 232 | 233 | // ExecConfig specifies a command to provide client credentials 234 | type ExecConfig struct { 235 | // Command to execute. 236 | Command string `json:"command"` 237 | // Arguments to pass to the command when executing it 238 | Args []string `json:"args"` 239 | // Env defines additional environment variables to expose to the process 240 | Env []ExecEnvVar `json:"env"` 241 | // Preferred input version of the ExecInfo 242 | APIVersion string `json:"apiVersion,omitempty"` 243 | } 244 | 245 | // ExecEnvVar is used for setting environment variables when executing an exec-based 246 | // credential plugin. 247 | type ExecEnvVar struct { 248 | Name string `json:"name"` 249 | Value string `json:"value"` 250 | } 251 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | 8 | "github.com/aws/aws-sdk-go-v2/aws/external" 9 | "github.com/c-bata/go-prompt" 10 | ) 11 | 12 | // Version is the CLI tool version, provided by the build process, see Makefile 13 | var Version string 14 | 15 | // ag is the global access graph, mostly read-only besides the init phase when 16 | // all the pertinent information is gathered from IAM and Kubernetes via NewAccessGraph() 17 | var ag *AccessGraph 18 | 19 | // history keeps the selected items such as roles or service accounts around 20 | var history []string 21 | 22 | func main() { 23 | cfg, err := external.LoadDefaultAWSConfig() 24 | if err != nil { 25 | fmt.Printf("Can't load AWS config: %v", err.Error()) 26 | os.Exit(1) 27 | } 28 | 29 | offline := os.Getenv("RBIAM_OFFLINE") 30 | switch { 31 | case offline != "": 32 | fmt.Println("Loading IAM and Kubernetes info from local dump.") 33 | ag, err = load("rbiam-offline.json") 34 | if err != nil { 35 | pwarning(fmt.Sprintf("Can't import access graph: %v\n", err)) 36 | } 37 | default: 38 | fmt.Println("Gathering info from IAM and Kubernetes. This may take a bit, please stand by.") 39 | ag = NewAccessGraph(cfg) 40 | } 41 | 42 | // fmt.Println(ag) 43 | var prefix string 44 | tracecntr := 0 45 | tracemode := false 46 | cursel := "help" // make sure to first show the help to guide users what to do 47 | for { 48 | prefix = "? " 49 | switch cursel { 50 | case "iam-user": 51 | presult(formatCaller(ag)) 52 | case "iam-roles": 53 | targetrole := prompt.Input(" ↪ ", selectRole, 54 | prompt.OptionMaxSuggestion(30), 55 | prompt.OptionSuggestionBGColor(prompt.DarkBlue)) 56 | if role, ok := ag.Roles[targetrole]; ok { 57 | presult(formatRole(&role)) 58 | appendhist("IAM role", targetrole) 59 | if tracemode { 60 | tracecntr++ 61 | } 62 | } 63 | case "iam-policies": 64 | targetpolicy := prompt.Input(" ↪ ", selectPolicy, 65 | prompt.OptionMaxSuggestion(30), 66 | prompt.OptionSuggestionBGColor(prompt.DarkBlue)) 67 | if policy, ok := ag.Policies[targetpolicy]; ok { 68 | presult(formatPolicy(&policy)) 69 | appendhist("IAM policy", targetpolicy) 70 | if tracemode { 71 | tracecntr++ 72 | } 73 | } 74 | case "k8s-sa": 75 | targetsa := prompt.Input(" ↪ ", selectSA, 76 | prompt.OptionMaxSuggestion(30), 77 | prompt.OptionSuggestionBGColor(prompt.DarkBlue)) 78 | if sa, ok := ag.ServiceAccounts[targetsa]; ok { 79 | presult(formatSA(&sa)) 80 | appendhist("Kubernetes service account", targetsa) 81 | if tracemode { 82 | tracecntr++ 83 | } 84 | } 85 | case "k8s-secrets": 86 | targetsec := prompt.Input(" ↪ ", selectSecret, 87 | prompt.OptionMaxSuggestion(30), 88 | prompt.OptionSuggestionBGColor(prompt.DarkBlue)) 89 | if secret, ok := ag.Secrets[targetsec]; ok { 90 | presult(formatSecret(&secret)) 91 | appendhist("Kubernetes secret", targetsec) 92 | if tracemode { 93 | tracecntr++ 94 | } 95 | } 96 | case "k8s-pods": 97 | targetpod := prompt.Input(" ↪ ", selectPod, 98 | prompt.OptionMaxSuggestion(30), 99 | prompt.OptionSuggestionBGColor(prompt.DarkBlue)) 100 | if pod, ok := ag.Pods[targetpod]; ok { 101 | presult(formatPod(&pod)) 102 | appendhist("Kubernetes pod", targetpod) 103 | if tracemode { 104 | tracecntr++ 105 | } 106 | } 107 | case "history": 108 | dumphist() 109 | case "sync": 110 | fmt.Println("Gathering info from IAM and Kubernetes. This may take a bit, please stand by ...") 111 | ag = NewAccessGraph(cfg) 112 | case "trace": 113 | tracemode = true 114 | tracecntr = 0 115 | presult("Starting to trace now. Use an 'export-xxx' command to stop tracing and export to one of the supported formats.\n") 116 | case "export-raw": 117 | tracemode = false 118 | fn, err := exportRaw(history[0:tracecntr], ag) 119 | if err != nil { 120 | pwarning(fmt.Sprintf("Can't export trace: %v\n", err)) 121 | continue 122 | } 123 | presult(fmt.Sprintf("Raw trace exported to %v\n", fn)) 124 | case "export-graph": 125 | tracemode = false 126 | fn, err := exportGraph(history[0:tracecntr], ag) 127 | if err != nil { 128 | pwarning(fmt.Sprintf("Can't export trace: %v\n", err)) 129 | continue 130 | } 131 | presult(fmt.Sprintf("Graph trace exported to %v\n", fn)) 132 | case "help": 133 | presult(fmt.Sprintf("\nThis is rbIAM in version %v\n\n", Version)) 134 | presult(strings.Repeat("-", 80)) 135 | presult("\nSelect one of the supported query commands:\n") 136 | presult("- iam-user … to look up the calling AWS IAM user\n") 137 | presult("- iam-roles … to look up an AWS IAM role by ARN\n") 138 | presult("- iam-policies … to look up an AWS IAM policy by ARN\n") 139 | presult("- k8s-sa … to look up an Kubernetes service account\n") 140 | presult("- k8s-secrets … to look up a Kubernetes secret\n") 141 | presult("- k8s-pods … to look up a Kubernetes pod\n") 142 | presult("- history … show history\n") 143 | presult("- sync … to refresh the local data\n") 144 | presult("- trace … start tracing\n") 145 | presult("- export-raw … stop tracing and export trace to JSON dump in current working directory\n") 146 | presult("- export-graph … stop tracing and export trace as DOT file in current working directory\n") 147 | presult(strings.Repeat("-", 80)) 148 | presult("\n\nNote: simply start typing and/or use the tab and cursor keys to select.\n") 149 | presult("CTRL+L clears the screen and if you're stuck type 'help' or 'quit' to leave.\n\n") 150 | case "quit": 151 | presult("bye!\n") 152 | os.Exit(0) 153 | case "dump": 154 | err := dump(ag) 155 | if err != nil { 156 | pwarning(fmt.Sprintf("Can't export access graph: %v\n", err)) 157 | } 158 | default: 159 | presult("Not yet implemented, sorry\n") 160 | } 161 | if tracemode { 162 | prefix = "T " 163 | } 164 | cursel = prompt.Input(prefix, toplevel, 165 | prompt.OptionMaxSuggestion(20), 166 | prompt.OptionSuggestionBGColor(prompt.DarkBlue), 167 | prompt.OptionSelectedDescriptionBGColor(prompt.DarkBlue)) 168 | } 169 | } 170 | 171 | func appendhist(kind, entry string) { 172 | history = append([]string{fmt.Sprintf("[%v] %v", kind, entry)}, history...) 173 | } 174 | 175 | func dumphist() { 176 | for _, entry := range history { 177 | presult(fmt.Sprintf("%v\n", entry)) 178 | } 179 | } 180 | 181 | // Below some helper functions for output. For available colors see: 182 | // https://misc.flogisoft.com/bash/tip_colors_and_formatting 183 | 184 | // presult writes msg in blue to stdout and note that you need to take 185 | // care of newlines yourself. 186 | func presult(msg string) { 187 | _, _ = fmt.Fprintf(os.Stdout, "\x1b[34m%v\x1b[0m", msg) 188 | } 189 | 190 | // pwarning writes msg in red to stdout and note that you need to take 191 | // care of newlines yourself. 192 | func pwarning(msg string) { 193 | _, _ = fmt.Fprintf(os.Stdout, "\x1b[31m%v\x1b[0m", msg) 194 | } 195 | -------------------------------------------------------------------------------- /site/docs/getting-started.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | This guide walks you through the setup and usage of `rbIAM`. 4 | 5 | ## Prerequisites 6 | 7 | In order for you to use `rbIAM`, the following must be true: 8 | 9 | - You have credentials for AWS configured. 10 | - You have access to an EKS cluster or in general an Kubernetes-on-AWS cluster. 11 | - You have `kubectl` [installed](https://kubernetes.io/docs/tasks/tools/install-kubectl/). 12 | 13 | ## Install 14 | 15 | !!! warning 16 | This tool is still WIP and currently the CLI binaries are available only for macOS and Linux platforms. Please [raise an issue on GitHub](https://github.com/mhausenblas/rbIAM/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc) if you're experiencing problems or something does not quite work like described in here. 17 | 18 | To install `rbIAM`, execute the following two commands. First, download 19 | the respective binary (here shown for macOS) like so: 20 | 21 | ```sh 22 | curl -L https://github.com/mhausenblas/rbIAM/releases/latest/download/rbiam-macos -o /usr/local/bin/rbiam 23 | ``` 24 | 25 | And then make it executable: 26 | 27 | ```sh 28 | chmod +x /usr/local/bin/rbiam 29 | ``` 30 | 31 | !!! tip 32 | For Linux install, simply replace the `-macos` part with `-linux` 33 | 34 | Alternatively, you can download the binaries from the [releases](https://github.com/mhausenblas/rbIAM/releases) page on GitHub. 35 | 36 | ## Usage 37 | 38 | The following lists the commands `rbIAM` supports and then walks you through an example usage, end-to-end. 39 | 40 | ### Commands 41 | 42 | The available commands in `rbIAM` v0.3 are: 43 | 44 | 1. General: 45 | * `history` … lists history of selected items in reverse chronological order 46 | * `sync` … synchronizes the local state with the remote one from IAM and Kubernetes 47 | * `help` … lists available commands and provides usage tips 48 | * `quit` … terminates the interactive session and quits the program 49 | 50 | 2. For exploring AWS IAM: 51 | * `iam-user` … allows you to describe the calling AWS IAM user details 52 | * `iam-roles` … allows you to select an AWS IAM role and describe its details 53 | * `iam-policies` … allows you to select an AWS IAM policy and describe its details 54 | 55 | 3. For exploring Kubernetes RBAC: 56 | * `k8s-pods` … allows you to select a Kubernetes pod and describe its details 57 | * `k8s-sa` … allows you to select an Kubernetes service accounts and describe its details 58 | * `k8s-secrets` … allows you to select a Kubernetes secret and describe its details 59 | 60 | 4. For tracing: 61 | * `trace` … start tracing 62 | * `export-raw` … export trace to JSON dump in current working directory (stops tracing) 63 | * `export-graph` … export trace as DOT file in current working directory (stops tracing) 64 | 65 | ### Walkthrough 66 | 67 | In the following we do an end-to-end walkthrough, showing `rbIAM` in action. 68 | 69 | #### Launching & terminating 70 | 71 | After you launch the tool by typing `rbiam` in the shell of your choice, you should see something like this: 72 | 73 | ![startup screen](img/w_startup.png){: style="width:95%; display: block; margin: 10px auto 50px auto; padding: 1px; -webkit-box-shadow: -2px 0px 10px 0px rgba(0,0,0,0.4); -moz-box-shadow: -2px 0px 18px 0px rgba(0,0,0,0.4); box-shadow: -2px 0px 18px 0px rgba(0,0,0,0.4);"} 74 | 75 | On startup, `rbiam` queries both IAM and Kubernetes to get all the pertinent info, from the point of view of the authenticated user. This can take a couple of seconds, and if anything changes, for example you created a new secret in Kubernetes or attached a new policy to a role, you can use the `sync` command to manually trigger this process. 76 | 77 | Now, use the `TAB` key or → (right arrow key) to display the top-level menu: 78 | 79 | ![top-level menu](img/w_toplevelmenu.png){: style="width:95%; display: block; margin: 10px auto 50px auto; padding: 1px; -webkit-box-shadow: -2px 0px 10px 0px rgba(0,0,0,0.4); -moz-box-shadow: -2px 0px 18px 0px rgba(0,0,0,0.4); box-shadow: -2px 0px 18px 0px rgba(0,0,0,0.4);"} 80 | 81 | !!! note 82 | Select any of the commands by navigating with the `TAB`/→ key or by start typing. 83 | When you start typing, only commands starting with said prefix are shown. For example, 84 | if you type `iam`, then the menu reduces to `iam-user`, `iam-roles` , and `iam-policies`. 85 | 86 | Once you're done, you want to terminate `rbIAM`. To do so, start typing `q` and auto-complete 87 | it with `TAB` so that the `quit` command appears and when hitting `ENTER` you then execute said 88 | command and terminate the program. 89 | 90 | !!! tip 91 | In order to clear the screen, you can hit `CTRL+L`. 92 | 93 | Now that we know how to launch and terminate `rbIAM`, let's look something up. 94 | 95 | #### Querying IAM user info { #markdown data-toc-label='IAM user info' } 96 | 97 | To learn about the logged in AWS user and their caller identity, use the `iam-user` command: 98 | 99 | ![AWS IAM user](img/w_iam_user.png){: style="width:95%; display: block; margin: 10px auto 50px auto; padding: 1px; -webkit-box-shadow: -2px 0px 10px 0px rgba(0,0,0,0.4); -moz-box-shadow: -2px 0px 18px 0px rgba(0,0,0,0.4); box-shadow: -2px 0px 18px 0px rgba(0,0,0,0.4);"} 100 | 101 | Above, we've hidden certain sensitive info but, naturally, when you execute the 102 | command you'll see the actual values. 103 | 104 | #### Exploring IAM roles & policies { #markdown data-toc-label='IAM roles &policies' } 105 | 106 | If you want to learn about AWS roles, use the `iam-roles` command. Once selected, 107 | you will see a list of IAM roles you can select from or start typing to filter down 108 | the list. For example, here we used `eksctl` to filter down the list to two entries 109 | and then selected one for exploration with `ENTER`: 110 | 111 | ![AWS IAM roles](img/w_iam_roles.png){: style="width:95%; display: block; margin: 10px auto 50px auto; padding: 1px; -webkit-box-shadow: -2px 0px 10px 0px rgba(0,0,0,0.4); -moz-box-shadow: -2px 0px 18px 0px rgba(0,0,0,0.4); box-shadow: -2px 0px 18px 0px rgba(0,0,0,0.4);"} 112 | 113 | !!! tip 114 | In order to clear the term to the left of the cursor, you can hit `CTRL+W`. 115 | 116 | Further, lo learn about AWS policies, use the `iam-policies` command like so: 117 | 118 | ![AWS IAM policies](img/w_iam_policies.png){: style="width:95%; display: block; margin: 10px auto 50px auto; padding: 1px; -webkit-box-shadow: -2px 0px 10px 0px rgba(0,0,0,0.4); -moz-box-shadow: -2px 0px 18px 0px rgba(0,0,0,0.4); box-shadow: -2px 0px 18px 0px rgba(0,0,0,0.4);"} 119 | 120 | Now that we've covered the IAM side of the house, let's move on to RBAC. 121 | 122 | #### Exploring Kubernetes pods { #markdown data-toc-label='Kubernetes pods' } 123 | 124 | Starting off with the app of interest, you want to find the Kubernetes pod hosting it. 125 | For this, use the `k8s-pods` command as follows: 126 | 127 | ![Kubernetes pods](img/w_k8s_pods.png){: style="width:95%; display: block; margin: 10px auto 50px auto; padding: 1px; -webkit-box-shadow: -2px 0px 10px 0px rgba(0,0,0,0.4); -moz-box-shadow: -2px 0px 18px 0px rgba(0,0,0,0.4); box-shadow: -2px 0px 18px 0px rgba(0,0,0,0.4);"} 128 | 129 | Now that we have the pod info, we can continue with the service account we found 130 | here, in this case, it's the one named `fluent-bit`. 131 | 132 | #### Exploring Kubernetes service accounts & secrets { #markdown data-toc-label='Kubernetes service accounts' } 133 | 134 | To explore Kubernetes service accounts and their secrets, use the `k8s-sa` and 135 | the `k8s-secrets` command, respectively. From the previous step we've gathered 136 | that for the Fluent Bit pod we're interested in the service account `fluent-bit`, 137 | so let's look this up along with its secret: 138 | 139 | ![Kubernetes service accounts & secrets](img/w_k8s_sa_secret.png){: style="width:95%; display: block; margin: 10px auto 50px auto; padding: 1px; -webkit-box-shadow: -2px 0px 10px 0px rgba(0,0,0,0.4); -moz-box-shadow: -2px 0px 18px 0px rgba(0,0,0,0.4); box-shadow: -2px 0px 18px 0px rgba(0,0,0,0.4);"} 140 | 141 | !!! note 142 | Values in secrets are `base64` encoded, however `rbIAM` automatically converts 143 | them into clear text so that you can directly copy & paste them. For example, 144 | in the case shown above, the value of `ca.cert` would be the actual content of 145 | the certificate. 146 | 147 | At any point you can use the `history` command to list the selected items in 148 | reverse chronological order. In our case this would be `fluent-bit-token-5bwm6`, 149 | `fluent-bit`, and `fluent-bit-cscsh`. 150 | 151 | #### Tracing 152 | 153 | Tracing in `rbIAM` means remembering consecutive queries for selected items for 154 | the purpose to export them. If you imaging all the items such as IAM roles and 155 | policies as well as Kubernetes service accounts and pods forming a graph, a 156 | trace is essentially a finite [path](https://en.wikipedia.org/wiki/Path_(graph_theory)). 157 | 158 | You start creating a trace by issuing the `trace` command and when you're done, you'd use one of the `export` commands such as `export-raw`. 159 | 160 | !!! tip 161 | When you start tracing, the prompt will change from `? ` to `T `, indicating that you're currently tracing. As soon as you issue an 162 | export command the prompt will turn back to `? `. 163 | 164 | Let's have a look at it: 165 | 166 | ![Tracing](img/w_tracing.png){: style="width:95%; display: block; margin: 10px auto 50px auto; padding: 1px; -webkit-box-shadow: -2px 0px 10px 0px rgba(0,0,0,0.4); -moz-box-shadow: -2px 0px 18px 0px rgba(0,0,0,0.4); box-shadow: -2px 0px 18px 0px rgba(0,0,0,0.4);"} 167 | 168 | We can now check the exported trace, in raw format using JSON serialization, 169 | available via the file (in our case) `rbiam-trace-1564327989.json`: 170 | 171 | ```json 172 | {"metadata":{"name":"s3-echoer-7q9nt","namespace":"default","labels":{"controller-uid":"be4654b6-aec2-11e9-a44e-02a12cf44842","job-name":"s3-echoer"},"annotations":{"kubernetes.io/psp":"eks.privileged"}},"spec":{"volumes":[{"name":"s3-echoer-token-cm9tx"}],"containers":[{"name":"main","image":"amazonlinux:2018.03","command":["sh","-c","curl -sL -o /s3-echoer https://github.com/mhausenblas/s3-echoer/releases/latest/download/s3-echoer-linux \u0026\u0026 chmod +x /s3-echoer \u0026\u0026 sleep 10000"],"env":[{"name":"AWS_DEFAULT_REGION","value":"us-west-2"}],"imagePullPolicy":"IfNotPresent"}],"serviceAccountName":"s3-echoer"},"status":{"phase":"Succeeded","conditions":[{"type":"Initialized","status":"True","reason":"PodCompleted"},{"type":"Ready","status":"False","reason":"PodCompleted"},{"type":"ContainersReady","status":"False","reason":"PodCompleted"},{"type":"PodScheduled","status":"True"}],"hostIP":"192.168.182.80","podIP":"192.168.159.202"}} 173 | {"metadata":{"name":"s3-echoer","namespace":"default"},"secrets":[{"name":"s3-echoer-token-cm9tx"}]} 174 | ``` 175 | 176 | When you look closely, you will notice that the trace only contains the two 177 | items we queried for after we issued the `trace` command and before we entered 178 | the `export-raw` command. The `history` command at the bottom confirms this. 179 | Remember that the history has the items in reverse chronological order, that is, 180 | the most recent item is always on top of the list. 181 | 182 | While an export command will stop tracing, the traced items will be preserved. 183 | In other words, until you start a new trace you can export the current trace in 184 | different formats. For example, you could issue first an `export-raw` command 185 | and then an `export-graph` command and you would end up with two different 186 | representations of the same trace, once in JSON and once in the [DOT](https://www.graphviz.org/doc/info/lang.html) format. 187 | 188 | !!! tip 189 | To view DOT files, either use an online tool such as [dreampuf.github.io/GraphvizOnline/](https://dreampuf.github.io/GraphvizOnline/) or install Graphviz locally. For example, on macOS you'd do this via `brew install graphviz`. 190 | 191 | to do: show traced and exported Graph here -------------------------------------------------------------------------------- /site/docs/img/cclfb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhausenblas/rbIAM/64085f087b52bf8a820cc07d16c9b7aaea36e487/site/docs/img/cclfb.png -------------------------------------------------------------------------------- /site/docs/img/iam-concept.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhausenblas/rbIAM/64085f087b52bf8a820cc07d16c9b7aaea36e487/site/docs/img/iam-concept.png -------------------------------------------------------------------------------- /site/docs/img/iam-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhausenblas/rbIAM/64085f087b52bf8a820cc07d16c9b7aaea36e487/site/docs/img/iam-example.png -------------------------------------------------------------------------------- /site/docs/img/iam-rbac-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhausenblas/rbIAM/64085f087b52bf8a820cc07d16c9b7aaea36e487/site/docs/img/iam-rbac-example.png -------------------------------------------------------------------------------- /site/docs/img/rbac-concept.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhausenblas/rbIAM/64085f087b52bf8a820cc07d16c9b7aaea36e487/site/docs/img/rbac-concept.png -------------------------------------------------------------------------------- /site/docs/img/rbac-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhausenblas/rbIAM/64085f087b52bf8a820cc07d16c9b7aaea36e487/site/docs/img/rbac-example.png -------------------------------------------------------------------------------- /site/docs/img/w_iam_policies.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhausenblas/rbIAM/64085f087b52bf8a820cc07d16c9b7aaea36e487/site/docs/img/w_iam_policies.png -------------------------------------------------------------------------------- /site/docs/img/w_iam_roles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhausenblas/rbIAM/64085f087b52bf8a820cc07d16c9b7aaea36e487/site/docs/img/w_iam_roles.png -------------------------------------------------------------------------------- /site/docs/img/w_iam_user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhausenblas/rbIAM/64085f087b52bf8a820cc07d16c9b7aaea36e487/site/docs/img/w_iam_user.png -------------------------------------------------------------------------------- /site/docs/img/w_k8s_pods.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhausenblas/rbIAM/64085f087b52bf8a820cc07d16c9b7aaea36e487/site/docs/img/w_k8s_pods.png -------------------------------------------------------------------------------- /site/docs/img/w_k8s_sa_secret.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhausenblas/rbIAM/64085f087b52bf8a820cc07d16c9b7aaea36e487/site/docs/img/w_k8s_sa_secret.png -------------------------------------------------------------------------------- /site/docs/img/w_startup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhausenblas/rbIAM/64085f087b52bf8a820cc07d16c9b7aaea36e487/site/docs/img/w_startup.png -------------------------------------------------------------------------------- /site/docs/img/w_toplevelmenu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhausenblas/rbIAM/64085f087b52bf8a820cc07d16c9b7aaea36e487/site/docs/img/w_toplevelmenu.png -------------------------------------------------------------------------------- /site/docs/img/w_tracing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhausenblas/rbIAM/64085f087b52bf8a820cc07d16c9b7aaea36e487/site/docs/img/w_tracing.png -------------------------------------------------------------------------------- /site/docs/index.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | When using Amazon Elastic Kubernetes Service ([EKS](https://aws.amazon.com/eks/)) you will at some point ask yourself: how does AWS Identity and Access Management ([IAM](https://aws.amazon.com/iam/)) and Kubernetes Role-based access control ([RBAC](https://kubernetes.io/docs/reference/access-authn-authz/rbac/)) play together. Do they overlap? Are they complementary? What are the dependencies? 4 | 5 | `rbIAM` aims to help you navigate this space. 6 | 7 | If you want to learn more about the Why then read on here. For more background, peruse the [terminology](terminology/), and if you want to try it out yourself, check out the [getting started](getting-started/) guide now. 8 | 9 | ## Motivation 10 | 11 | For motivation, let's have a look at a concrete example, the [Fluent Bit output plugin](https://github.com/aws/amazon-kinesis-firehose-for-fluent-bit) for Amazon Kinesis Data Firehose. In [Centralized Container Logging with Fluent Bit](https://aws.amazon.com/blogs/opensource/centralized-container-logging-fluent-bit/) we described the end-to-end setup and how to use it. 12 | 13 | Zooming in on one path, the log shipping in EKS, it looks as follows: 14 | 15 | ![Container log shipping with Fluent Bit on EKS](img/cclfb.png){: style="width:95%; display: block; margin: 30px auto 50px auto; padding: 20px 50px 20px 50px; -webkit-box-shadow: -2px 0px 10px 0px rgba(0,0,0,0.4); -moz-box-shadow: -2px 0px 18px 0px rgba(0,0,0,0.4); box-shadow: -2px 0px 18px 0px rgba(0,0,0,0.4);"} 16 | 17 | The Fluent Bit plugin is deployed as a [daemon set](https://github.com/aws-samples/amazon-ecs-fluent-bit-daemon-service/blob/master/eks/eks-fluent-bit-daemonset.yaml) and in order to do its job: 18 | 19 | 1. It depends on an IAM policy, defined in [eks-fluent-bit-daemonset-policy.json](https://github.com/aws-samples/amazon-ecs-fluent-bit-daemon-service/blob/master/eks/eks-fluent-bit-daemonset-policy.json), giving it the permissions to write to the Kinesis Data Firehose, manage log streams in CloudWatch, etc., as well as 20 | 1. It depends on a Kubernetes role, defined in [eks-fluent-bit-daemonset-rbac.yaml](https://github.com/aws-samples/amazon-ecs-fluent-bit-daemon-service/blob/master/eks/eks-fluent-bit-daemonset-rbac.yaml), giving it the permissions to list and query pods, so that it can receive the logs from the NGINX containers. 21 | 22 | Zooming in even further, focusing on the AWS and Kubernetes access control regimes in place, the relevant parts are: 23 | 24 | ![IAM RBAC example](img/iam-rbac-example.png){: style="width:95%; display: block; margin: 30px auto 50px auto; padding: 20px 50px 20px 50px; -webkit-box-shadow: -2px 0px 10px 0px rgba(0,0,0,0.4); -moz-box-shadow: -2px 0px 18px 0px rgba(0,0,0,0.4); box-shadow: -2px 0px 18px 0px rgba(0,0,0,0.4);"} 25 | 26 | The Fluent Bit plugin has, through the Kubernetes RBAC settings in the `pod-log-reader` role, the permission to read the logs of the NGINX pods and, due to the fact that it is running on an EC2 instance with an AWS IAM role `eksctl-fluent-bit-demo-nodegroup-ng-2fb6f1a-NodeInstanceRole-P6QXJ5EYS6` that has an inline policy attached, allowing it to write the log entries to a Kinesis Data Firehose delivery stream. 27 | 28 | For a finer-grained description of the many moving parts here, have a look at the [terminology](terminology/) section, which defines the terms and explains the motivational example in detail. 29 | 30 | ## Use cases 31 | 32 | You want to use `rbIAM` for: 33 | 34 | - Exploring a given permissions setup, for example, an existing deployment in Kubernetes, when using EKS. 35 | - Find the necessary permissions for a desired setup, both for the IAM policies and the RBAC roles. 36 | - Understand how AWS services, such as S3 or CloudWatch or Firehose interact with Kubernetes resources, such as pods, from an access control perspective. 37 | - Look up what a given Kubernetes resource can or can not do, concerning AWS services. 38 | 39 | We expect that infra admins, devops roles, and also developers can 40 | benefit from `rbIAM`. In order to use the tool, we assume you're familiar with 41 | both AWS IAM and Kubernetes RBAC. If you want to brush up your knowledge, we 42 | recommend first having a look at the [terminology](terminology/) section. -------------------------------------------------------------------------------- /site/docs/terminology.md: -------------------------------------------------------------------------------- 1 | # Terminology 2 | 3 | In the following we define the terms used in AWS IAM and Kubernetes RBAC using the [motivational example](../#motivation) to illustrate what each term means. 4 | 5 | ## AWS Identity and Access Management (IAM) { #markdown data-toc-label='IAM terms' } 6 | 7 | Conceptually, AWS IAM looks as follows: the **access** an identity—such as a user or role—has concerning an AWS service or resource is determined through the attached policies that list allowed actions on resources. 8 | 9 | ![AWS IAM concept](img/iam-concept.png){: style="width:95%; display: block; margin: 30px auto 50px auto; padding: 20px 50px 20px 50px; -webkit-box-shadow: -2px 0px 10px 0px rgba(0,0,0,0.4); -moz-box-shadow: -2px 0px 18px 0px rgba(0,0,0,0.4); box-shadow: -2px 0px 18px 0px rgba(0,0,0,0.4);"} 10 | 11 | More formally, we're using the following terms: 12 | 13 | Principal 14 | 15 | : An identity in AWS able to carry out an action offered by an AWS service 16 | (like listing EC2 instances) or able access a resource (such as reading from an S3 bucket). The identity can be an [account root user](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_root-user.html), an [IAM user](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_users.html), or a **role**. 17 | 18 | Role 19 | 20 | : An identity that—in contrast to an IAM user/root user, which are uniquely 21 | associated with a person—is intended to be assumable by someone (person) or something (service). A role doesn't have long-term credentials, but rather, when assuming a role, [temporary security credentials](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_temp.html) are provided, for the duration of a session. 22 | 23 | Policy 24 | 25 | : A JSON document using the [IAM policy language](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies.html) that either: 26 | 27 | 1. defines allowed actions on resources (services) a **role** can use (permissions policy), or 28 | 29 | 1. defines who is allowed to **assume a role**, in which case the trusted entity is included in the policy as the **principal** (trust policy). 30 | 31 | For example, for our Fluent Bit output plugin deployed as a `DaemonSet` in EKS, one of the IAM regimes looks as follows (compare: [IAM policy used](https://github.com/aws-samples/amazon-ecs-fluent-bit-daemon-service/blob/6bf267b5c750de7df94a0553f0dde9e5c1e4d75a/eks/eks-fluent-bit-daemonset-policy.json#L5)): 32 | 33 | ![AWS IAM example](img/iam-example.png){: style="width:95%; display: block; margin: 30px auto 50px auto; padding: 20px 50px 20px 50px; -webkit-box-shadow: -2px 0px 10px 0px rgba(0,0,0,0.4); -moz-box-shadow: -2px 0px 18px 0px rgba(0,0,0,0.4); box-shadow: -2px 0px 18px 0px rgba(0,0,0,0.4);"} 34 | 35 | In a nutshell: the Fluent Bit output plugin, running in a container part of a pod that is running on an EC2 instance part of a node group with a role `eksctl-fluent-bit-demo-nodegroup-ng-2fb6f1a-NodeInstanceRole-P6QXJ5EYS6` is permitted to perform the `PutRecordBatch` action in Firehose; in fact, with said policy, the Fluent Bit plugin is allowed to put records into *any* delivery stream, since the resource has not been limited to a specific one. 36 | 37 | 38 | ## Kubernetes Role-based Access Control (RBAC) { #markdown data-toc-label='RBAC terms' } 39 | 40 | Conceptually, Kubernetes RBAC looks as follows: the **access** an entity—such as a user or service account—has concerning a Kubernetes resource is determined through two indirections: roles (which define access rules) and role bindings (attaching or binding a role to an entity). 41 | 42 | ![Kubernetes RBAC concept](img/rbac-concept.png){: style="width:95%; display: block; margin: 30px auto 50px auto; padding: 20px 50px 20px 50px; -webkit-box-shadow: -2px 0px 10px 0px rgba(0,0,0,0.4); -moz-box-shadow: -2px 0px 18px 0px rgba(0,0,0,0.4); box-shadow: -2px 0px 18px 0px rgba(0,0,0,0.4);"} 43 | 44 | More formally, we're using the following terms: 45 | 46 | Entity 47 | 48 | : A user, group, or a Kubernetes [service account](https://kubernetes.io/docs/reference/access-authn-authz/service-accounts-admin/). 49 | 50 | 51 | User 52 | 53 | : A human being that is using Kubernetes, either via CLI tools such as 54 | `kubectl`, using the HTTP API of the API server, or indirectly, via cloud native apps. 55 | 56 | Service account 57 | 58 | : Represents processes running in pods that wish to interact with the 59 | API server; a namespaced Kubernetes resource, representing the identity of an app. 60 | 61 | Resource 62 | 63 | : A Kubernetes abstraction, representing operational aspects. Can be 64 | namespaced, for example a pod (co-scheduled containers), a service (east-west load balancer), or a deployment (pod supervisor for app life cycle management) or cluster-wide, such as nodes or namespaces themselves. 65 | 66 | Role 67 | 68 | : Defines a set of strictly additive rules, representing a set of permissions. 69 | These permissions define what actions an **entity** is allowed to carry out 70 | with respect to a set of **resources**. Can be namespaced (then the role is only valid in the context of said namespace) or cluster-wide. 71 | 72 | Role binding 73 | 74 | : Grants the permissions defined in a **role** to an **entity**. Can be 75 | namespaced (then the binding is only valid in the context of said namespace) 76 | or cluster-wide. Note that it is perfectly possible and even desirable to define a cluster-wide role and then use a (namespaced) role binding. This allows straight-forward re-use of roles across namespaces. 77 | 78 | For example, for our Fluent Bit output plugin deployed as a `DaemonSet` in EKS, the RBAC regime looks as follows (compare: [role & role binding used](https://github.com/aws-samples/amazon-ecs-fluent-bit-daemon-service/blob/master/eks/eks-fluent-bit-daemonset-rbac.yaml)): 79 | 80 | ![Kubernetes RBAC example](img/rbac-example.png){: style="width:95%; display: block; margin: 30px auto 50px auto; padding: 20px 60px 20px 40px; -webkit-box-shadow: -2px 0px 10px 0px rgba(0,0,0,0.4); -moz-box-shadow: -2px 0px 18px 0px rgba(0,0,0,0.4); box-shadow: -2px 0px 18px 0px rgba(0,0,0,0.4);"} 81 | 82 | In a nutshell: the Fluent Bit output plugin, using the `default:fluent-bit` service account, is permitted to read and list pods in the default namespace. -------------------------------------------------------------------------------- /site/mkdocs.yml: -------------------------------------------------------------------------------- 1 | # See https://squidfunk.github.io/mkdocs-material/ for options 2 | site_name: rbIAM 3 | site_description: 'Unified AWS IAM & Kubernetes RBAC access control' 4 | site_author: 'Michael Hausenblas' 5 | repo_name: 'mhausenblas/rbIAM' 6 | repo_url: 'https://github.com/mhausenblas/rbIAM' 7 | copyright: 'Copyright © 2019 AWS' 8 | nav: 9 | - Overview: index.md 10 | - Getting Started: getting-started.md 11 | - Terminology: terminology.md 12 | theme: 13 | name: 'material' 14 | custom_dir: 'theme' 15 | favicon: 'assets/images/favicon.png' 16 | logo: 17 | icon: 'security' 18 | palette: 19 | primary: 'white' 20 | accent: 'amber' 21 | highlightjs: true 22 | hljs_languages: 23 | - yaml 24 | - json 25 | - bash 26 | markdown_extensions: 27 | - toc: 28 | permalink: true 29 | - admonition 30 | - def_list 31 | - attr_list 32 | - codehilite: 33 | linenums: true 34 | -------------------------------------------------------------------------------- /site/theme/assets/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhausenblas/rbIAM/64085f087b52bf8a820cc07d16c9b7aaea36e487/site/theme/assets/images/favicon.png --------------------------------------------------------------------------------