├── .github ├── CODEOWNERS ├── CODE_OF_CONDUCT.md ├── DEVELOPER.md ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── documentation_issue.md │ ├── feature_request.md │ └── question.md ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ ├── Release.yaml │ └── unit_test.yaml ├── .gitignore ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── Makefile ├── PROJECT ├── README.md ├── api └── v1alpha1 │ ├── StringOrStrings.go │ ├── client.go │ ├── groupversion_info.go │ ├── iamrole_types.go │ ├── iamrole_types_test.go │ ├── iamrole_webhook.go │ └── zz_generated.deepcopy.go ├── cmd ├── main.go └── main_test.go ├── codecov.yml ├── config ├── certmanager │ ├── certificate.yaml │ ├── kustomization.yaml │ └── kustomizeconfig.yaml ├── crd │ ├── bases │ │ ├── iammanager.keikoproj.io_iamroles-configmap.yaml │ │ └── iammanager.keikoproj.io_iamroles.yaml │ ├── kustomization.yaml │ ├── kustomizeconfig.yaml │ └── patches │ │ ├── cainjection_in_iamroles.yaml │ │ └── webhook_in_iamroles.yaml ├── crd_no_webhook │ ├── bases │ │ ├── iammanager.keikoproj.io_iamroles-configmap.yaml │ │ └── iammanager.keikoproj.io_iamroles.yaml │ └── kustomization.yaml ├── default │ ├── kustomization.yaml │ ├── manager_auth_proxy_patch.yaml │ ├── manager_prometheus_metrics_patch.yaml │ ├── manager_webhook_patch.yaml │ └── webhookcainjection_patch.yaml ├── default_no_webhook │ ├── kustomization.yaml │ ├── manager_auth_proxy_patch.yaml │ └── manager_prometheus_metrics_patch.yaml ├── manager │ ├── kustomization.yaml │ └── manager.yaml ├── rbac │ ├── auth_proxy_role.yaml │ ├── auth_proxy_role_binding.yaml │ ├── auth_proxy_service.yaml │ ├── kustomization.yaml │ ├── leader_election_role.yaml │ ├── leader_election_role_binding.yaml │ ├── role.yaml │ └── role_binding.yaml ├── samples │ └── iammanager_v1alpha1_iamrole.yaml └── webhook │ ├── kustomization.yaml │ ├── kustomizeconfig.yaml │ ├── manifests.yaml │ └── service.yaml ├── docs ├── README.md ├── architecture.md ├── aws-integration.md ├── aws-security.md ├── configmap-properties.md ├── crd-reference.md ├── design.md ├── developer-guide.md ├── features.md ├── images │ ├── Lock.jpg │ ├── camera.jpg │ ├── guard.png │ └── iam-manager-arch.jpeg ├── install.md ├── quickstart.md └── troubleshooting.md ├── examples ├── basic-iam-role.yaml ├── complex-s3-role.yaml └── irsa-role.yaml ├── go.mod ├── go.sum ├── hack ├── allowed_policies.txt ├── boilerplate.go.txt ├── cert-manager.yaml ├── cert.yaml ├── iam-manager-cfn.yaml ├── iam-manager.yaml ├── iam-manager_with_webhook.yaml ├── iammanager.keikoproj.io_iamroles-configmap.yaml ├── install.sh └── update_with_kiam.sh ├── internal ├── config │ ├── constants.go │ ├── properties.go │ └── properties_test.go ├── controllers │ ├── iamrole_controller.go │ ├── iamrole_controller_test.go │ ├── iamrole_reconcile_manager.go │ ├── metrics_test.go │ └── suite_test.go └── utils │ ├── oidc.go │ ├── oidc_test.go │ ├── utils.go │ └── utils_test.go └── pkg ├── awsapi ├── eks.go ├── eks_test.go ├── iam.go ├── iam_test.go ├── sts.go └── sts_test.go ├── k8s ├── client.go └── rbac.go ├── logging └── logging.go └── validation ├── validate.go └── validate_test.go /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # PLEASE READ: 2 | 3 | # This is a comment. 4 | # Each line is a file pattern followed by one or more owners. 5 | 6 | # These owners will be the default owners for everything in 7 | # the repo. Unless a later match takes precedence, 8 | # review when someone opens a pull request. 9 | * @keikoproj/authorized-approvers 10 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | We welcome participation from individuals and groups of all backgrounds who want to benefit the broader open source community 4 | through participation in this project. We are dedicated to ensuring a productive, safe and educational experience for all. 5 | 6 | # Guidelines 7 | 8 | Be welcoming 9 | * Make it easy for new members to learn and contribute. Help them along the path. Don't make them jump through hoops. 10 | 11 | Be considerate 12 | * There is a live person at the other end of the Internet. Consider how your comments will affect them. It is often better to give a quick but useful reply than to delay to compose a more thorough reply. 13 | 14 | Be respectful 15 | * Not everyone is Linus Torvalds, and this is probably a good thing :) but everyone is deserving of respect and consideration for wanting to benefit the broader community. Criticize ideas but respect the person. Saying something positive before you criticize lets the other person know that your criticism is not personal. 16 | 17 | Be patient 18 | * We have diverse backgrounds. It will take time and effort to understand each others' points of view. Some of us have day jobs and other responsibilities and may take time to respond to requests. 19 | 20 | # Relevant References 21 | * http://contributor-covenant.org/version/1/4/code_of_conduct.md 22 | * http://contributor-covenant.org/ -------------------------------------------------------------------------------- /.github/DEVELOPER.md: -------------------------------------------------------------------------------- 1 | # Developing 2 | 3 | 4 | ### Requirements 5 | * Go1.13 6 | * Docker 7 | * Your favorite IDE 8 | * kubebuilder 9 | 10 | ### Quickstart 11 | 12 | First, go and fork the Github repo to your own personal project. Once that's 13 | done, set up a local build environment off of the original Github repo. Then we 14 | add in your fork'ed repo as a new target for doing git pushes. 15 | 16 | $ go clean -modcache 17 | $ go get -v github.com/keikoproj/iam-manager 18 | $ cd "$(go env GOPATH)/src/github.com/keikoproj/iam-manager" 19 | $ make test 20 | $ go mod vendor 21 | $ git remote add myfork 22 | 23 | ### Install Kubebuilder 24 | 25 | Kubebuilder is a requirement for the testsuite.. you can install it quickly 26 | on your own or with our make target: 27 | 28 | $ make kubebuilder 29 | 30 | ### Build project 31 | 32 | $ make 33 | 34 | ### Running Tests 35 | 36 | There are several environment variables that must be set in order for the 37 | test suite to work. The [Makefile](/Makefile) sets these for you, so please 38 | use the `make test` target to run tests: 39 | 40 | $ make test 41 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Report a bug in the project 4 | title: '[BUG] ' 5 | labels: bug 6 | assignees: '' 7 | --- 8 | 9 | ## Bug Description 10 | 11 | 12 | 13 | ## Steps To Reproduce 14 | 15 | 16 | 17 | 1. Step one 18 | 2. Step two 19 | 3. Step three 20 | 21 | ## Expected Behavior 22 | 23 | 24 | 25 | ## Actual Behavior 26 | 27 | 28 | 29 | ## Screenshots/Logs 30 | 31 | 32 | 33 | ## Environment 34 | 35 | 36 | 37 | - Version: 38 | - Kubernetes version: 39 | - Cloud Provider: 40 | - Installation method: 41 | - OS: 42 | - Browser (if applicable): 43 | 44 | ## Additional Context 45 | 46 | 47 | 48 | 49 | ## Impact 50 | 51 | 52 | 53 | - [ ] Blocking (cannot proceed with work) 54 | - [ ] High (significant workaround needed) 55 | - [ ] Medium (minor workaround needed) 56 | - [ ] Low (inconvenience) 57 | 58 | ## Possible Solution 59 | 60 | 61 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/documentation_issue.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Documentation Issue 3 | about: Report issues with documentation or suggest improvements 4 | title: '[DOCS] ' 5 | labels: documentation 6 | assignees: '' 7 | --- 8 | 9 | ## Documentation Issue 10 | 11 | 12 | 13 | 14 | ## Page/Location 15 | 16 | 17 | 18 | 19 | ## Suggested Changes 20 | 21 | 22 | 23 | 24 | ## Additional Information 25 | 26 | 27 | 28 | 29 | ## Would you be willing to contribute this documentation improvement? 30 | 31 | 32 | - [ ] Yes, I can submit a PR with the changes 33 | - [ ] No, I'm not able to contribute documentation for this 34 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: Suggest an enhancement or new feature 4 | title: '[FEATURE] ' 5 | labels: enhancement 6 | assignees: '' 7 | --- 8 | 9 | ## Problem Statement 10 | 11 | 12 | 13 | 14 | ## Proposed Solution 15 | 16 | 17 | 18 | 19 | ## Alternatives Considered 20 | 21 | 22 | 23 | 24 | ## User Value 25 | 26 | 27 | 28 | 29 | ## Implementation Ideas 30 | 31 | 32 | 33 | 34 | ## Additional Context 35 | 36 | 37 | 38 | ## Would you be willing to contribute this feature? 39 | 40 | 41 | - [ ] Yes, I'd like to implement this feature 42 | - [ ] I could contribute partially 43 | - [ ] No, I'm not able to contribute code for this 44 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question or Support Request 3 | about: Ask a question or request support 4 | title: '[QUESTION] ' 5 | labels: question 6 | assignees: '' 7 | --- 8 | 9 | ## Question 10 | 11 | 12 | 13 | ## Context 14 | 15 | 16 | 17 | 18 | ## Environment (if relevant) 19 | 20 | 21 | - Version: 22 | - Kubernetes version: 23 | - Cloud Provider: 24 | - OS: 25 | 26 | ## Screenshots/Logs (if applicable) 27 | 28 | 29 | 30 | ## Related Documentation 31 | 32 | 33 | 34 | ## Search Terms 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 8 | 9 | ## What type of PR is this? 10 | 11 | 12 | - [ ] Bug fix (non-breaking change which fixes an issue) 13 | - [ ] Feature/Enhancement (non-breaking change which adds functionality) 14 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 15 | - [ ] Documentation update 16 | - [ ] Refactoring (no functional changes) 17 | - [ ] Performance improvement 18 | - [ ] Test updates 19 | - [ ] CI/CD related changes 20 | - [ ] Dependency upgrade 21 | 22 | ## Description 23 | 24 | 25 | ## Related issue(s) 26 | 27 | 28 | ## High-level overview of changes 29 | 33 | 34 | ## Testing performed 35 | 42 | 43 | ## Checklist 44 | 45 | 46 | - [ ] I've read the [CONTRIBUTING](/CONTRIBUTING.md) doc 47 | - [ ] I've added/updated tests that prove my fix is effective or that my feature works 48 | - [ ] I've added necessary documentation (if appropriate) 49 | - [ ] I've run `make test` locally and all tests pass 50 | - [ ] I've signed-off my commits with `git commit -s` for DCO verification 51 | - [ ] I've updated any relevant documentation 52 | - [ ] Code follows the style guidelines of this project 53 | 54 | ## Additional information 55 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | # Maintain dependencies for GitHub Actions 9 | - package-ecosystem: "github-actions" 10 | directory: "/" 11 | schedule: 12 | interval: "daily" 13 | - package-ecosystem: "gomod" 14 | directory: "/" 15 | schedule: 16 | interval: "weekly" 17 | allow: 18 | - dependency-type: "direct" 19 | ignore: 20 | - dependency-name: "k8s.io*" 21 | update-types: [ "version-update:semver-major", "version-update:semver-minor" ] 22 | - dependency-name: "*" 23 | update-types: ["version-update:semver-major"] 24 | - package-ecosystem: "docker" 25 | directory: "/" 26 | schedule: 27 | interval: "weekly" 28 | ignore: 29 | - dependency-name: "golang" 30 | -------------------------------------------------------------------------------- /.github/workflows/Release.yaml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | tags: 7 | - 'v*' 8 | 9 | jobs: 10 | push: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Check out code into the Go module directory 14 | uses: actions/checkout@v4 15 | 16 | - name: Set up Docker Buildx 17 | id: buildx 18 | uses: docker/setup-buildx-action@v3 19 | with: 20 | install: true 21 | version: latest 22 | 23 | - name: Set up QEMU 24 | id: qemu 25 | uses: docker/setup-qemu-action@v3 26 | with: 27 | image: tonistiigi/binfmt:latest 28 | platforms: all 29 | 30 | - name: Login to DockerHub 31 | uses: docker/login-action@v3 32 | with: 33 | username: ${{ secrets.DOCKERHUB_USERNAME }} 34 | password: ${{ secrets.DOCKERHUB_TOKEN }} 35 | 36 | - 37 | name: Docker meta 38 | id: docker_meta 39 | uses: docker/metadata-action@v5 40 | with: 41 | images: ${{ github.repository_owner }}/iam-manager 42 | - 43 | name: Build and push 44 | uses: docker/build-push-action@v6 45 | with: 46 | context: . 47 | file: ./Dockerfile 48 | platforms: linux/amd64,linux/arm/v7,linux/arm64 49 | push: true 50 | tags: ${{ steps.docker_meta.outputs.tags }} 51 | -------------------------------------------------------------------------------- /.github/workflows/unit_test.yaml: -------------------------------------------------------------------------------- 1 | name: unit-test 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | lint: 11 | name: lint 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Check out code into the Go module directory 15 | uses: actions/checkout@v4 16 | 17 | - name: Set up Go 1.x 18 | uses: actions/setup-go@v5 19 | with: 20 | go-version: 1.24 21 | cache: true 22 | cache-dependency-path: go.sum 23 | 24 | - name: Generate mocks 25 | run: | 26 | make mock 27 | 28 | # Simple linting first using standard go tools 29 | - name: Run go fmt 30 | run: | 31 | make fmt 32 | 33 | - name: Run go vet 34 | run: | 35 | make vet 36 | 37 | # Run golangci-lint but allow it to fail - document issues for future PR 38 | - name: Run golangci-lint (Non-blocking) 39 | id: lint 40 | continue-on-error: true 41 | uses: golangci/golangci-lint-action@v7 42 | with: 43 | version: latest 44 | args: --timeout=5m --issues-exit-code=0 45 | 46 | unit-test: 47 | name: unit-test 48 | runs-on: ubuntu-latest 49 | needs: lint 50 | steps: 51 | - name: Check out code into the Go module directory 52 | uses: actions/checkout@v4 53 | 54 | - name: Set up Go 1.x 55 | uses: actions/setup-go@v5 56 | with: 57 | go-version: 1.24 58 | cache: true 59 | cache-dependency-path: go.sum 60 | 61 | - name: Build 62 | run: | 63 | make docker-build 64 | 65 | - name: Test 66 | run: | 67 | make test 68 | 69 | - name: Upload coverage reports to Codecov 70 | uses: codecov/codecov-action@v5 71 | with: 72 | files: ./coverage.out 73 | token: ${{ secrets.CODECOV_TOKEN }} 74 | -------------------------------------------------------------------------------- /.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 | mocks/ 17 | gomock*/ 18 | kubebuilder* 19 | 20 | #IDE files 21 | .idea/ 22 | bin/ 23 | .tool-versions 24 | .DS_Store 25 | 26 | manager 27 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to iam-manager 2 | 3 | Thank you for considering contributing to iam-manager! This document outlines the process for contributing to the project and provides guidelines to help you get started. 4 | 5 | ## Code of Conduct 6 | 7 | By participating in this project, you agree to abide by the [Code of Conduct](.github/CODE_OF_CONDUCT.md). Please read it to understand the expectations for all interactions within the community. 8 | 9 | ## Ways to Contribute 10 | 11 | There are many ways to contribute to iam-manager: 12 | 13 | - **Report bugs**: Submit issues for any bugs you encounter 14 | - **Suggest enhancements**: Submit ideas for new features or improvements 15 | - **Improve documentation**: Help us improve or correct documentation 16 | - **Submit code changes**: Contribute bug fixes or new features 17 | - **Review code**: Help review pull requests from other contributors 18 | 19 | ## Development Workflow 20 | 21 | ### Setting up your development environment 22 | 23 | 1. Fork the repository on GitHub 24 | 2. Clone your fork locally: `git clone https://github.com/YOUR-USERNAME/iam-manager.git` 25 | 3. Add the upstream repository: `git remote add upstream https://github.com/keikoproj/iam-manager.git` 26 | 4. Create a new branch for your changes: `git checkout -b feature/your-feature-name` 27 | 28 | ### Making changes 29 | 30 | 1. Make your changes to the codebase 31 | 2. Write or update tests for the changes you make 32 | 3. Make sure your code passes all tests 33 | 4. Update documentation as needed 34 | 5. Commit your changes (see DCO section below) 35 | 36 | ## Pull Request Process 37 | 38 | 1. Push your changes to your fork: `git push origin feature/your-feature-name` 39 | 2. Open a pull request against the main repository 40 | 3. Ensure the PR description clearly describes the problem and solution 41 | 4. Include issue numbers if applicable (e.g., "Fixes #123") 42 | 5. Wait for maintainers to review your PR 43 | 6. Make any requested changes 44 | 7. Once approved, your PR will be merged 45 | 46 | ## Developer Certificate of Origin (DCO) Signing 47 | 48 | We require all contributors to sign their commits with a Developer Certificate of Origin (DCO). The DCO is a lightweight way for contributors to certify that they wrote or otherwise have the right to submit the code they are contributing. 49 | 50 | ### What is DCO? 51 | 52 | The DCO is a simple statement that you, as a contributor, have the legal right to make the contribution and agree to do so under the project's license. The full text of the DCO can be found at [developercertificate.org](https://developercertificate.org/). 53 | 54 | ### How to sign your commits with DCO 55 | 56 | Add a `Signed-off-by` line to your commit messages using the `-s` flag: 57 | 58 | ```bash 59 | git commit -s -m "Your commit message" 60 | ``` 61 | 62 | This will add a signature line to your commit message: 63 | 64 | ``` 65 | Signed-off-by: Your Name 66 | ``` 67 | 68 | Make sure the email address used matches your GitHub account's email address. 69 | 70 | ### DCO Verification 71 | 72 | Pull requests are automatically checked for DCO signatures. PRs that don't have properly signed commits will need to be fixed before they can be merged. 73 | 74 | ## Reporting Bugs 75 | 76 | When reporting bugs, please include: 77 | 78 | - A clear and descriptive title 79 | - Steps to reproduce the issue 80 | - Expected behavior 81 | - Actual behavior 82 | - Any relevant logs or screenshots 83 | - Your environment information (OS, version, etc.) 84 | 85 | Submit bug reports at: https://github.com/keikoproj/iam-manager/issues 86 | 87 | ## Suggesting Enhancements 88 | 89 | When suggesting enhancements, please include: 90 | 91 | - A clear and descriptive title 92 | - A detailed description of the proposed functionality 93 | - Rationale: why this enhancement would be valuable 94 | - If possible, example use cases or implementations 95 | 96 | Submit enhancement suggestions at: https://github.com/keikoproj/iam-manager/issues 97 | 98 | ## Communication 99 | 100 | - GitHub Issues: For bug reports, feature requests, and project discussions 101 | - Pull Requests: For code reviews and submitting changes 102 | - Slack: Join our [Slack channel](https://keikoproj.slack.com/messages/iam-manager) for quick questions and community discussion 103 | 104 | ## License 105 | 106 | By contributing to iam-manager, you agree that your contributions will be licensed under the project's [license](LICENSE). 107 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build the manager binary 2 | FROM golang:1.24 as builder 3 | 4 | WORKDIR /workspace 5 | # Copy the Go Modules manifests 6 | COPY go.mod go.mod 7 | COPY go.sum go.sum 8 | # cache deps before building and copying source so that we don't need to re-download as much 9 | # and so that source changes don't invalidate our downloaded layer 10 | RUN go mod download 11 | 12 | # Copy the go source 13 | COPY cmd/ cmd/ 14 | COPY api/ api/ 15 | COPY pkg/ pkg/ 16 | COPY internal/ internal/ 17 | # Build 18 | RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GO111MODULE=on go build -v -a -o manager cmd/main.go 19 | 20 | # Use distroless as minimal base image to package the manager binary 21 | # Refer to https://github.com/GoogleContainerTools/distroless for more details 22 | FROM gcr.io/distroless/static:nonroot 23 | WORKDIR / 24 | COPY --from=builder /workspace/manager . 25 | USER nonroot:nonroot 26 | 27 | ENTRYPOINT ["/manager"] 28 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Image URL to use all building/pushing image targets 2 | IMG ?= keikoproj/iam-manager:latest 3 | 4 | # Tools required to run the full suite of tests properly 5 | OSNAME ?= $(shell uname -s | tr A-Z a-z) 6 | KUBEBUILDER_ARCH ?= amd64 7 | ENVTEST_K8S_VERSION = 1.28.0 8 | 9 | LOCALBIN ?= $(shell pwd)/bin 10 | # Export local bin to path for all recipes 11 | export PATH := $(LOCALBIN):$(PATH) 12 | 13 | ## Tool Binaries 14 | MOCKGEN ?= $(LOCALBIN)/mockgen 15 | KUSTOMIZE ?= $(LOCALBIN)/kustomize 16 | CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen 17 | 18 | KUBECONFIG ?= $(HOME)/.kube/config 19 | LOCAL ?= true 20 | ALLOWED_POLICY_ACTION ?= s3:,sts:,ec2:Describe,acm:Describe,acm:List,acm:Get,route53:Get,route53:List,route53:Create,route53:Delete,route53:Change,kms:Decrypt,kms:Encrypt,kms:ReEncrypt,kms:GenerateDataKey,kms:DescribeKey,dynamodb:,secretsmanager:GetSecretValue,es:,sqs:SendMessage,sqs:ReceiveMessage,sqs:DeleteMessage,SNS:Publish,sqs:GetQueueAttributes,sqs:GetQueueUrl 21 | RESTRICTED_POLICY_RESOURCES ?= policy-resource 22 | RESTRICTED_S3_RESOURCES ?= s3-resource 23 | AWS_ACCOUNT_ID ?= 123456789012 24 | AWS_REGION ?= us-west-2 25 | MANAGED_POLICIES ?= arn:aws:iam::123456789012:policy/SOMETHING 26 | MANAGED_PERMISSION_BOUNDARY_POLICY ?= arn:aws:iam::1123456789012:role/iam-manager-permission-boundary 27 | CLUSTER_NAME ?= k8s_test_keiko 28 | CLUSTER_OIDC_ISSUER_URL ?= https://google.com/OIDC 29 | DEFAULT_TRUST_POLICY ?= '{"Version": "2012-10-17", "Statement": [{"Effect": "Allow","Principal": {"Federated": "arn:aws:iam::AWS_ACCOUNT_ID:oidc-provider/OIDC_PROVIDER"},"Action": "sts:AssumeRoleWithWebIdentity","Condition": {"StringEquals": {"OIDC_PROVIDER:sub": "system:serviceaccount:{{.NamespaceName}}:SERVICE_ACCOUNT_NAME"}}}, {"Effect": "Allow","Principal": {"AWS": ["arn:aws:iam::{{.AccountID}}:role/trust_role"]},"Action": "sts:AssumeRole"}]}' 30 | 31 | ENVTEST ?= $(LOCALBIN)/setup-envtest 32 | 33 | # Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) 34 | ifeq (,$(shell go env GOBIN)) 35 | GOBIN := $(shell go env GOPATH)/bin 36 | else 37 | GOBIN := $(shell go env GOBIN) 38 | endif 39 | 40 | all: manager 41 | 42 | # Build manager binary 43 | manager: $(LOCALBIN)/manager 44 | $(LOCALBIN)/manager: generate fmt mock vet update 45 | go build -o $(LOCALBIN)/manager cmd/main.go 46 | 47 | mock: $(MOCKGEN) 48 | @echo "mockgen is in progess" 49 | @for pkg in $(shell go list ./...) ; do \ 50 | go generate ./... ;\ 51 | done 52 | 53 | # Run tests 54 | test: mock generate fmt manifests envtest 55 | KUBECONFIG=$(KUBECONFIG) \ 56 | LOCAL=$(LOCAL) \ 57 | ALLOWED_POLICY_ACTION=$(ALLOWED_POLICY_ACTION) \ 58 | RESTRICTED_POLICY_RESOURCES=$(RESTRICTED_POLICY_RESOURCES) \ 59 | RESTRICTED_S3_RESOURCES=$(RESTRICTED_S3_RESOURCES) \ 60 | AWS_ACCOUNT_ID=$(AWS_ACCOUNT_ID) \ 61 | AWS_REGION=$(AWS_REGION) \ 62 | MANAGED_POLICIES=$(MANAGED_POLICIES) \ 63 | MANAGED_PERMISSION_BOUNDARY_POLICY=$(MANAGED_PERMISSION_BOUNDARY_POLICY) \ 64 | CLUSTER_NAME=$(CLUSTER_NAME) \ 65 | CLUSTER_OIDC_ISSUER_URL="$(CLUSTER_OIDC_ISSUER_URL)" \ 66 | DEFAULT_TRUST_POLICY=$(DEFAULT_TRUST_POLICY) \ 67 | KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" go test ./... -coverprofile cover.out 68 | 69 | # Run against the configured Kubernetes cluster in ~/.kube/config 70 | run: generate fmt vet manifests 71 | go run ./cmd/main.go 72 | 73 | # Install CRDs into a cluster 74 | install: manifests kustomize 75 | $(KUSTOMIZE) build config/crd_no_webhook | kubectl apply -f - 76 | 77 | # Deploy controller in the configured Kubernetes cluster in ~/.kube/config 78 | deploy: manifests kustomize 79 | cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} 80 | $(KUSTOMIZE) build config/default_no_webhook | kubectl apply -f - 81 | 82 | # Install CRDs into a cluster 83 | install_with_webhook: manifests kustomize 84 | $(KUSTOMIZE) build config/crd | kubectl apply -f - 85 | 86 | # Deploy controller in the configured Kubernetes cluster in ~/.kube/config 87 | deploy_with_webhook: manifests kustomize 88 | cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} 89 | $(KUSTOMIZE) build config/default | kubectl apply -f - 90 | 91 | # updates the full config yaml file 92 | update: manifests kustomize 93 | cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} 94 | $(KUSTOMIZE) build config/default_no_webhook > hack/iam-manager.yaml 95 | $(KUSTOMIZE) build config/default > hack/iam-manager_with_webhook.yaml 96 | 97 | # Generate manifests e.g. CRD, RBAC etc. 98 | manifests: controller-gen 99 | $(CONTROLLER_GEN) rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases 100 | $(CONTROLLER_GEN) rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd_no_webhook/bases 101 | 102 | # Run go fmt against code 103 | fmt: 104 | go fmt ./... 105 | 106 | # Run go vet against code 107 | vet: mock 108 | go vet ./... 109 | 110 | # Generate code 111 | generate: controller-gen 112 | $(CONTROLLER_GEN) object:headerFile=./hack/boilerplate.go.txt paths="./..." 113 | 114 | # Build the docker image 115 | docker-build: 116 | docker build . -t ${IMG} 117 | 118 | # Push the docker image 119 | docker-push: 120 | docker push ${IMG} 121 | 122 | 123 | ## Tool Versions 124 | MOCKGEN_VERSION ?= v1.6.0 125 | KUSTOMIZE_VERSION ?= v4.2.0 126 | CONTROLLER_TOOLS_VERSION ?= v0.17.0 127 | 128 | $(MOCKGEN): $(LOCALBIN) ## Download mockgen if necessary. 129 | GOBIN=$(LOCALBIN) go install github.com/golang/mock/mockgen@$(MOCKGEN_VERSION) 130 | 131 | .PHONY: controller-gen 132 | controller-gen: $(CONTROLLER_GEN) ## Download controller-gen if necessary. 133 | $(CONTROLLER_GEN): $(LOCALBIN) 134 | GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-tools/cmd/controller-gen@$(CONTROLLER_TOOLS_VERSION) 135 | 136 | KUSTOMIZE_INSTALL_SCRIPT ?= "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh" 137 | .PHONY: kustomize 138 | kustomize: $(KUSTOMIZE) ## Download kustomize locally if necessary. 139 | $(KUSTOMIZE): $(LOCALBIN) 140 | rm -f $(KUSTOMIZE) || true 141 | curl -s $(KUSTOMIZE_INSTALL_SCRIPT) | bash -s -- $(subst v,,$(KUSTOMIZE_VERSION)) $(LOCALBIN) 142 | 143 | .PHONY: envtest 144 | envtest: $(ENVTEST) ## Download envtest-setup locally if necessary. 145 | $(ENVTEST): $(LOCALBIN) 146 | test -s $(LOCALBIN)/setup-envtest || GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-runtime/tools/setup-envtest@latest 147 | 148 | $(LOCALBIN): 149 | mkdir -p $(LOCALBIN) 150 | -------------------------------------------------------------------------------- /PROJECT: -------------------------------------------------------------------------------- 1 | version: "3" 2 | domain: keikoproj.io 3 | repo: github.com/keikoproj/iam-manager 4 | resources: 5 | - group: iammanager 6 | version: v1alpha1 7 | kind: Iamrole 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # iam-manager 2 | 3 | [![Maintenance](https://img.shields.io/badge/Maintained%3F-yes-green.svg)][GithubMaintainedUrl] 4 | [![PR](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)][GithubPrsUrl] 5 | [![slack](https://img.shields.io/badge/slack-join%20the%20conversation-ff69b4.svg)][SlackUrl] 6 | 7 | [![Release][ReleaseImg]][ReleaseUrl] 8 | [![Build Status][BuildStatusImg]][BuildMasterUrl] 9 | [![codecov][CodecovImg]][CodecovUrl] 10 | [![Go Report Card][GoReportImg]][GoReportUrl] 11 | 12 | A Kubernetes operator that manages AWS IAM roles for namespaces and service accounts using custom resources. 13 | 14 | ## Table of Contents 15 | - [Overview](#overview) 16 | - [Requirements](#requirements) 17 | - [Features](#features) 18 | - [Architecture](#architecture) 19 | - [Quick Start](#quick-start) 20 | - [Usage](#usage) 21 | - [Documentation](#documentation) 22 | - [Version Compatibility](#version-compatibility) 23 | - [Contributing](#contributing) 24 | 25 | ## Overview 26 | 27 | iam-manager simplifies AWS IAM role management within Kubernetes clusters by providing a declarative approach through custom resources. It enables namespace-scoped IAM role creation, enforces security best practices, and integrates with AWS IAM Role for Service Accounts (IRSA). 28 | 29 | Originally developed at Intuit to manage IAM roles across 200+ clusters and 8000+ namespaces, iam-manager allows application teams to create and update IAM roles as part of their GitOps deployment pipelines, eliminating manual IAM policy management. This enables a "single manifest" approach where teams can manage both Kubernetes resources and IAM permissions together. For more details on the design principles and origin story, see the [Managing IAM Roles as K8s Resources](https://medium.com/keikoproj/managing-iam-roles-as-k8s-resources-aa00c5c4447f) article. 30 | 31 | ## Requirements 32 | 33 | - Kubernetes cluster 1.16+ 34 | - AWS IAM permissions to create/update/delete roles 35 | - AWS account with permission boundary policy configured 36 | - Cert-manager (for webhook validation, optional) 37 | 38 | ## Features 39 | 40 | iam-manager provides a comprehensive set of features for IAM role management: 41 | 42 | - [IAM Roles Management](docs/features.md#iam-roles-management) - Create, update, and delete IAM roles through Kubernetes resources 43 | - [IAM Role for Service Accounts (IRSA)](docs/features.md#iam-role-for-service-accounts-irsa) - Integration with AWS IAM Roles for Service Accounts 44 | - [AWS Service-Linked Roles](docs/features.md#aws-service-linked-roles) - Support for service-linked roles 45 | - [Default Trust Policy for All Roles](docs/features.md#default-trust-policy-for-all-roles) - Enforce consistent trust policies 46 | - [Maximum Number of Roles per Namespace](docs/features.md#maximum-number-of-roles-per-namespace) - Governance controls 47 | - [Attaching Managed IAM Policies for All Roles](docs/features.md#attaching-managed-iam-policies-for-all-roles) - Simplified policy management 48 | - [Multiple Trust policies](docs/features.md#multiple-trust-policies) - Flexible trust relationship configuration 49 | 50 | ## Architecture 51 | 52 | iam-manager follows a Kubernetes operator pattern that watches for Iamrole custom resources and manages the corresponding IAM roles in AWS. 53 | 54 | ![IAM Manager Architecture](docs/images/iam-manager-architecture.png) 55 | 56 | The controller reconciles Kubernetes resources with AWS IAM roles, ensuring that: 57 | - Each valid Iamrole CR has a corresponding IAM role in AWS 58 | - Changes to Iamrole CRs are reflected in the AWS IAM roles 59 | - Deleted Iamrole CRs result in cleanup of the corresponding AWS resources 60 | 61 | For a more detailed view of the architecture including component interactions and workflows, see the [Architecture Documentation](docs/architecture.md). 62 | 63 | ## Quick Start 64 | 65 | The fastest way to install iam-manager is to use the provided installation script: 66 | 67 | ```bash 68 | git clone https://github.com/keikoproj/iam-manager.git 69 | cd iam-manager 70 | ./hack/install.sh [cluster_name] [aws_region] [aws_profile] 71 | ``` 72 | 73 | For detailed installation instructions, configuration options, and prerequisites, see the [Installation Guide](docs/install.md). 74 | 75 | ## Usage 76 | 77 | Here's a minimal example of an IAM role for accessing S3: 78 | 79 | ```yaml 80 | apiVersion: iammanager.keikoproj.io/v1alpha1 81 | kind: Iamrole 82 | metadata: 83 | name: s3-reader-role 84 | namespace: default 85 | spec: 86 | PolicyDocument: 87 | Statement: 88 | - Effect: "Allow" 89 | Action: 90 | - "s3:GetObject" 91 | - "s3:ListBucket" 92 | Resource: 93 | - "arn:aws:s3:::your-bucket-name/*" 94 | - "arn:aws:s3:::your-bucket-name" 95 | Sid: "AllowS3Access" 96 | ``` 97 | 98 | For IRSA (IAM Roles for Service Accounts) integration: 99 | 100 | ```yaml 101 | apiVersion: iammanager.keikoproj.io/v1alpha1 102 | kind: Iamrole 103 | metadata: 104 | name: app-role 105 | namespace: default 106 | annotations: 107 | iam.amazonaws.com/irsa-service-account: app-service-account 108 | spec: 109 | PolicyDocument: 110 | Statement: 111 | - Effect: "Allow" 112 | Action: ["s3:GetObject"] 113 | Resource: ["arn:aws:s3:::your-bucket-name/*"] 114 | ``` 115 | 116 | For detailed examples and usage patterns, see the [examples directory](examples/) and the [CRD Reference](docs/crd-reference.md). 117 | 118 | ## Documentation 119 | 120 | Comprehensive documentation is available: 121 | 122 | - [Architecture Documentation](docs/architecture.md) 123 | - [Quick Start Guide](docs/quickstart.md) 124 | - [Design Documentation](docs/design.md) 125 | - [Configuration Options](docs/configmap-properties.md) 126 | - [Developer Guide](docs/developer-guide.md) 127 | - [AWS Integration](docs/aws-integration.md) 128 | - [AWS Security](docs/aws-security.md) 129 | - [Features](docs/features.md) 130 | - [Installation Guide](docs/install.md) 131 | - [CRD Reference](docs/crd-reference.md) 132 | - [Troubleshooting Guide](docs/troubleshooting.md) 133 | 134 | ## Version Compatibility 135 | 136 | | iam-manager Version | Kubernetes Version | Go Version | Key Features | 137 | |---------------------|-------------------|------------|--------------| 138 | | current | 1.16 - 1.27 | 1.24+ | Upgrade to Go 1.24 | 139 | | v0.22.0 | 1.16 - 1.25 | 1.19+ | IRSA regional endpoint configuration | 140 | | v0.21.0 | 1.16 - 1.24 | 1.18+ | Enhanced security features | 141 | | v0.20.0 | 1.16 - 1.23 | 1.17+ | Improved reconciliation controller | 142 | | v0.19.0 | 1.16 - 1.22 | 1.16+ | IRSA support improvements | 143 | | v0.18.0 | 1.16 - 1.21 | 1.15+ | Custom role naming | 144 | 145 | For detailed information about each release, see the [GitHub Releases page](https://github.com/keikoproj/iam-manager/releases). 146 | 147 | ## Contributing 148 | 149 | Please check [CONTRIBUTING.md](CONTRIBUTING.md) before contributing. 150 | 151 | 152 | [GithubMaintainedUrl]: https://github.com/keikoproj/iam-manager/graphs/commit-activity 153 | [GithubPrsUrl]: https://github.com/keikoproj/iam-manager/pulls 154 | [SlackUrl]: https://keikoproj.slack.com/app_redirect?channel=iam-manager 155 | 156 | [ReleaseImg]: https://img.shields.io/github/release/keikoproj/iam-manager.svg 157 | [ReleaseUrl]: https://github.com/keikoproj/iam-manager/releases 158 | 159 | [BuildStatusImg]: https://github.com/keikoproj/iam-manager/actions/workflows/unit_test.yaml/badge.svg 160 | [BuildMasterUrl]: https://github.com/keikoproj/iam-manager/actions/workflows/unit_test.yaml 161 | 162 | [CodecovImg]: https://codecov.io/gh/keikoproj/iam-manager/branch/master/graph/badge.svg 163 | [CodecovUrl]: https://codecov.io/gh/keikoproj/iam-manager 164 | 165 | [GoReportImg]: https://goreportcard.com/badge/github.com/keikoproj/iam-manager 166 | [GoReportUrl]: https://goreportcard.com/report/github.com/keikoproj/iam-manager -------------------------------------------------------------------------------- /api/v1alpha1/StringOrStrings.go: -------------------------------------------------------------------------------- 1 | package v1alpha1 2 | 3 | import "encoding/json" 4 | 5 | // StringOrStrings type accepts one string or multiple strings 6 | // +kubebuilder:object:generate=false 7 | type StringOrStrings []string 8 | 9 | // MarshalJSON function is a custom implementation of json.Marshal for StringOrStrings 10 | func (s StringOrStrings) MarshalJSON() ([]byte, error) { 11 | //This is going to be tricky 12 | //if len(s) == 1 { 13 | // return json.Marshal(s[0]) 14 | //} 15 | //I need to convert it to string array 16 | // if i use json.Marshal(s) here it is going to go into infinite loop 17 | // since json.Marshal for type StringOrStrings are overwritten in this very own method 18 | var k []string 19 | for _, str := range s { 20 | k = append(k, str) 21 | } 22 | return json.Marshal(k) 23 | } 24 | 25 | // UnmarshalJson function is a custom implementation of json to unmarshal StringOrStrings 26 | func (s *StringOrStrings) UnmarshalJSON(b []byte) error { 27 | //Try to convert to array 28 | var strings []string 29 | if err := json.Unmarshal(b, &strings); err != nil { 30 | //If err, convert it to string and add it to array 31 | var str string 32 | err = json.Unmarshal(b, &str) 33 | if err != nil { 34 | return err 35 | } 36 | strings = []string{str} 37 | 38 | } 39 | 40 | *s = strings 41 | return nil 42 | } 43 | -------------------------------------------------------------------------------- /api/v1alpha1/client.go: -------------------------------------------------------------------------------- 1 | package v1alpha1 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | 7 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 8 | "k8s.io/apimachinery/pkg/runtime/schema" 9 | "sigs.k8s.io/controller-runtime/pkg/client" 10 | 11 | "github.com/keikoproj/iam-manager/pkg/logging" 12 | ) 13 | 14 | /** 15 | * This function is used to retrieve all IAM-Roles from the cluster across all namespaces. 16 | * It will return a list of IAM-Roles in structured format. 17 | */ 18 | func ListIamRoles(ctx context.Context, c client.Client) ([]*Iamrole, error) { 19 | log := logging.Logger(ctx, "k8s", "client", "ListIamRoles") 20 | 21 | var uRoleList *unstructured.UnstructuredList = &unstructured.UnstructuredList{} 22 | var iamRoles []*Iamrole = []*Iamrole{} 23 | var err error 24 | var b []byte 25 | var IamroleGroupVersionKind = schema.GroupVersionKind{ 26 | Group: "iammanager.keikoproj.io", 27 | Version: "v1alpha1", 28 | Kind: "Iamrole", 29 | } 30 | uRoleList.SetGroupVersionKind(IamroleGroupVersionKind) 31 | 32 | if err = c.List(ctx, uRoleList, &client.ListOptions{}); err != nil { 33 | log.Error(err, "unable to list iamroles resources") 34 | return iamRoles, err 35 | } 36 | 37 | if b, err = json.Marshal(uRoleList.Items); err != nil { 38 | log.Error(err, "unable to marshal iamroles resources") 39 | return iamRoles, err 40 | } 41 | 42 | if err = json.Unmarshal(b, &iamRoles); err != nil { 43 | log.Error(err, "unable to unmarshal iamroles resources") 44 | return iamRoles, err 45 | } 46 | 47 | return iamRoles, nil 48 | } 49 | 50 | func GetIamRole(ctx context.Context, c client.Client, name, namespace string) (*Iamrole, error) { 51 | log := logging.Logger(ctx, "k8s", "client", "GetIamRole") 52 | log.V(1).Info("get api call for iamrole") 53 | 54 | var uRole *unstructured.Unstructured = &unstructured.Unstructured{} 55 | var iamRole *Iamrole = &Iamrole{} 56 | var err error 57 | var b []byte 58 | var IamroleGroupVersionKind = schema.GroupVersionKind{ 59 | Group: "iammanager.keikoproj.io", 60 | Version: "v1alpha1", 61 | Kind: "Iamrole", 62 | } 63 | uRole.SetGroupVersionKind(IamroleGroupVersionKind) 64 | 65 | if err = c.Get(ctx, client.ObjectKey{Namespace: namespace, Name: name}, uRole); err != nil { 66 | log.Error(err, "unable to get iamrole resource") 67 | return iamRole, err 68 | } 69 | 70 | if b, err = json.Marshal(uRole); err != nil { 71 | log.Error(err, "unable to marshal iamrole resource") 72 | return iamRole, err 73 | } 74 | 75 | if err = json.Unmarshal(b, iamRole); err != nil { 76 | log.Error(err, "unable to unmarshal iamrole resource") 77 | return iamRole, err 78 | } 79 | 80 | return iamRole, nil 81 | } 82 | -------------------------------------------------------------------------------- /api/v1alpha1/groupversion_info.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | 16 | // Package v1alpha1 contains API Schema definitions for the iammanager v1alpha1 API group 17 | // +kubebuilder:object:generate=true 18 | // +groupName=iammanager.keikoproj.io 19 | package v1alpha1 20 | 21 | import ( 22 | "k8s.io/apimachinery/pkg/runtime/schema" 23 | "sigs.k8s.io/controller-runtime/pkg/scheme" 24 | ) 25 | 26 | var ( 27 | // GroupVersion is group version used to register these objects 28 | GroupVersion = schema.GroupVersion{Group: "iammanager.keikoproj.io", Version: "v1alpha1"} 29 | 30 | // SchemeBuilder is used to add go types to the GroupVersionKind scheme 31 | SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} 32 | 33 | // AddToScheme adds the types in this group-version to the given scheme. 34 | AddToScheme = SchemeBuilder.AddToScheme 35 | ) 36 | -------------------------------------------------------------------------------- /api/v1alpha1/iamrole_types.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | 16 | package v1alpha1 17 | 18 | import ( 19 | "fmt" 20 | "hash/adler32" 21 | "strings" 22 | 23 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 24 | ) 25 | 26 | // EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! 27 | // NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. 28 | 29 | // IamroleSpec defines the desired state of Iamrole 30 | type IamroleSpec struct { 31 | PolicyDocument PolicyDocument `json:"PolicyDocument"` 32 | // +optional 33 | AssumeRolePolicyDocument *AssumeRolePolicyDocument `json:"AssumeRolePolicyDocument,omitempty"` 34 | // RoleName can be passed only for privileged namespaces. This will be respected only during new iamrole creation and will be ignored during iamrole update 35 | // Please check the documentation for more on how to configure privileged namespace using annotation for iam-manager 36 | // +optional 37 | RoleName string `json:"RoleName,omitempty"` 38 | } 39 | 40 | // +kubebuilder:validation:Required 41 | 42 | // PolicyDocument type defines IAM policy struct 43 | type PolicyDocument struct { 44 | 45 | // Version specifies IAM policy version 46 | // By default, this value is "2012-10-17" 47 | // +optional 48 | Version string `json:"Version,omitempty"` 49 | 50 | // Statement allows list of statement object 51 | Statement []Statement `json:"Statement"` 52 | } 53 | 54 | // +kubebuilder:validation:Required 55 | // Statement type defines the AWS IAM policy statement 56 | type Statement struct { 57 | //Effect allowed/denied 58 | Effect Effect `json:"Effect"` 59 | 60 | //Action allowed on specific resources 61 | Action []string `json:"Action"` 62 | 63 | //Resources defines target resources which IAM policy will be applied 64 | Resource []string `json:"Resource"` 65 | // Sid is an optional field which describes the specific statement action 66 | // +optional 67 | Sid string `json:"Sid,omitempty"` 68 | } 69 | 70 | // Effect describes whether to allow or deny the specific action 71 | // Allowed values are 72 | // - "Allow" : allows the specific action on resources 73 | // - "Deny" : denies the specific action on resources 74 | // +kubebuilder:validation:Enum=Allow;Deny 75 | type Effect string 76 | 77 | // +optional 78 | type AssumeRolePolicyDocument struct { 79 | // Version specifies IAM policy version 80 | // By default, this value is "2012-10-17" 81 | // +optional 82 | Version string `json:"Version,omitempty"` 83 | 84 | // Statement allows list of TrustPolicyStatement objects 85 | // +optional 86 | Statement []TrustPolicyStatement `json:"Statement,omitempty"` 87 | } 88 | 89 | // TrustPolicy struct holds Trust policy 90 | // +optional 91 | type TrustPolicyStatement struct { 92 | //Effect allowed/denied 93 | Effect Effect `json:"Effect,omitempty"` 94 | //Action can be performed 95 | Action string `json:"Action,omitempty"` 96 | // +optional 97 | Principal Principal `json:"Principal,omitempty"` 98 | // +optional 99 | Condition *Condition `json:"Condition,omitempty"` 100 | } 101 | 102 | // Id returns the sid of the trust policy statement 103 | func (tps *TrustPolicyStatement) Id() string { 104 | sid := strings.Title(fmt.Sprintf("%s%s%x", tps.Effect, 105 | strings.ReplaceAll(strings.Title(tps.Action), ":", ""), 106 | adler32.Checksum([]byte(fmt.Sprintf("%+v", tps.Principal))))) 107 | 108 | if tps.HasCondition() { 109 | if tps.IsConditionAnyServiceAccount() { 110 | sid = fmt.Sprintf("%s%s", sid, "Any") 111 | } else { 112 | sid = fmt.Sprintf("%s%s", sid, tps.ConditionChecksum()) 113 | } 114 | } 115 | return sid 116 | } 117 | 118 | func (tps *TrustPolicyStatement) HasCondition() bool { 119 | return tps.Condition != nil 120 | } 121 | 122 | func (tps *TrustPolicyStatement) ConditionChecksum() string { 123 | if !tps.HasCondition() { 124 | return "" 125 | } 126 | return fmt.Sprintf("%x", adler32.Checksum([]byte(fmt.Sprintf("%+v", *tps.Condition)))) 127 | } 128 | 129 | func (tps *TrustPolicyStatement) IsConditionAnyServiceAccount() bool { 130 | if !tps.HasCondition() || len(tps.Condition.StringLike) == 0 { 131 | return false 132 | } 133 | 134 | for k, v := range tps.Condition.StringLike { 135 | if strings.HasSuffix(k, ":sub") { 136 | parts := strings.Split(v, ":") 137 | if parts[len(parts)-1] == "*" { 138 | return true 139 | } 140 | } 141 | } 142 | 143 | return false 144 | } 145 | 146 | // Principal struct holds AWS principal 147 | // +optional 148 | type Principal struct { 149 | // +optional 150 | AWS StringOrStrings `json:"AWS,omitempty"` 151 | // +optional 152 | Service string `json:"Service,omitempty"` 153 | // +optional 154 | Federated string `json:"Federated,omitempty"` 155 | } 156 | 157 | // Condition struct holds Condition 158 | // +optional 159 | type Condition struct { 160 | //StringEquals can be used to define Equal condition 161 | // +optional 162 | StringEquals map[string]string `json:"StringEquals,omitempty"` 163 | //StringLike can be used for regex as supported by AWS 164 | // +optional 165 | StringLike map[string]string `json:"StringLike,omitempty"` 166 | } 167 | 168 | const ( 169 | //Allow Policy allows policy 170 | AllowPolicy Effect = "Allow" 171 | 172 | //DenyPolicy denies policy 173 | DenyPolicy Effect = "Deny" 174 | ) 175 | 176 | // IamroleStatus defines the observed state of Iamrole 177 | type IamroleStatus struct { 178 | //RoleName represents the name of the iam role created in AWS 179 | RoleName string `json:"roleName,omitempty"` 180 | //RoleARN represents the ARN of an IAM role 181 | RoleARN string `json:"roleARN,omitempty"` 182 | //RoleID represents the unique ID of the role which can be used in S3 policy etc 183 | RoleID string `json:"roleID,omitempty"` 184 | //State of the resource 185 | State State `json:"state,omitempty"` 186 | //RetryCount in case of error 187 | RetryCount int `json:"retryCount"` 188 | //ErrorDescription in case of error 189 | // +optional 190 | ErrorDescription string `json:"errorDescription,omitempty"` 191 | //LastUpdatedTimestamp represents the last time the iam role has been modified 192 | // +optional 193 | LastUpdatedTimestamp metav1.Time `json:"lastUpdatedTimestamp,omitempty"` 194 | } 195 | 196 | type State string 197 | 198 | const ( 199 | Ready State = "Ready" 200 | Error State = "Error" 201 | PolicyNotAllowed State = "PolicyNotAllowed" 202 | RolesMaxLimitReached State = "RolesMaxLimitReached" 203 | RoleNameNotAvailable State = "RoleNameNotAvailable" 204 | ) 205 | 206 | // +kubebuilder:object:root=true 207 | // +kubebuilder:subresource:status 208 | // +kubebuilder:resource:path=iamroles,scope=Namespaced,shortName=iam,singular=iamrole 209 | // +kubebuilder:printcolumn:name="State",type="string",JSONPath=".status.state",description="current state of the iam role" 210 | // +kubebuilder:printcolumn:name="RoleName",type="string",JSONPath=".status.roleName",description="Name of the role" 211 | // +kubebuilder:printcolumn:name="RetryCount",type="integer",JSONPath=".status.retryCount",description="Retry count" 212 | // +kubebuilder:printcolumn:name="LastUpdatedTimestamp",type="string",format="date-time",JSONPath=".status.lastUpdatedTimestamp",description="last updated iam role timestamp" 213 | // +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="time passed since iamrole creation" 214 | // Iamrole is the Schema for the iamroles API 215 | type Iamrole struct { 216 | metav1.TypeMeta `json:",inline"` 217 | metav1.ObjectMeta `json:"metadata,omitempty"` 218 | 219 | Spec IamroleSpec `json:"spec,omitempty"` 220 | Status IamroleStatus `json:"status,omitempty"` 221 | } 222 | 223 | // +kubebuilder:object:root=true 224 | 225 | // IamroleList contains a list of Iamrole 226 | type IamroleList struct { 227 | metav1.TypeMeta `json:",inline"` 228 | metav1.ListMeta `json:"metadata,omitempty"` 229 | Items []Iamrole `json:"items"` 230 | } 231 | 232 | func init() { 233 | SchemeBuilder.Register(&Iamrole{}, &IamroleList{}) 234 | } 235 | -------------------------------------------------------------------------------- /api/v1alpha1/zz_generated.deepcopy.go: -------------------------------------------------------------------------------- 1 | //go:build !ignore_autogenerated 2 | 3 | /* 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | 18 | // Code generated by controller-gen. DO NOT EDIT. 19 | 20 | package v1alpha1 21 | 22 | import ( 23 | "k8s.io/apimachinery/pkg/runtime" 24 | ) 25 | 26 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 27 | func (in *AssumeRolePolicyDocument) DeepCopyInto(out *AssumeRolePolicyDocument) { 28 | *out = *in 29 | if in.Statement != nil { 30 | in, out := &in.Statement, &out.Statement 31 | *out = make([]TrustPolicyStatement, len(*in)) 32 | for i := range *in { 33 | (*in)[i].DeepCopyInto(&(*out)[i]) 34 | } 35 | } 36 | } 37 | 38 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AssumeRolePolicyDocument. 39 | func (in *AssumeRolePolicyDocument) DeepCopy() *AssumeRolePolicyDocument { 40 | if in == nil { 41 | return nil 42 | } 43 | out := new(AssumeRolePolicyDocument) 44 | in.DeepCopyInto(out) 45 | return out 46 | } 47 | 48 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 49 | func (in *Condition) DeepCopyInto(out *Condition) { 50 | *out = *in 51 | if in.StringEquals != nil { 52 | in, out := &in.StringEquals, &out.StringEquals 53 | *out = make(map[string]string, len(*in)) 54 | for key, val := range *in { 55 | (*out)[key] = val 56 | } 57 | } 58 | if in.StringLike != nil { 59 | in, out := &in.StringLike, &out.StringLike 60 | *out = make(map[string]string, len(*in)) 61 | for key, val := range *in { 62 | (*out)[key] = val 63 | } 64 | } 65 | } 66 | 67 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Condition. 68 | func (in *Condition) DeepCopy() *Condition { 69 | if in == nil { 70 | return nil 71 | } 72 | out := new(Condition) 73 | in.DeepCopyInto(out) 74 | return out 75 | } 76 | 77 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 78 | func (in *Iamrole) DeepCopyInto(out *Iamrole) { 79 | *out = *in 80 | out.TypeMeta = in.TypeMeta 81 | in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) 82 | in.Spec.DeepCopyInto(&out.Spec) 83 | in.Status.DeepCopyInto(&out.Status) 84 | } 85 | 86 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Iamrole. 87 | func (in *Iamrole) DeepCopy() *Iamrole { 88 | if in == nil { 89 | return nil 90 | } 91 | out := new(Iamrole) 92 | in.DeepCopyInto(out) 93 | return out 94 | } 95 | 96 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 97 | func (in *Iamrole) DeepCopyObject() runtime.Object { 98 | if c := in.DeepCopy(); c != nil { 99 | return c 100 | } 101 | return nil 102 | } 103 | 104 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 105 | func (in *IamroleList) DeepCopyInto(out *IamroleList) { 106 | *out = *in 107 | out.TypeMeta = in.TypeMeta 108 | in.ListMeta.DeepCopyInto(&out.ListMeta) 109 | if in.Items != nil { 110 | in, out := &in.Items, &out.Items 111 | *out = make([]Iamrole, len(*in)) 112 | for i := range *in { 113 | (*in)[i].DeepCopyInto(&(*out)[i]) 114 | } 115 | } 116 | } 117 | 118 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IamroleList. 119 | func (in *IamroleList) DeepCopy() *IamroleList { 120 | if in == nil { 121 | return nil 122 | } 123 | out := new(IamroleList) 124 | in.DeepCopyInto(out) 125 | return out 126 | } 127 | 128 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 129 | func (in *IamroleList) DeepCopyObject() runtime.Object { 130 | if c := in.DeepCopy(); c != nil { 131 | return c 132 | } 133 | return nil 134 | } 135 | 136 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 137 | func (in *IamroleSpec) DeepCopyInto(out *IamroleSpec) { 138 | *out = *in 139 | in.PolicyDocument.DeepCopyInto(&out.PolicyDocument) 140 | if in.AssumeRolePolicyDocument != nil { 141 | in, out := &in.AssumeRolePolicyDocument, &out.AssumeRolePolicyDocument 142 | *out = new(AssumeRolePolicyDocument) 143 | (*in).DeepCopyInto(*out) 144 | } 145 | } 146 | 147 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IamroleSpec. 148 | func (in *IamroleSpec) DeepCopy() *IamroleSpec { 149 | if in == nil { 150 | return nil 151 | } 152 | out := new(IamroleSpec) 153 | in.DeepCopyInto(out) 154 | return out 155 | } 156 | 157 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 158 | func (in *IamroleStatus) DeepCopyInto(out *IamroleStatus) { 159 | *out = *in 160 | in.LastUpdatedTimestamp.DeepCopyInto(&out.LastUpdatedTimestamp) 161 | } 162 | 163 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IamroleStatus. 164 | func (in *IamroleStatus) DeepCopy() *IamroleStatus { 165 | if in == nil { 166 | return nil 167 | } 168 | out := new(IamroleStatus) 169 | in.DeepCopyInto(out) 170 | return out 171 | } 172 | 173 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 174 | func (in *PolicyDocument) DeepCopyInto(out *PolicyDocument) { 175 | *out = *in 176 | if in.Statement != nil { 177 | in, out := &in.Statement, &out.Statement 178 | *out = make([]Statement, len(*in)) 179 | for i := range *in { 180 | (*in)[i].DeepCopyInto(&(*out)[i]) 181 | } 182 | } 183 | } 184 | 185 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PolicyDocument. 186 | func (in *PolicyDocument) DeepCopy() *PolicyDocument { 187 | if in == nil { 188 | return nil 189 | } 190 | out := new(PolicyDocument) 191 | in.DeepCopyInto(out) 192 | return out 193 | } 194 | 195 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 196 | func (in *Principal) DeepCopyInto(out *Principal) { 197 | *out = *in 198 | if in.AWS != nil { 199 | in, out := &in.AWS, &out.AWS 200 | *out = make(StringOrStrings, len(*in)) 201 | copy(*out, *in) 202 | } 203 | } 204 | 205 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Principal. 206 | func (in *Principal) DeepCopy() *Principal { 207 | if in == nil { 208 | return nil 209 | } 210 | out := new(Principal) 211 | in.DeepCopyInto(out) 212 | return out 213 | } 214 | 215 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 216 | func (in *Statement) DeepCopyInto(out *Statement) { 217 | *out = *in 218 | if in.Action != nil { 219 | in, out := &in.Action, &out.Action 220 | *out = make([]string, len(*in)) 221 | copy(*out, *in) 222 | } 223 | if in.Resource != nil { 224 | in, out := &in.Resource, &out.Resource 225 | *out = make([]string, len(*in)) 226 | copy(*out, *in) 227 | } 228 | } 229 | 230 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Statement. 231 | func (in *Statement) DeepCopy() *Statement { 232 | if in == nil { 233 | return nil 234 | } 235 | out := new(Statement) 236 | in.DeepCopyInto(out) 237 | return out 238 | } 239 | 240 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 241 | func (in *TrustPolicyStatement) DeepCopyInto(out *TrustPolicyStatement) { 242 | *out = *in 243 | in.Principal.DeepCopyInto(&out.Principal) 244 | if in.Condition != nil { 245 | in, out := &in.Condition, &out.Condition 246 | *out = new(Condition) 247 | (*in).DeepCopyInto(*out) 248 | } 249 | } 250 | 251 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TrustPolicyStatement. 252 | func (in *TrustPolicyStatement) DeepCopy() *TrustPolicyStatement { 253 | if in == nil { 254 | return nil 255 | } 256 | out := new(TrustPolicyStatement) 257 | in.DeepCopyInto(out) 258 | return out 259 | } 260 | -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | 16 | package main 17 | 18 | import ( 19 | "context" 20 | "flag" 21 | "os" 22 | 23 | // +kubebuilder:scaffold:imports 24 | "k8s.io/apimachinery/pkg/runtime" 25 | clientgoscheme "k8s.io/client-go/kubernetes/scheme" 26 | _ "k8s.io/client-go/plugin/pkg/client/auth/gcp" 27 | ctrl "sigs.k8s.io/controller-runtime" 28 | "sigs.k8s.io/controller-runtime/pkg/manager" 29 | 30 | "sigs.k8s.io/controller-runtime/pkg/metrics/filters" 31 | metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" 32 | "sigs.k8s.io/controller-runtime/pkg/webhook" 33 | 34 | // +kubebuilder:scaffold:imports 35 | 36 | iammanagerv1alpha1 "github.com/keikoproj/iam-manager/api/v1alpha1" 37 | "github.com/keikoproj/iam-manager/internal/config" 38 | "github.com/keikoproj/iam-manager/internal/controllers" 39 | "github.com/keikoproj/iam-manager/internal/utils" 40 | "github.com/keikoproj/iam-manager/pkg/awsapi" 41 | "github.com/keikoproj/iam-manager/pkg/k8s" 42 | "github.com/keikoproj/iam-manager/pkg/logging" 43 | ) 44 | 45 | var ( 46 | scheme = runtime.NewScheme() 47 | ) 48 | 49 | func init() { 50 | _ = clientgoscheme.AddToScheme(scheme) 51 | 52 | _ = iammanagerv1alpha1.AddToScheme(scheme) 53 | // +kubebuilder:scaffold:scheme 54 | } 55 | 56 | func main() { 57 | var metricsAddr string 58 | var enableLeaderElection bool 59 | var debug bool 60 | flag.StringVar(&metricsAddr, "metrics-addr", ":8080", "The address the metric endpoint binds to.") 61 | flag.BoolVar(&enableLeaderElection, "enable-leader-election", false, 62 | "Enable leader election for controller manager. Enabling this will ensure there is only one active controller manager.") 63 | flag.BoolVar(&debug, "debug", false, "Enable Debug?") 64 | flag.Parse() 65 | 66 | logging.New() 67 | log := logging.Logger(context.Background(), "main", "setup") 68 | 69 | go config.RunConfigMapInformer(context.Background()) 70 | 71 | mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ 72 | Scheme: scheme, 73 | Metrics: metricsserver.Options{ 74 | BindAddress: metricsAddr, 75 | SecureServing: true, 76 | FilterProvider: filters.WithAuthenticationAndAuthorization, 77 | }, 78 | LeaderElection: enableLeaderElection, 79 | WebhookServer: webhook.NewServer(webhook.Options{Port: 9443}), 80 | LeaderElectionID: "controller-leader-election-helper", 81 | }) 82 | 83 | if err != nil { 84 | log.Error(err, "unable to start manager") 85 | os.Exit(1) 86 | } 87 | 88 | log.V(1).Info("Setting up reconciler with manager") 89 | log.Info("region ", "region", config.Props.AWSRegion()) 90 | 91 | iamClient := awsapi.NewIAM(config.Props.AWSRegion()) 92 | if err := handleOIDCSetupForIRSA(context.Background(), iamClient); err != nil { 93 | log.Error(err, "unable to complete/verify oidc setup for IRSA") 94 | } 95 | 96 | controller := &controllers.IamroleReconciler{ 97 | Client: mgr.GetClient(), 98 | IAMClient: iamClient, 99 | Recorder: k8s.NewK8sClientDoOrDie().SetUpEventHandler(context.Background()), 100 | } 101 | 102 | if err = controller.SetupWithManager(mgr); err != nil { 103 | log.Error(err, "unable to create controller", "controller", "Iamrole") 104 | os.Exit(1) 105 | } 106 | 107 | // Add another runnable to the manager, it will run concurrently with the main controller thread 108 | if err = mgr.Add(manager.RunnableFunc(func(ctx context.Context) error { 109 | return controller.StartControllerReconcileCronJob(ctx) 110 | })); err != nil { 111 | log.Error(err, "unable to add StartControllerReconcileCronJob runnable to manager") 112 | os.Exit(1) 113 | } 114 | 115 | //Get the client 116 | iammanagerv1alpha1.NewWClient() 117 | if config.Props.IsWebHookEnabled() { 118 | log.Info("Registering webhook") 119 | if err = (&iammanagerv1alpha1.Iamrole{}).SetupWebhookWithManager(mgr); err != nil { 120 | log.Error(err, "unable to create webhook", "webhook", "Iamrole") 121 | os.Exit(1) 122 | } 123 | } 124 | 125 | // +kubebuilder:scaffold:builder 126 | 127 | log.Info("Registering controller") 128 | if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { 129 | log.Error(err, "problem running manager") 130 | os.Exit(1) 131 | } 132 | } 133 | 134 | // handleOIDCSetupForIRSA will be used to setup the OIDC in AWS IAM 135 | func handleOIDCSetupForIRSA(ctx context.Context, iamClient *awsapi.IAM) error { 136 | log := logging.Logger(ctx, "main", "handleOIDCSetupForIRSA") 137 | 138 | //Creating OIDC provider if config map has an entry 139 | 140 | if config.Props.IsIRSAEnabled() { 141 | //Fetch cert thumb print 142 | thumbprint, err := utils.GetIdpServerCertThumbprint(context.Background(), config.Props.OIDCIssuerUrl()) 143 | if err != nil { 144 | log.Error(err, "unable to get the OIDC IDP server thumbprint") 145 | return err 146 | } 147 | 148 | err = iamClient.CreateOIDCProvider(ctx, config.Props.OIDCIssuerUrl(), config.OIDCAudience, thumbprint) 149 | if err != nil { 150 | log.Error(err, "unable to setup OIDC with the url", "url", config.Props.OIDCIssuerUrl()) 151 | return err 152 | } 153 | log.Info("OIDC provider setup is successfully completed") 154 | 155 | } 156 | 157 | return nil 158 | } 159 | -------------------------------------------------------------------------------- /cmd/main_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | 16 | package main_test 17 | 18 | import ( 19 | "flag" 20 | "os" 21 | "testing" 22 | 23 | "sigs.k8s.io/controller-runtime/pkg/metrics/filters" 24 | metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" 25 | ) 26 | 27 | // TestMetricsSecureConfiguration tests that the metrics server is correctly configured 28 | // with authentication and authorization 29 | func TestMetricsSecureConfiguration(t *testing.T) { 30 | // Define test parameters 31 | metricsAddr := ":8443" 32 | 33 | // Create metrics options similar to those in main() 34 | metricsOpts := metricsserver.Options{ 35 | BindAddress: metricsAddr, 36 | SecureServing: true, 37 | FilterProvider: filters.WithAuthenticationAndAuthorization, 38 | } 39 | 40 | // Verify the metrics configuration 41 | if metricsOpts.BindAddress != metricsAddr { 42 | t.Errorf("Expected BindAddress to be %s, got %s", metricsAddr, metricsOpts.BindAddress) 43 | } 44 | 45 | if !metricsOpts.SecureServing { 46 | t.Errorf("Expected SecureServing to be true, got false") 47 | } 48 | 49 | // Can't directly compare function values with Equal, so just verify it's not nil 50 | if metricsOpts.FilterProvider == nil { 51 | t.Errorf("Expected FilterProvider to be set, got nil") 52 | } 53 | } 54 | 55 | // TestCommandLineFlagParsing tests that command-line flags are correctly defined and parsed 56 | func TestCommandLineFlagParsing(t *testing.T) { 57 | // Save original command-line arguments 58 | originalArgs := os.Args 59 | defer func() { 60 | os.Args = originalArgs 61 | flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ContinueOnError) 62 | }() 63 | 64 | // Set up test command-line arguments 65 | os.Args = []string{"cmd", "--metrics-addr=:9443", "--enable-leader-election=true", "--debug=true"} 66 | 67 | // Create a new flag set 68 | fs := flag.NewFlagSet("test", flag.ContinueOnError) 69 | 70 | // Define flags similar to those in main() 71 | var metricsAddr string 72 | var enableLeaderElection bool 73 | var debug bool 74 | fs.StringVar(&metricsAddr, "metrics-addr", ":8080", "The address the metric endpoint binds to.") 75 | fs.BoolVar(&enableLeaderElection, "enable-leader-election", false, "Enable leader election for controller manager.") 76 | fs.BoolVar(&debug, "debug", false, "Enable Debug?") 77 | 78 | // Parse flags 79 | if err := fs.Parse(os.Args[1:]); err != nil { 80 | t.Fatalf("Failed to parse flags: %v", err) 81 | } 82 | 83 | // Verify flags are parsed correctly 84 | if metricsAddr != ":9443" { 85 | t.Errorf("Expected metrics-addr flag to be ':9443', got '%s'", metricsAddr) 86 | } 87 | 88 | if !enableLeaderElection { 89 | t.Errorf("Expected enable-leader-election flag to be true, got false") 90 | } 91 | 92 | if !debug { 93 | t.Errorf("Expected debug flag to be true, got false") 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | range: "50...90" 3 | status: 4 | patch: off 5 | 6 | ignore: 7 | - "**/zz_generated.*" 8 | - "bin" 9 | - "config" 10 | - "hack" 11 | - "docs" 12 | - "**/mocks/" -------------------------------------------------------------------------------- /config/certmanager/certificate.yaml: -------------------------------------------------------------------------------- 1 | # The following manifests contain a self-signed issuer CR and a certificate CR. 2 | # More document can be found at https://docs.cert-manager.io 3 | apiVersion: cert-manager.io/v1alpha2 4 | kind: Issuer 5 | metadata: 6 | name: selfsigned-issuer 7 | namespace: system 8 | spec: 9 | selfSigned: {} 10 | --- 11 | apiVersion: cert-manager.io/v1alpha2 12 | kind: Certificate 13 | metadata: 14 | name: serving-cert # this name should match the one appeared in kustomizeconfig.yaml 15 | namespace: system 16 | spec: 17 | # $(SERVICE_NAME) and $(SERVICE_NAMESPACE) will be substituted by kustomize 18 | commonName: $(SERVICE_NAME).$(SERVICE_NAMESPACE).svc 19 | dnsNames: 20 | - $(SERVICE_NAME).$(SERVICE_NAMESPACE).svc 21 | - $(SERVICE_NAME).$(SERVICE_NAMESPACE).svc.cluster.local 22 | issuerRef: 23 | kind: Issuer 24 | name: selfsigned-issuer 25 | secretName: webhook-server-cert # this secret will not be prefixed, since it's not managed by kustomize 26 | -------------------------------------------------------------------------------- /config/certmanager/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - certificate.yaml 3 | 4 | configurations: 5 | - kustomizeconfig.yaml 6 | -------------------------------------------------------------------------------- /config/certmanager/kustomizeconfig.yaml: -------------------------------------------------------------------------------- 1 | # This configuration is for teaching kustomize how to update name ref and var substitution 2 | nameReference: 3 | - kind: Issuer 4 | group: cert-manager.io 5 | fieldSpecs: 6 | - kind: Certificate 7 | group: cert-manager.io 8 | path: spec/issuerRef/name 9 | 10 | varReference: 11 | - kind: Certificate 12 | group: cert-manager.io 13 | path: spec/commonName 14 | - kind: Certificate 15 | group: cert-manager.io 16 | path: spec/dnsNames 17 | -------------------------------------------------------------------------------- /config/crd/bases/iammanager.keikoproj.io_iamroles-configmap.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: iamroles-v1alpha1-configmap 5 | namespace: dev 6 | data: 7 | iam.policy.action.prefix.whitelist: "s3:,sts:,ec2:Describe,acm:Describe,acm:List,acm:Get,route53:Get,route53:List,route53:Create,route53:Delete,route53:Change,kms:Decrypt,kms:Encrypt,kms:ReEncrypt,kms:GenerateDataKey,kms:DescribeKey,dynamodb:,secretsmanager:GetSecretValue,es:,sqs:SendMessage,sqs:ReceiveMessage,sqs:DeleteMessage,SNS:Publish,sqs:GetQueueAttributes,sqs:GetQueueUrl" 8 | iam.policy.resource.blacklist: "kops" 9 | iam.policy.s3.restricted.resource: "*" 10 | aws.accountId: "000011112222" 11 | aws.MasterRole: "masters.cluster.k8s.local" 12 | iam.managed.policies: "shared.cluster.k8s.local" 13 | iam.managed.permission.boundary.policy: "iam-manager-permission-boundary" -------------------------------------------------------------------------------- /config/crd/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # This kustomization.yaml is not intended to be run by itself, 2 | # since it depends on service name and namespace that are out of this kustomize package. 3 | # It should be run by config/default 4 | resources: 5 | - bases/iammanager.keikoproj.io_iamroles.yaml 6 | - bases/iammanager.keikoproj.io_iamroles-configmap.yaml 7 | # +kubebuilder:scaffold:crdkustomizeresource 8 | 9 | patchesStrategicMerge: 10 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. 11 | # patches here are for enabling the conversion webhook for each CRD 12 | - patches/webhook_in_iamroles.yaml 13 | # +kubebuilder:scaffold:crdkustomizewebhookpatch 14 | 15 | # [CERTMANAGER] To enable webhook, uncomment all the sections with [CERTMANAGER] prefix. 16 | # patches here are for enabling the CA injection for each CRD 17 | - patches/cainjection_in_iamroles.yaml 18 | # +kubebuilder:scaffold:crdkustomizecainjectionpatch 19 | 20 | # the following config is for teaching kustomize how to do kustomization for CRDs. 21 | configurations: 22 | - kustomizeconfig.yaml 23 | -------------------------------------------------------------------------------- /config/crd/kustomizeconfig.yaml: -------------------------------------------------------------------------------- 1 | # This file is for teaching kustomize how to substitute name and namespace reference in CRD 2 | nameReference: 3 | - kind: Service 4 | version: v1 5 | fieldSpecs: 6 | - kind: CustomResourceDefinition 7 | group: apiextensions.k8s.io 8 | path: spec/conversion/webhookClientConfig/service/name 9 | 10 | namespace: 11 | - kind: CustomResourceDefinition 12 | group: apiextensions.k8s.io 13 | path: spec/conversion/webhookClientConfig/service/namespace 14 | create: false 15 | 16 | varReference: 17 | - path: metadata/annotations 18 | -------------------------------------------------------------------------------- /config/crd/patches/cainjection_in_iamroles.yaml: -------------------------------------------------------------------------------- 1 | # The following patch adds a directive for certmanager to inject CA into the CRD 2 | # CRD conversion requires k8s 1.13 or later. 3 | apiVersion: apiextensions.k8s.io/v1 4 | kind: CustomResourceDefinition 5 | metadata: 6 | annotations: 7 | cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) 8 | name: iamroles.iammanager.keikoproj.io 9 | -------------------------------------------------------------------------------- /config/crd/patches/webhook_in_iamroles.yaml: -------------------------------------------------------------------------------- 1 | # The following patch enables conversion webhook for CRD 2 | # CRD conversion requires k8s 1.13 or later. 3 | apiVersion: apiextensions.k8s.io/v1 4 | kind: CustomResourceDefinition 5 | metadata: 6 | name: iamroles.iammanager.keikoproj.io 7 | #spec: 8 | # conversion: 9 | # strategy: Webhook 10 | # webhookClientConfig: 11 | # # this is "\n" used as a placeholder, otherwise it will be rejected by the apiserver for being blank, 12 | # # but we're going to set it later using the cert-manager (or potentially a patch if not using cert-manager) 13 | # caBundle: Cg== 14 | # service: 15 | # namespace: system 16 | # name: webhook-service 17 | # path: /convert 18 | -------------------------------------------------------------------------------- /config/crd_no_webhook/bases/iammanager.keikoproj.io_iamroles-configmap.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: iamroles-v1alpha1-configmap 5 | namespace: dev 6 | data: 7 | iam.policy.action.prefix.whitelist: "s3:,sts:,ec2:Describe,acm:Describe,acm:List,acm:Get,route53:Get,route53:List,route53:Create,route53:Delete,route53:Change,kms:Decrypt,kms:Encrypt,kms:ReEncrypt,kms:GenerateDataKey,kms:DescribeKey,dynamodb:,secretsmanager:GetSecretValue,es:,sqs:SendMessage,sqs:ReceiveMessage,sqs:DeleteMessage,SNS:Publish,sqs:GetQueueAttributes,sqs:GetQueueUrl" 8 | iam.policy.resource.blacklist: "kops" 9 | iam.policy.s3.restricted.resource: "*" 10 | aws.accountId: "000011112222" 11 | aws.MasterRole: "masters.cluster.k8s.local" 12 | iam.managed.policies: "shared.cluster.k8s.local" 13 | iam.managed.permission.boundary.policy: "iam-manager-permission-boundary" -------------------------------------------------------------------------------- /config/crd_no_webhook/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # This kustomization.yaml is not intended to be run by itself, 2 | # since it depends on service name and namespace that are out of this kustomize package. 3 | # It should be run by config/default 4 | resources: 5 | - bases/iammanager.keikoproj.io_iamroles.yaml 6 | #- bases/iammanager.keikoproj.io_iamroles-configmap.yaml 7 | # +kubebuilder:scaffold:crdkustomizeresource 8 | 9 | patchesStrategicMerge: 10 | 11 | -------------------------------------------------------------------------------- /config/default/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # Adds namespace to all resources. 2 | namespace: iam-manager-system 3 | 4 | # Value of this field is prepended to the 5 | # names of all resources, e.g. a deployment named 6 | # "wordpress" becomes "alices-wordpress". 7 | # Note that it should also match with the prefix (text before '-') of the namespace 8 | # field above. 9 | namePrefix: iam-manager- 10 | 11 | # Labels to add to all resources and selectors. 12 | commonLabels: 13 | app: iam-manager 14 | 15 | bases: 16 | - ../crd 17 | - ../rbac 18 | - ../manager 19 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in crd/kustomization.yaml 20 | - ../webhook 21 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required. 22 | - ../certmanager 23 | 24 | patchesStrategicMerge: 25 | # Protect the /metrics endpoint by putting it behind auth. 26 | # Only one of manager_auth_proxy_patch.yaml and 27 | # manager_prometheus_metrics_patch.yaml should be enabled. 28 | #- manager_auth_proxy_patch.yaml 29 | # If you want your controller-manager to expose the /metrics 30 | # endpoint w/o any authn/z, uncomment the following line and 31 | # comment manager_auth_proxy_patch.yaml. 32 | # Only one of manager_auth_proxy_patch.yaml and 33 | # manager_prometheus_metrics_patch.yaml should be enabled. 34 | #- manager_prometheus_metrics_patch.yaml 35 | 36 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in crd/kustomization.yaml 37 | - manager_webhook_patch.yaml 38 | 39 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 40 | # Uncomment 'CERTMANAGER' sections in crd/kustomization.yaml to enable the CA injection in the admission webhooks. 41 | # 'CERTMANAGER' needs to be enabled to use ca injection 42 | - webhookcainjection_patch.yaml 43 | 44 | # the following config is for teaching kustomize how to do var substitution 45 | vars: 46 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix. 47 | - name: CERTIFICATE_NAMESPACE # namespace of the certificate CR 48 | objref: 49 | kind: Certificate 50 | group: cert-manager.io 51 | version: v1alpha2 52 | name: serving-cert # this name should match the one in certificate.yaml 53 | fieldref: 54 | fieldpath: metadata.namespace 55 | - name: CERTIFICATE_NAME 56 | objref: 57 | kind: Certificate 58 | group: cert-manager.io 59 | version: v1alpha2 60 | name: serving-cert # this name should match the one in certificate.yaml 61 | - name: SERVICE_NAMESPACE # namespace of the service 62 | objref: 63 | kind: Service 64 | version: v1 65 | name: webhook-service 66 | fieldref: 67 | fieldpath: metadata.namespace 68 | - name: SERVICE_NAME 69 | objref: 70 | kind: Service 71 | version: v1 72 | name: webhook-service 73 | -------------------------------------------------------------------------------- /config/default/manager_auth_proxy_patch.yaml: -------------------------------------------------------------------------------- 1 | # This patch inject a sidecar container which is a HTTP proxy for the controller manager, 2 | # it performs RBAC authorization against the Kubernetes API using SubjectAccessReviews. 3 | apiVersion: apps/v1 4 | kind: Deployment 5 | metadata: 6 | name: controller-manager 7 | namespace: system 8 | spec: 9 | template: 10 | spec: 11 | containers: 12 | - name: manager 13 | args: 14 | - "--metrics-addr=:8443" 15 | - "--enable-leader-election" 16 | ports: 17 | - containerPort: 8443 18 | name: https 19 | protocol: TCP 20 | -------------------------------------------------------------------------------- /config/default/manager_prometheus_metrics_patch.yaml: -------------------------------------------------------------------------------- 1 | # This patch enables Prometheus scraping for the manager pod. 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | name: controller-manager 6 | namespace: system 7 | spec: 8 | template: 9 | metadata: 10 | annotations: 11 | prometheus.io/scrape: 'true' 12 | spec: 13 | containers: 14 | # Expose the prometheus metrics on default port 15 | - name: manager 16 | ports: 17 | - containerPort: 8080 18 | name: metrics 19 | protocol: TCP 20 | -------------------------------------------------------------------------------- /config/default/manager_webhook_patch.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: controller-manager 5 | namespace: system 6 | spec: 7 | template: 8 | spec: 9 | containers: 10 | - name: manager 11 | ports: 12 | - containerPort: 9443 13 | name: webhook-server 14 | protocol: TCP 15 | volumeMounts: 16 | - mountPath: /tmp/k8s-webhook-server/serving-certs 17 | name: cert 18 | readOnly: true 19 | volumes: 20 | - name: cert 21 | secret: 22 | defaultMode: 420 23 | secretName: webhook-server-cert 24 | -------------------------------------------------------------------------------- /config/default/webhookcainjection_patch.yaml: -------------------------------------------------------------------------------- 1 | # This patch add annotation to admission webhook config and 2 | # the variables $(CERTIFICATE_NAMESPACE) and $(CERTIFICATE_NAME) will be substituted by kustomize. 3 | apiVersion: admissionregistration.k8s.io/v1 4 | kind: MutatingWebhookConfiguration 5 | metadata: 6 | name: mutating-webhook-configuration 7 | annotations: 8 | cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) 9 | --- 10 | apiVersion: admissionregistration.k8s.io/v1 11 | kind: ValidatingWebhookConfiguration 12 | metadata: 13 | name: validating-webhook-configuration 14 | annotations: 15 | cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) 16 | -------------------------------------------------------------------------------- /config/default_no_webhook/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # Adds namespace to all resources. 2 | namespace: iam-manager-system 3 | 4 | # Value of this field is prepended to the 5 | # names of all resources, e.g. a deployment named 6 | # "wordpress" becomes "alices-wordpress". 7 | # Note that it should also match with the prefix (text before '-') of the namespace 8 | # field above. 9 | namePrefix: iam-manager- 10 | 11 | # Labels to add to all resources and selectors. 12 | commonLabels: 13 | app: iam-manager 14 | 15 | bases: 16 | - ../crd_no_webhook 17 | - ../rbac 18 | - ../manager 19 | 20 | patchesStrategicMerge: 21 | # Protect the /metrics endpoint by putting it behind auth. 22 | # Only one of manager_auth_proxy_patch.yaml and 23 | # manager_prometheus_metrics_patch.yaml should be enabled. 24 | #- manager_auth_proxy_patch.yaml 25 | # If you want your controller-manager to expose the /metrics 26 | # endpoint w/o any authn/z, uncomment the following line and 27 | # comment manager_auth_proxy_patch.yaml. 28 | # Only one of manager_auth_proxy_patch.yaml and 29 | # manager_prometheus_metrics_patch.yaml should be enabled. 30 | #- manager_prometheus_metrics_patch.yaml 31 | 32 | # the following config is for teaching kustomize how to do var substitution 33 | vars: 34 | 35 | -------------------------------------------------------------------------------- /config/default_no_webhook/manager_auth_proxy_patch.yaml: -------------------------------------------------------------------------------- 1 | # This patch inject a sidecar container which is a HTTP proxy for the controller manager, 2 | # it performs RBAC authorization against the Kubernetes API using SubjectAccessReviews. 3 | apiVersion: apps/v1 4 | kind: Deployment 5 | metadata: 6 | name: controller-manager 7 | namespace: system 8 | spec: 9 | template: 10 | spec: 11 | containers: 12 | - name: manager 13 | args: 14 | - "--metrics-addr=:8443" 15 | - "--enable-leader-election" 16 | ports: 17 | - containerPort: 8443 18 | name: https 19 | protocol: TCP 20 | -------------------------------------------------------------------------------- /config/default_no_webhook/manager_prometheus_metrics_patch.yaml: -------------------------------------------------------------------------------- 1 | # This patch enables Prometheus scraping for the manager pod. 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | name: controller-manager 6 | namespace: system 7 | spec: 8 | template: 9 | metadata: 10 | annotations: 11 | prometheus.io/scrape: 'true' 12 | spec: 13 | containers: 14 | # Expose the prometheus metrics on default port 15 | - name: manager 16 | ports: 17 | - containerPort: 8080 18 | name: metrics 19 | protocol: TCP 20 | -------------------------------------------------------------------------------- /config/manager/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - manager.yaml 3 | apiVersion: kustomize.config.k8s.io/v1beta1 4 | kind: Kustomization 5 | images: 6 | - name: controller 7 | newName: keikoproj/iam-manager 8 | newTag: latest 9 | -------------------------------------------------------------------------------- /config/manager/manager.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | labels: 5 | control-plane: controller-manager 6 | name: system 7 | --- 8 | apiVersion: apps/v1 9 | kind: Deployment 10 | metadata: 11 | name: controller-manager 12 | namespace: system 13 | labels: 14 | control-plane: controller-manager 15 | spec: 16 | selector: 17 | matchLabels: 18 | control-plane: controller-manager 19 | replicas: 1 20 | template: 21 | metadata: 22 | labels: 23 | control-plane: controller-manager 24 | annotations: 25 | iam.amazonaws.com/role: k8s-cluster-iam-manager-role 26 | spec: 27 | containers: 28 | - command: 29 | - /manager 30 | args: 31 | - --enable-leader-election 32 | image: controller:latest 33 | name: manager 34 | resources: 35 | limits: 36 | cpu: 100m 37 | memory: 30Mi 38 | requests: 39 | cpu: 100m 40 | memory: 20Mi 41 | terminationGracePeriodSeconds: 10 42 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_role.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: proxy-role 5 | rules: 6 | - apiGroups: ["authentication.k8s.io"] 7 | resources: 8 | - tokenreviews 9 | verbs: ["create"] 10 | - apiGroups: ["authorization.k8s.io"] 11 | resources: 12 | - subjectaccessreviews 13 | verbs: ["create"] 14 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | name: proxy-rolebinding 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: ClusterRole 8 | name: proxy-role 9 | subjects: 10 | - kind: ServiceAccount 11 | name: default 12 | namespace: system 13 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | annotations: 5 | prometheus.io/port: "8443" 6 | prometheus.io/scheme: https 7 | prometheus.io/scrape: "true" 8 | labels: 9 | control-plane: controller-manager 10 | name: controller-manager-metrics-service 11 | namespace: system 12 | spec: 13 | ports: 14 | - name: https 15 | port: 8443 16 | targetPort: https 17 | selector: 18 | control-plane: controller-manager 19 | -------------------------------------------------------------------------------- /config/rbac/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - role.yaml 3 | - role_binding.yaml 4 | - leader_election_role.yaml 5 | - leader_election_role_binding.yaml 6 | # Comment the following 3 lines if you want to disable 7 | # the auth proxy (https://github.com/brancz/kube-rbac-proxy) 8 | # which protects your /metrics endpoint. 9 | - auth_proxy_service.yaml 10 | - auth_proxy_role.yaml 11 | - auth_proxy_role_binding.yaml 12 | -------------------------------------------------------------------------------- /config/rbac/leader_election_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions to do leader election. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: Role 4 | metadata: 5 | name: leader-election-role 6 | rules: 7 | - apiGroups: 8 | - "" 9 | resources: 10 | - configmaps 11 | verbs: 12 | - get 13 | - list 14 | - watch 15 | - create 16 | - update 17 | - patch 18 | - delete 19 | - apiGroups: 20 | - "" 21 | resources: 22 | - configmaps/status 23 | verbs: 24 | - get 25 | - update 26 | - patch 27 | - apiGroups: 28 | - "" 29 | resources: 30 | - events 31 | verbs: 32 | - create 33 | -------------------------------------------------------------------------------- /config/rbac/leader_election_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: RoleBinding 3 | metadata: 4 | name: leader-election-rolebinding 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: Role 8 | name: leader-election-role 9 | subjects: 10 | - kind: ServiceAccount 11 | name: default 12 | namespace: system 13 | -------------------------------------------------------------------------------- /config/rbac/role.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: manager-role 6 | rules: 7 | - apiGroups: 8 | - "" 9 | resources: 10 | - events 11 | - serviceaccounts 12 | verbs: 13 | - create 14 | - get 15 | - list 16 | - patch 17 | - update 18 | - watch 19 | - apiGroups: 20 | - iammanager.keikoproj.io 21 | resources: 22 | - iamroles 23 | verbs: 24 | - create 25 | - delete 26 | - get 27 | - list 28 | - patch 29 | - update 30 | - watch 31 | - apiGroups: 32 | - iammanager.keikoproj.io 33 | resources: 34 | - iamroles/status 35 | verbs: 36 | - get 37 | - patch 38 | - update 39 | -------------------------------------------------------------------------------- /config/rbac/role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | name: manager-rolebinding 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: ClusterRole 8 | name: manager-role 9 | subjects: 10 | - kind: ServiceAccount 11 | name: default 12 | namespace: system 13 | -------------------------------------------------------------------------------- /config/samples/iammanager_v1alpha1_iamrole.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: iammanager.keikoproj.io/v1alpha1 3 | kind: Iamrole 4 | metadata: 5 | name: sds 6 | spec: 7 | PolicyDocument: 8 | Version: '2012-10-17' 9 | Statement: 10 | - Effect: Allow 11 | Action: 12 | - s3:ListBucket 13 | Resource: 14 | - arn:aws:s3:::iksm-dummy-us-west-2-dev 15 | - Effect: Allow 16 | Action: 17 | - s3:* 18 | Resource: 19 | - arn:aws:s3:::dummy-us-west-2-dev/* 20 | - arn:aws:s3:::dummy-prd-us-west-2/* 21 | - arn:aws:s3:::dummy-prd-us-east-2/* 22 | - Effect: Allow 23 | Resource: 24 | - "*" 25 | Action: 26 | - sts:AssumeRole 27 | - Effect: Allow 28 | Action: 29 | - ec2:Describe* 30 | Resource: 31 | - "*" 32 | - Effect: Allow 33 | Action: 34 | - route53:Get* 35 | - route53:List* 36 | - route53:Create* 37 | - route53:Delete* 38 | - route53:Change* 39 | Resource: 40 | - "*" 41 | - Effect: Allow 42 | Action: 43 | - s3:PutObject 44 | - s3:PutObjectAcl 45 | Resource: 46 | - arn:aws:s3:::intu-oim* 47 | -------------------------------------------------------------------------------- /config/webhook/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - manifests.yaml 3 | - service.yaml 4 | 5 | configurations: 6 | - kustomizeconfig.yaml 7 | -------------------------------------------------------------------------------- /config/webhook/kustomizeconfig.yaml: -------------------------------------------------------------------------------- 1 | # the following config is for teaching kustomize where to look at when substituting vars. 2 | # It requires kustomize v2.1.0 or newer to work properly. 3 | nameReference: 4 | - kind: Service 5 | version: v1 6 | fieldSpecs: 7 | - kind: MutatingWebhookConfiguration 8 | group: admissionregistration.k8s.io 9 | path: webhooks/clientConfig/service/name 10 | - kind: ValidatingWebhookConfiguration 11 | group: admissionregistration.k8s.io 12 | path: webhooks/clientConfig/service/name 13 | 14 | namespace: 15 | - kind: MutatingWebhookConfiguration 16 | group: admissionregistration.k8s.io 17 | path: webhooks/clientConfig/service/namespace 18 | create: true 19 | - kind: ValidatingWebhookConfiguration 20 | group: admissionregistration.k8s.io 21 | path: webhooks/clientConfig/service/namespace 22 | create: true 23 | 24 | varReference: 25 | - path: metadata/annotations 26 | -------------------------------------------------------------------------------- /config/webhook/manifests.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: admissionregistration.k8s.io/v1 3 | kind: MutatingWebhookConfiguration 4 | metadata: 5 | name: mutating-webhook-configuration 6 | webhooks: 7 | - admissionReviewVersions: 8 | - v1 9 | clientConfig: 10 | service: 11 | name: webhook-service 12 | namespace: system 13 | path: /mutate-iammanager-keikoproj-io-v1alpha1-iamrole 14 | failurePolicy: Fail 15 | name: miamrole.kb.io 16 | rules: 17 | - apiGroups: 18 | - iammanager.keikoproj.io 19 | apiVersions: 20 | - v1alpha1 21 | operations: 22 | - CREATE 23 | - UPDATE 24 | resources: 25 | - iamroles 26 | sideEffects: None 27 | --- 28 | apiVersion: admissionregistration.k8s.io/v1 29 | kind: ValidatingWebhookConfiguration 30 | metadata: 31 | name: validating-webhook-configuration 32 | webhooks: 33 | - admissionReviewVersions: 34 | - v1 35 | clientConfig: 36 | service: 37 | name: webhook-service 38 | namespace: system 39 | path: /validate-iammanager-keikoproj-io-v1alpha1-iamrole 40 | failurePolicy: Fail 41 | name: viamrole.kb.io 42 | rules: 43 | - apiGroups: 44 | - iammanager.keikoproj.io 45 | apiVersions: 46 | - v1alpha1 47 | operations: 48 | - CREATE 49 | - UPDATE 50 | resources: 51 | - iamroles 52 | sideEffects: None 53 | -------------------------------------------------------------------------------- /config/webhook/service.yaml: -------------------------------------------------------------------------------- 1 | 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | name: webhook-service 6 | namespace: system 7 | spec: 8 | ports: 9 | - port: 443 10 | targetPort: 9443 11 | selector: 12 | control-plane: controller-manager 13 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # IAM Manager Documentation 2 | 3 | Welcome to the IAM Manager documentation. This directory contains resources to help you install, configure, and use IAM Manager. 4 | 5 | ## Getting Started 6 | 7 | * [Quick Start Guide](quickstart.md) - Follow this guide to quickly set up IAM Manager and create your first IAM role 8 | 9 | ## Core Concepts 10 | 11 | * [Architecture Documentation](architecture.md) - Learn about IAM Manager's architecture and components 12 | * [Design Documentation](design.md) - Understand the design principles and decisions behind IAM Manager 13 | 14 | ## Configuration 15 | 16 | * [Configuration Options](configmap-properties.md) - Detailed explanation of all available configuration options 17 | 18 | ## Security 19 | 20 | * [AWS Security](AWS_Security.md) - Understanding the security features and AWS IAM permission boundaries 21 | 22 | ## Advanced Topics 23 | 24 | * [AWS Integration](aws-integration.md) - Guide for setting up IAM Manager with AWS services 25 | * [Troubleshooting Guide](troubleshooting.md) - Solutions for common issues and troubleshooting steps 26 | 27 | ## For Developers 28 | 29 | * [Developer Guide](developer-guide.md) - Information for developers who want to contribute to IAM Manager 30 | * [Contributing Guidelines](../CONTRIBUTING.md) - How to contribute to the IAM Manager project 31 | 32 | ## Examples 33 | 34 | The [examples](../examples) directory contains sample configurations for common use cases. 35 | 36 | ## Images 37 | 38 | The [images](./images) directory contains diagrams illustrating IAM Manager architecture and workflows. 39 | 40 | ## Additional Resources 41 | 42 | * [GitHub Repository](https://github.com/keikoproj/iam-manager) - Main repository for IAM Manager 43 | * [Releases](https://github.com/keikoproj/iam-manager/releases) - Release notes and version information 44 | * [Issues](https://github.com/keikoproj/iam-manager/issues) - Report bugs or request features 45 | * [Medium Article](https://medium.com/keikoproj/managing-iam-roles-as-k8s-resources-aa00c5c4447f) - Original article describing the motivation behind IAM Manager 46 | -------------------------------------------------------------------------------- /docs/architecture.md: -------------------------------------------------------------------------------- 1 | # IAM Manager Architecture 2 | 3 | This document provides an overview of the IAM Manager architecture and its interaction with Kubernetes and AWS components. 4 | 5 | ## Architecture Diagram 6 | 7 | ```mermaid 8 | graph TD 9 | %% Define styles 10 | classDef k8s fill:#326ce5,color:white,stroke:white,stroke-width:2px 11 | classDef aws fill:#FF9900,color:white,stroke:white,stroke-width:2px 12 | classDef security fill:#3f8428,color:white,stroke:white,stroke-width:2px 13 | classDef app fill:#764ABC,color:white,stroke:white,stroke-width:2px 14 | 15 | %% Kubernetes Components 16 | User([DevOps/User]) 17 | User -->|Create/Update/Delete| IAMRoleCR[IAMRole CR] 18 | 19 | subgraph Kubernetes Cluster 20 | IAMRoleCR:::k8s 21 | APIServer[Kubernetes API Server]:::k8s 22 | Webhook[Validation Webhook]:::security 23 | IAMControllerPod[IAM Manager Controller]:::k8s 24 | ServiceAccount[Kubernetes Service Accounts]:::k8s 25 | AppPods[Application Pods]:::app 26 | 27 | IAMRoleCR -->|Submit| APIServer 28 | APIServer -->|Validate| Webhook 29 | Webhook -->|Policy Validation| APIServer 30 | APIServer -->|Watch| IAMControllerPod 31 | IAMControllerPod -->|Update Status| APIServer 32 | ServiceAccount -->|Mount Token| AppPods 33 | end 34 | 35 | %% AWS Components 36 | subgraph AWS Account 37 | IAMRoles[IAM Roles]:::aws 38 | PermBoundary[Permission Boundaries]:::security 39 | TrustPolicy[Trust Relationships]:::security 40 | AwsServices[AWS Services]:::aws 41 | 42 | IAMControllerPod -->|Create/Update/Delete| IAMRoles 43 | IAMRoles -->|Limited by| PermBoundary 44 | IAMRoles -->|Define| TrustPolicy 45 | AppPods -->|AssumeRole| IAMRoles 46 | IAMRoles -->|Access| AwsServices 47 | end 48 | 49 | %% IRSA Specific 50 | subgraph IRSA Components 51 | EKSOIDCProvider[EKS OIDC Provider]:::aws 52 | 53 | IAMControllerPod -->|Configure| TrustPolicy 54 | ServiceAccount -->|Token Used By| EKSOIDCProvider 55 | EKSOIDCProvider -->|Authenticate| TrustPolicy 56 | end 57 | ``` 58 | 59 | ## Component Descriptions 60 | 61 | ### Kubernetes Components 62 | 63 | - **IAMRole CR**: Custom Resource that defines the desired IAM role configuration 64 | - **Validation Webhook**: Ensures IAM policies comply with allowed policies and resource limits 65 | - **IAM Manager Controller**: Reconciles IAMRole CRs with actual AWS IAM roles 66 | - **Service Accounts**: Kubernetes identities that can be associated with IAM roles (IRSA) 67 | - **Application Pods**: Workloads that use IAM roles to access AWS services 68 | 69 | ### AWS Components 70 | 71 | - **IAM Roles**: AWS Identity & Access Management roles created and managed by the controller 72 | - **Permission Boundaries**: Limit the maximum permissions that can be granted to roles 73 | - **Trust Relationships**: Define which entities can assume the roles 74 | - **AWS Services**: Cloud services accessed using the IAM roles 75 | 76 | ### IRSA Components 77 | 78 | - **EKS OIDC Provider**: Allows Kubernetes service accounts to authenticate to AWS and assume IAM roles 79 | 80 | ## Workflows 81 | 82 | 1. **Creation Flow**: 83 | - User creates an IAMRole CR in a Kubernetes namespace 84 | - Webhook validates the policy against allowed actions and resources 85 | - Controller creates an AWS IAM role with permission boundary 86 | - Status is updated with the role ARN and creation state 87 | 88 | 2. **IRSA Flow**: 89 | - IAMRole CR includes a service account annotation 90 | - Controller configures the trust policy to allow the service account to assume the role 91 | - Pods using the service account can access AWS resources via the role 92 | 93 | 3. **Security Controls**: 94 | - Permission boundaries limit the maximum permissions 95 | - Namespace-level role restrictions control proliferation of roles 96 | - Validation webhooks prevent creation of overly permissive policies 97 | -------------------------------------------------------------------------------- /docs/aws-integration.md: -------------------------------------------------------------------------------- 1 | # AWS Integration for IAM Manager 2 | 3 | This guide explains how to integrate iam-manager with AWS services, particularly focusing on IAM Roles for Service Accounts (IRSA) on Amazon EKS clusters. 4 | 5 | ## Overview 6 | 7 | IAM Manager works by creating and managing AWS IAM roles based on Kubernetes custom resources. This integration requires proper AWS setup, including: 8 | 9 | 1. Permission boundaries to control the maximum permissions allowed 10 | 2. IAM policies for the iam-manager controller 11 | 3. OIDC provider configuration for EKS clusters (for IRSA) 12 | 13 | ## IAM Manager Controller Permissions 14 | 15 | The iam-manager controller requires specific AWS IAM permissions to manage IAM roles. These permissions should be restricted using the principle of least privilege. 16 | 17 | ### Required Permissions 18 | 19 | The controller needs permissions to: 20 | 21 | ```json 22 | { 23 | "Version": "2012-10-17", 24 | "Statement": [ 25 | { 26 | "Effect": "Allow", 27 | "Action": [ 28 | "iam:CreateRole", 29 | "iam:DeleteRole", 30 | "iam:GetRole", 31 | "iam:PassRole", 32 | "iam:TagRole", 33 | "iam:UntagRole", 34 | "iam:ListRoleTags", 35 | "iam:PutRolePolicy", 36 | "iam:GetRolePolicy", 37 | "iam:DeleteRolePolicy", 38 | "iam:ListRolePolicies", 39 | "iam:AttachRolePolicy", 40 | "iam:DetachRolePolicy", 41 | "iam:ListAttachedRolePolicies" 42 | ], 43 | "Resource": [ 44 | "arn:aws:iam::*:role/k8s-*" 45 | ], 46 | "Condition": { 47 | "StringEquals": { 48 | "iam:PermissionsBoundary": "arn:aws:iam::*:policy/iam-manager-permission-boundary" 49 | } 50 | } 51 | } 52 | ] 53 | } 54 | ``` 55 | 56 | ### Setting Up Controller IAM 57 | 58 | You can set up the required IAM resources using the provided CloudFormation template: 59 | 60 | ```bash 61 | aws cloudformation create-stack \ 62 | --stack-name iam-manager-resources \ 63 | --template-body file://hack/iam-manager-cfn.yaml \ 64 | --capabilities CAPABILITY_NAMED_IAM \ 65 | --parameters ParameterKey=ClusterName,ParameterValue=your-cluster-name 66 | ``` 67 | 68 | ## IAM Roles for Service Accounts (IRSA) 69 | 70 | ### Setting Up IRSA Controller Access 71 | 72 | When running iam-manager on EKS, you should use IRSA to grant the controller access to AWS: 73 | 74 | 1. **Verify OIDC Provider Configuration** 75 | 76 | ```bash 77 | aws eks describe-cluster --name your-cluster-name --query "cluster.identity.oidc.issuer" --output text 78 | ``` 79 | 80 | If this command returns a URL, your cluster has an OIDC provider configured. If not, set it up: 81 | 82 | ```bash 83 | eksctl utils associate-iam-oidc-provider --cluster your-cluster-name --approve 84 | ``` 85 | 86 | 2. **Create an IAM Role for the Controller** 87 | 88 | Use eksctl to create the role and associate it with the iam-manager service account: 89 | 90 | ```bash 91 | eksctl create iamserviceaccount \ 92 | --name iam-manager-controller \ 93 | --namespace iam-manager-system \ 94 | --cluster your-cluster-name \ 95 | --attach-policy-arn arn:aws:iam::YOUR_ACCOUNT_ID:policy/IamManagerControllerPolicy \ 96 | --approve 97 | ``` 98 | 99 | 3. **Configure Controller Deployment** 100 | 101 | Ensure the deployment uses the correct service account: 102 | 103 | ```yaml 104 | apiVersion: apps/v1 105 | kind: Deployment 106 | metadata: 107 | name: iam-manager-controller-manager 108 | namespace: iam-manager-system 109 | spec: 110 | template: 111 | spec: 112 | serviceAccountName: iam-manager-controller 113 | ``` 114 | 115 | ### Using IAM Manager for IRSA 116 | 117 | IAM Manager can create roles specifically for IRSA: 118 | 119 | 1. **Configure ConfigMap with OIDC Provider URL** 120 | 121 | ```yaml 122 | apiVersion: v1 123 | kind: ConfigMap 124 | metadata: 125 | name: iammanager-config 126 | namespace: iam-manager-system 127 | data: 128 | cluster.oidc-provider-url: "https://oidc.eks.us-west-2.amazonaws.com/id/EXAMPLED539D4633E53DE1B71EXAMPLE" 129 | ``` 130 | 131 | 2. **Create an IAM Role with IRSA Annotation** 132 | 133 | ```yaml 134 | apiVersion: iammanager.keikoproj.io/v1alpha1 135 | kind: Iamrole 136 | metadata: 137 | name: app-role 138 | namespace: default 139 | annotations: 140 | iam.amazonaws.com/irsa-service-account: app-service-account 141 | spec: 142 | PolicyDocument: 143 | Statement: 144 | - Effect: "Allow" 145 | Action: 146 | - "s3:GetObject" 147 | Resource: 148 | - "arn:aws:s3:::my-bucket/*" 149 | ``` 150 | 151 | 3. **Use the Role in Your Application** 152 | 153 | ```yaml 154 | apiVersion: apps/v1 155 | kind: Deployment 156 | metadata: 157 | name: app 158 | namespace: default 159 | spec: 160 | template: 161 | spec: 162 | serviceAccountName: app-service-account 163 | ``` 164 | 165 | ## Working with Permission Boundaries 166 | 167 | IAM Manager uses permission boundaries to limit the maximum permissions that can be granted to roles it creates. 168 | 169 | ### Understanding Permission Boundaries 170 | 171 | A permission boundary is an IAM policy that sets the maximum permissions that a role can have. Even if a role has a very permissive policy attached, the effective permissions are limited by the boundary. 172 | 173 | For example, if a role policy allows `s3:*` but the permission boundary only allows `s3:GetObject`, the role can only perform the GetObject action. 174 | 175 | ### Configuring Permission Boundaries 176 | 177 | IAM Manager applies a permission boundary to all roles it creates. The boundary is configured in the CloudFormation template and referenced in the ConfigMap: 178 | 179 | ```yaml 180 | apiVersion: v1 181 | kind: ConfigMap 182 | metadata: 183 | name: iammanager-config 184 | namespace: iam-manager-system 185 | data: 186 | defaults.permission-boundary-policy: "iam-manager-permission-boundary" 187 | ``` 188 | 189 | ### Customizing Permission Boundaries 190 | 191 | To customize the permission boundary, update the CloudFormation template with your desired permissions before creating the stack. 192 | 193 | ## Troubleshooting AWS Integration 194 | 195 | ### Common Issues 196 | 197 | 1. **Controller Cannot Create Roles** 198 | 199 | - Check if the controller has the correct IAM permissions 200 | - Verify the permission boundary policy exists 201 | - Check controller logs for specific error messages 202 | 203 | 2. **IRSA Not Working** 204 | 205 | - Verify the OIDC provider URL in the ConfigMap 206 | - Check that the service account specified in the annotation exists 207 | - Ensure the pod is using the correct service account 208 | 209 | 3. **Incorrect Permissions in Created Roles** 210 | 211 | - Check the permission boundary policy 212 | - Review the policy document in the Iamrole CR 213 | - Verify the role was created successfully in AWS 214 | 215 | ### Debugging AWS API Calls 216 | 217 | To debug AWS API calls, enable verbose logging in the controller: 218 | 219 | ```bash 220 | kubectl edit deployment iam-manager-controller-manager -n iam-manager-system 221 | ``` 222 | 223 | Add environment variables: 224 | 225 | ```yaml 226 | spec: 227 | template: 228 | spec: 229 | containers: 230 | - name: manager 231 | env: 232 | - name: AWS_SDK_GO_LOG_LEVEL 233 | value: "Debug" 234 | - name: LOG_LEVEL 235 | value: "debug" 236 | ``` 237 | 238 | ## Security Best Practices 239 | 240 | 1. **Use Least Privilege Policies** 241 | - Restrict the controller's IAM permissions to only what it needs 242 | - Use specific resources in role policies instead of wildcards 243 | 244 | 2. **Implement Strong Boundary Policies** 245 | - Design permission boundaries that restrict access to sensitive resources 246 | - Regularly review and update boundaries as security requirements change 247 | 248 | 3. **Audit Role Usage** 249 | - Regularly audit the roles created by iam-manager 250 | - Monitor AWS CloudTrail logs for suspicious activity 251 | 252 | 4. **Manage Controller Access** 253 | - Restrict access to modify the iam-manager controller and its resources 254 | - Use Kubernetes RBAC to control who can create and modify IAM roles 255 | 256 | ## References 257 | 258 | - [AWS IAM Permission Boundaries](https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_boundaries.html) 259 | - [EKS IRSA Documentation](https://docs.aws.amazon.com/eks/latest/userguide/iam-roles-for-service-accounts.html) 260 | - [AWS SDK for Go Documentation](https://docs.aws.amazon.com/sdk-for-go/api/) 261 | -------------------------------------------------------------------------------- /docs/aws-security.md: -------------------------------------------------------------------------------- 1 | This document explains the security measurements in place with iam-manager solution in AWS use case. 2 | 3 | 4 | #### Security: 5 | To manage IAM role lifecycle independently, controller(Pod) needs to have AWS IAM access to create/update/delete role which is a security concern if solution is not designed properly but thanks to AWS IAM Permission Boundaries which got released in Q3 2018 and we can restrict the permissions on what user/role can do even if they have iam.* access. 6 | To have more confidence in terms of security, any good design should consider implementing Proactive and Reactive security measurements. 7 | 8 | ##### Proactive measurements: 9 | 10 | ###### Kubernetes validation web hook implementation 11 | 12 | Kubernetes web hook validation comes in very handy if we want to validate user input before inserting into persistence system(etcd). This allows us to implement following actions and reject the request if it violates the defined policy. 13 | i. Allow IAM Role creation only with "pre-defined IAM whitelisted policies" 14 | ii. Allow only ONE role per namespace 15 | 16 | ###### AWS IAM Permission Boundary 17 | 18 | AWS IAM Permission Boundary is the core security concept used in the iam-manager. Permission boundaries are AWS IAM policy objects that establish the maximum permissions an IAM entity can have, regardless of what permissions are granted by the attached policies. 19 | 20 | In the context of iam-manager, permission boundaries provide a critical security control by limiting what permissions can be delegated, even if the IAM role policy appears to grant broader access. 21 | 22 | ### How Permission Boundaries Work 23 | 24 | The permission boundary acts as a guard rail for all roles created by iam-manager. The actual permissions granted to a role are the **intersection** of: 25 | 26 | 1. Permissions specified in the role's policy 27 | 2. Permissions allowed by the permission boundary 28 | 29 | For example, if an IAM role is created with "AdministratorAccess" policy but has a permission boundary that only allows "s3:Get*" operations, the effective permissions would be limited to just "s3:Get*" operations, even though the role policy technically grants much broader access. 30 | 31 | This ensures that even if a user specifies overly permissive policies in their IAMRole resource, the permission boundary will restrict the actual capabilities to only those explicitly allowed by the cluster administrator. 32 | 33 | ### Real-World Example 34 | 35 | Consider this scenario: 36 | 37 | - An IAMRole resource includes "ec2:*" permissions in its policy 38 | - The permission boundary only allows "ec2:Describe*" actions 39 | - The effective permissions for the role will be limited to only "ec2:Describe*" 40 | 41 | This prevents escalation of privileges and ensures that roles created through iam-manager cannot be used to gain unauthorized access to AWS resources, protecting both the cluster and the broader AWS environment. 42 | 43 | Another important security concern is having an aws iam write access to the controller itself. This is important for many reasons where an developer/hacker gets an access to controller pod (which is very unlikely, if we say this is possible than we have a bigger thing to worry about where developers having an access to resources in a different namespace. We are not talking about cluster admins here. well, cluster admin can delete the entire cluster) and start creating/deleting the roles which are not part of the Kubernetes environment (For ex: Administrator). This is where IAM Permission Boundaries, Controlling Access Using Tags comes into picture. 44 | 45 | In brief, If we define a permission boundary with "s3.Get*" access, any role created by controller pod can get only s3.Get access even if new role has an "Administrative" policy with full access attached. For more details, please refer the IAM Permission Boundaries. 46 | 47 | That being said, iam role attached to controller can do only following actions 48 | i. Can create roles only with pre-defined syntax. 49 | ii. Can not create a role with out providing pre-defined permission boundary name. 50 | iii. Can not delete any role which doesn't have a pre-defined TAG. (We will attach the tag only to the roles created by controller pod) 51 | 52 | ###### Do not provide an option to users to provide IAM role name 53 | Role name can be constructed based on the namespace where custom resource is being created. This allows us to create IAM role with consistent naming conventions. 54 | 55 | ###### Custom resource controller deployed in its own namespace. 56 | This is the recommended approach to deploy a CRD in Kubernetes. This allows us to restrict the access only to iam-manager pods. 57 | 58 | ##### Reactive measurements: 59 | 60 | ###### Remediate action triggered by AWS cloud watch rule 61 | 62 | For some reason, if any role got created by controller pod with malicious intent(Having a different IAM policies than the pre-defined whitelisted IAM policies) we want to make sure remediate action plan is in place. Cloud watch rule which can trigger a lambda function if it detects any action(create/update/delete and even attaching a policy api call) taken by controller pod IAM role, lambda function verifies that action was taken by "within the known limits" and if it detects any anomaly it simply attaches "Deny" all access so that role can not be used for anything. 63 | 64 | For more details: Please refer https://github.intuit.com/keikoproj/iam-manager-monitor/ repo for sample app 65 | 66 | Finally, with all the measurements in place controller pod can do only do limited actions which can be totally pre-defined 67 | 68 | ### Pros: 69 | 1. Solution is completely de-centralized and there are no outbound calls from the cluster. 70 | 2. More secure with the Permission Boundary. 71 | 3. Not customized solution for Intuit which means this can be distributed as open source project. 72 | 4. Auditing information is available with CloudTrail. 73 | 5. If there is any breach, ONLY clusters in this particular account gets compromised compared to ALL the clusters if iam is managed in central place. 74 | ### Cons: 75 | 1. Solution must be carefully implemented. 76 | 2. If there is any breach, clusters in this particular account gets compromised. -------------------------------------------------------------------------------- /docs/crd-reference.md: -------------------------------------------------------------------------------- 1 | # Iamrole CRD Reference 2 | 3 | This document provides a detailed reference for the `Iamrole` custom resource definition (CRD) used by IAM Manager. 4 | 5 | ## Overview 6 | 7 | The `Iamrole` CRD is the primary resource used to define AWS IAM roles in Kubernetes. When you create an `Iamrole` custom resource, IAM Manager creates a corresponding IAM role in AWS with the specified policies and trust relationships. 8 | 9 | ## Resource Definition 10 | 11 | ```yaml 12 | apiVersion: iammanager.keikoproj.io/v1alpha1 13 | kind: Iamrole 14 | metadata: 15 | name: example-role 16 | namespace: default 17 | # Optional annotation for IRSA integration 18 | annotations: 19 | iam.amazonaws.com/irsa-service-account: my-service-account 20 | spec: 21 | # IAM permissions policy (required) 22 | PolicyDocument: 23 | Version: "2012-10-17" # Optional, defaults to "2012-10-17" 24 | Statement: 25 | - Effect: Allow # Required: "Allow" or "Deny" 26 | Action: # Required: List of IAM actions 27 | - "s3:GetObject" 28 | - "s3:ListBucket" 29 | Resource: # Required: List of AWS resources 30 | - "arn:aws:s3:::mybucket/*" 31 | - "arn:aws:s3:::mybucket" 32 | Sid: "AllowS3Access" # Optional: Statement identifier 33 | 34 | # Trust policy (optional) 35 | # If not specified, a default trust policy will be used 36 | AssumeRolePolicyDocument: 37 | Version: "2012-10-17" # Optional, defaults to "2012-10-17" 38 | Statement: 39 | - Effect: Allow 40 | Action: "sts:AssumeRole" 41 | Principal: 42 | AWS: 43 | - "arn:aws:iam::123456789012:role/KubernetesNode" 44 | # Optional conditions 45 | Condition: 46 | StringEquals: 47 | "aws:SourceAccount": "123456789012" 48 | StringLike: 49 | "aws:username": "admin-*" 50 | 51 | # Custom role name (optional) 52 | # Only available in privileged namespaces 53 | RoleName: "custom-role-name" 54 | ``` 55 | 56 | ## Field Reference 57 | 58 | ### Spec Fields 59 | 60 | | Field | Type | Required | Description | 61 | |-------|------|----------|-------------| 62 | | `PolicyDocument` | Object | Yes | Defines the permissions for the IAM role | 63 | | `AssumeRolePolicyDocument` | Object | No | Defines which entities can assume the role (trust policy) | 64 | | `RoleName` | String | No | Custom name for the IAM role (only for privileged namespaces) | 65 | 66 | ### PolicyDocument Fields 67 | 68 | | Field | Type | Required | Description | 69 | |-------|------|----------|-------------| 70 | | `Version` | String | No | Policy language version (defaults to "2012-10-17") | 71 | | `Statement` | Array | Yes | List of policy statements | 72 | 73 | ### Statement Fields 74 | 75 | | Field | Type | Required | Description | 76 | |-------|------|----------|-------------| 77 | | `Effect` | String | Yes | Either "Allow" or "Deny" | 78 | | `Action` | Array of Strings | Yes | List of AWS API actions to allow or deny | 79 | | `Resource` | Array of Strings | Yes | List of AWS resources the actions apply to | 80 | | `Sid` | String | No | Statement identifier for logging and debugging | 81 | 82 | ### AssumeRolePolicyDocument Fields 83 | 84 | | Field | Type | Required | Description | 85 | |-------|------|----------|-------------| 86 | | `Version` | String | No | Policy language version (defaults to "2012-10-17") | 87 | | `Statement` | Array | Yes | List of trust policy statements | 88 | 89 | ### Trust Policy Statement Fields 90 | 91 | | Field | Type | Required | Description | 92 | |-------|------|----------|-------------| 93 | | `Effect` | String | No | Either "Allow" or "Deny" (defaults to "Allow") | 94 | | `Action` | String | No | The action to allow/deny (typically "sts:AssumeRole") | 95 | | `Principal` | Object | Yes | The entity that can assume the role | 96 | | `Condition` | Object | No | Additional conditions on the trust relationship | 97 | 98 | ### Principal Fields 99 | 100 | | Field | Type | Required | Description | 101 | |-------|------|----------|-------------| 102 | | `AWS` | String/Array | No* | AWS account/role/user ARN(s) that can assume the role | 103 | | `Service` | String | No* | AWS service that can assume the role (e.g., "ec2.amazonaws.com") | 104 | | `Federated` | String | No* | Federated identity provider (e.g., OIDC provider) | 105 | 106 | *At least one of AWS, Service, or Federated must be specified. 107 | 108 | ### Condition Fields 109 | 110 | | Field | Type | Required | Description | 111 | |-------|------|----------|-------------| 112 | | `StringEquals` | Map | No | Exact string matching conditions | 113 | | `StringLike` | Map | No | String pattern matching conditions using wildcards | 114 | 115 | ## Status Fields 116 | 117 | The `Iamrole` resource includes the following status fields that are populated by the controller: 118 | 119 | | Field | Description | 120 | |-------|-------------| 121 | | `roleName` | The name of the IAM role in AWS | 122 | | `roleARN` | The Amazon Resource Name (ARN) of the IAM role | 123 | | `roleID` | The unique identifier of the IAM role | 124 | | `state` | Current state of the IAM role (Ready, Error, etc.) | 125 | | `retryCount` | Number of reconciliation attempts | 126 | | `errorDescription` | Description of any errors that occurred | 127 | | `lastUpdatedTimestamp` | When the role was last updated | 128 | 129 | ## Annotations 130 | 131 | | Annotation | Description | 132 | |------------|-------------| 133 | | `iam.amazonaws.com/irsa-service-account` | Specifies the service account that can use this IAM role (for IRSA) | 134 | 135 | ## Examples 136 | 137 | ### Basic Role with S3 Access 138 | 139 | ```yaml 140 | apiVersion: iammanager.keikoproj.io/v1alpha1 141 | kind: Iamrole 142 | metadata: 143 | name: s3-reader 144 | namespace: default 145 | spec: 146 | PolicyDocument: 147 | Statement: 148 | - Effect: "Allow" 149 | Action: 150 | - "s3:GetObject" 151 | - "s3:ListBucket" 152 | Resource: 153 | - "arn:aws:s3:::example-bucket/*" 154 | - "arn:aws:s3:::example-bucket" 155 | Sid: "AllowS3Access" 156 | AssumeRolePolicyDocument: 157 | Statement: 158 | - Effect: "Allow" 159 | Action: "sts:AssumeRole" 160 | Principal: 161 | AWS: 162 | - "arn:aws:iam::123456789012:role/KubernetesNode" 163 | ``` 164 | 165 | ### IRSA Role for Pod Service Account 166 | 167 | ```yaml 168 | apiVersion: iammanager.keikoproj.io/v1alpha1 169 | kind: Iamrole 170 | metadata: 171 | name: app-role 172 | namespace: default 173 | annotations: 174 | iam.amazonaws.com/irsa-service-account: app-service-account 175 | spec: 176 | PolicyDocument: 177 | Statement: 178 | - Effect: "Allow" 179 | Action: 180 | - "dynamodb:GetItem" 181 | - "dynamodb:PutItem" 182 | Resource: 183 | - "arn:aws:dynamodb:*:*:table/my-table" 184 | ``` 185 | 186 | ## Common States 187 | 188 | | State | Description | 189 | |-------|-------------| 190 | | `Ready` | The IAM role has been successfully created/updated in AWS | 191 | | `Error` | An error occurred during creation/update | 192 | | `PolicyNotAllowed` | The policy contains actions that are not allowed | 193 | | `PermissionDenied` | IAM Manager does not have sufficient permissions | 194 | | `InvalidSpecification` | The spec contains invalid configuration | 195 | | `InProgress` | The role is being created or updated | 196 | -------------------------------------------------------------------------------- /docs/design.md: -------------------------------------------------------------------------------- 1 | # IAM Manager Design Documentation 2 | 3 | This document outlines the design principles, architecture decisions, and trade-offs considered during the development of IAM Manager. 4 | 5 | ## Design Goals 6 | 7 | IAM Manager was designed with the following goals in mind: 8 | 9 | 1. **Declarative Management**: Enable management of AWS IAM roles using Kubernetes-native declarative principles 10 | 2. **Security First**: Implement strong security boundaries to prevent privilege escalation 11 | 3. **GitOps Compatibility**: Allow IAM roles to be version-controlled alongside application code 12 | 4. **Namespace Isolation**: Provide isolation between different teams/namespaces 13 | 5. **Kubernetes Integration**: Leverage Kubernetes patterns and extend its API 14 | 15 | ## Core Design Principles 16 | 17 | ### 1. Separation of Concerns 18 | 19 | IAM Manager separates the following concerns: 20 | 21 | - **Role Definition**: Handled by the Iamrole CR 22 | - **Permission Boundaries**: Managed separately and enforced by the controller 23 | - **Trust Relationships**: Configured based on use case (standard or IRSA) 24 | - **Validation Logic**: Implemented in admission webhooks 25 | 26 | This separation allows for more flexible and maintainable code, as each component handles a specific aspect of IAM role management. 27 | 28 | ### 2. Kubernetes Extension Pattern 29 | 30 | IAM Manager follows the Kubernetes extension pattern: 31 | 32 | - Custom Resource Definitions (CRDs) to extend the Kubernetes API 33 | - Custom controllers to reconcile desired vs. actual state 34 | - Admission webhooks for validation 35 | - Operator pattern for reconciliation 36 | 37 | This approach provides a seamless integration with Kubernetes, allowing users to manage AWS IAM roles using familiar kubectl commands and GitOps workflows. 38 | 39 | ### 3. Secure by Default 40 | 41 | Security is a primary concern in IAM Manager's design: 42 | 43 | - **Permission Boundaries**: All roles are created with a permission boundary that limits their maximum permissions 44 | - **Validation Webhooks**: Prevent creation of roles with excessive permissions 45 | - **Namespace Restrictions**: Limit the number of roles per namespace 46 | - **Role Naming Conventions**: Enforce consistent naming with prefixes to identify managed roles 47 | - **Resource Tagging**: Tag all AWS resources for auditing and management 48 | 49 | ### 4. Reconciliation Loop 50 | 51 | IAM Manager uses a controller-based reconciliation loop: 52 | 53 | 1. Watch for changes to Iamrole CRs 54 | 2. Compare desired state (CR) with actual state (AWS) 55 | 3. Make changes to bring actual state in line with desired state 56 | 4. Update status to reflect current state 57 | 58 | This pattern ensures that even if changes are made directly in AWS, the controller will detect and revert them to match the desired state defined in Kubernetes. 59 | 60 | ## Architecture Decisions 61 | 62 | ### Decision 1: Kubernetes Custom Resources 63 | 64 | **Decision**: Use Kubernetes CRDs to represent IAM roles. 65 | 66 | **Alternatives Considered**: 67 | - External database to store role definitions 68 | - Annotations on namespaces or service accounts 69 | - External API server 70 | 71 | **Rationale**: 72 | - CRDs provide a native Kubernetes experience 73 | - Built-in validation and versioning 74 | - Can leverage existing Kubernetes RBAC 75 | - Works with existing GitOps tools 76 | 77 | ### Decision 2: Permission Boundaries 78 | 79 | **Decision**: Use AWS IAM Permission Boundaries to limit the maximum permissions of created roles. 80 | 81 | **Alternatives Considered**: 82 | - Policy validation only 83 | - Custom approval workflow 84 | - Limited IAM permissions for the controller 85 | 86 | **Rationale**: 87 | - Permission boundaries provide a hard limit at the AWS level 88 | - Even if the controller is compromised, it cannot create overly permissive roles 89 | - Clear separation between what permissions are allowed vs. what permissions are granted 90 | 91 | ### Decision 3: Namespace-Scoped Resources 92 | 93 | **Decision**: Make Iamrole resources namespace-scoped rather than cluster-scoped. 94 | 95 | **Alternatives Considered**: 96 | - Cluster-scoped resources with namespace field 97 | - Custom namespace-based isolation 98 | 99 | **Rationale**: 100 | - Aligns with Kubernetes' namespace isolation model 101 | - Allows for RBAC to be applied at namespace level 102 | - Teams can manage their own IAM roles without affecting others 103 | - Prevents naming conflicts between different teams 104 | 105 | ### Decision 4: AWS API Integration 106 | 107 | **Decision**: Use the AWS SDK for Go to directly interact with the AWS API. 108 | 109 | **Alternatives Considered**: 110 | - AWS CloudFormation 111 | - AWS CDK 112 | - Terraform 113 | 114 | **Rationale**: 115 | - Direct API access provides more control and better error handling 116 | - Faster reconciliation as there's no need to wait for external provisioning tools 117 | - Lower dependency footprint 118 | 119 | ### Decision 5: IRSA Integration 120 | 121 | **Decision**: Support IAM Roles for Service Accounts through annotations. 122 | 123 | **Alternatives Considered**: 124 | - Separate CRD for IRSA roles 125 | - External service account mapping 126 | 127 | **Rationale**: 128 | - Annotations provide a simple, declarative way to associate roles with service accounts 129 | - Consistent with how IRSA works in EKS 130 | - Minimizes the learning curve for users familiar with IRSA 131 | 132 | ## Trade-offs 133 | 134 | ### Trade-off 1: Controller Permissions 135 | 136 | **Context**: The controller needs permissions to create and manage IAM roles. 137 | 138 | **Trade-off**: Giving the controller broad IAM permissions could be a security risk, but too limited permissions would restrict functionality. 139 | 140 | **Decision**: Use a combination of: 141 | - Permission boundaries to limit what the controller can create 142 | - IAM conditions to restrict actions to specific role name patterns 143 | - Resource tagging to identify controller-managed resources 144 | 145 | ### Trade-off 2: Validation vs. Flexibility 146 | 147 | **Context**: Strict validation prevents misuse but can limit legitimate use cases. 148 | 149 | **Trade-off**: Stricter validation improves security but reduces flexibility. 150 | 151 | **Decision**: Implement configurable validation rules that can be adjusted by cluster administrators based on their security requirements. 152 | 153 | ### Trade-off 3: State Management 154 | 155 | **Context**: How to handle reconciliation between Kubernetes state and AWS state. 156 | 157 | **Trade-off**: Continuously checking and updating state provides better consistency but increases API calls and potential rate limiting. 158 | 159 | **Decision**: Implement periodic full reconciliation combined with event-driven updates to balance consistency with performance. 160 | 161 | ### Trade-off 4: Role Naming 162 | 163 | **Context**: How to name IAM roles created by the controller. 164 | 165 | **Trade-off**: Including namespace/name in role names provides clarity but can exceed AWS name length limits. 166 | 167 | **Decision**: Use a deterministic naming scheme with prefixes and truncation strategies to handle long names while maintaining uniqueness. 168 | 169 | ## Future Design Considerations 170 | 171 | 1. **Multi-Account Support**: Extend IAM Manager to create roles across multiple AWS accounts 172 | 2. **Enhanced Metrics and Auditing**: Add more detailed metrics and audit logs 173 | 3. **Advanced Policy Templates**: Support for policy templates and inheritance 174 | 4. **Cross-Namespace References**: Allow referencing roles across namespaces with proper authorization 175 | 5. **Integration with External Secrets Management**: Better integration with secrets management systems for sensitive credentials 176 | 177 | ## Lessons Learned 178 | 179 | 1. **AWS API Limitations**: Working within AWS API rate limits and consistency model 180 | 2. **CRD Versioning**: Importance of forward-compatible CRD designs for upgrades 181 | 3. **Error Handling**: Comprehensive error handling for eventual consistency 182 | 4. **Performance Optimization**: Balancing reconciliation frequency with performance 183 | 5. **Security Considerations**: Defense in depth approach to IAM security 184 | -------------------------------------------------------------------------------- /docs/developer-guide.md: -------------------------------------------------------------------------------- 1 | # Developer Guide for IAM Manager 2 | 3 | This guide provides instructions for developers who want to contribute to the iam-manager project. 4 | 5 | ## Project Overview 6 | 7 | IAM Manager is built using [Kubebuilder](https://book.kubebuilder.io/), a framework for building Kubernetes APIs using custom resource definitions (CRDs). Kubebuilder provides scaffolding tools to quickly create new APIs, controllers, and webhook components. Understanding Kubebuilder will greatly help in comprehending the iam-manager codebase structure and development workflow. 8 | 9 | ## Development Environment Setup 10 | 11 | ### Prerequisites 12 | 13 | - Go 1.19+ (check the current version in go.mod) 14 | - Kubernetes cluster for testing (minikube, kind, or a remote cluster) 15 | - Docker for building images 16 | - kubectl CLI 17 | - kustomize 18 | - controller-gen 19 | - AWS account with IAM permissions for testing 20 | 21 | ### Clone the Repository 22 | 23 | ```bash 24 | git clone https://github.com/keikoproj/iam-manager.git 25 | cd iam-manager 26 | ``` 27 | 28 | ### Install Required Tools 29 | 30 | The Makefile can help install required development tools: 31 | 32 | ```bash 33 | # Install controller-gen 34 | make controller-gen 35 | 36 | # Install kustomize 37 | make kustomize 38 | 39 | # Install mockgen (for tests) 40 | make mockgen 41 | ``` 42 | 43 | **Note for ARM64 users**: There are known issues with some versions of controller-gen on ARM64 architecture. If you encounter issues, try specifying a compatible version in the Makefile: 44 | 45 | ```makefile 46 | # Try version v0.13.0 or v0.17.0 for ARM64 47 | controller-gen: 48 | $(call go-install-tool,$(CONTROLLER_GEN),sigs.k8s.io/controller-tools/cmd/controller-gen@v0.13.0) 49 | ``` 50 | 51 | ### Build the Project 52 | 53 | ```bash 54 | # Build the manager binary 55 | make 56 | 57 | # Run the manager locally (outside the cluster) 58 | make run 59 | ``` 60 | 61 | ## Setting Up AWS Resources 62 | 63 | For local development, you'll need to set up the necessary AWS resources: 64 | 65 | ```bash 66 | # Set environment variables 67 | export AWS_ACCOUNT_ID=123456789012 68 | export AWS_REGION=us-west-2 69 | export CLUSTER_NAME=my-cluster 70 | 71 | # Create the AWS resources using CloudFormation 72 | aws cloudformation create-stack \ 73 | --stack-name iam-manager-dev-resources \ 74 | --template-body file://hack/iam-manager-cfn.yaml \ 75 | --capabilities CAPABILITY_NAMED_IAM \ 76 | --parameters ParameterKey=ClusterName,ParameterValue=$CLUSTER_NAME 77 | ``` 78 | 79 | ## Running Tests 80 | 81 | ### Unit Tests 82 | 83 | ```bash 84 | # Run unit tests 85 | make test 86 | ``` 87 | 88 | ### Integration Tests 89 | 90 | For integration tests, you need a Kubernetes cluster and AWS access: 91 | 92 | ```bash 93 | # Set up environment variables for integration tests 94 | export KUBECONFIG=~/.kube/config 95 | export AWS_REGION=us-west-2 96 | export AWS_PROFILE=your-aws-profile 97 | 98 | # Run integration tests 99 | make integration-test 100 | ``` 101 | 102 | ## Creating and Deploying Custom Builds 103 | 104 | ### Building Docker Images 105 | 106 | To build a custom Docker image: 107 | 108 | ```bash 109 | # Build the controller image 110 | make docker-build IMG=your-registry/iam-manager:your-tag 111 | 112 | # Push the image to your registry 113 | make docker-push IMG=your-registry/iam-manager:your-tag 114 | ``` 115 | 116 | ### Deploying Custom Builds 117 | 118 | Deploy your custom build to a cluster: 119 | 120 | ```bash 121 | # Deploy with your custom image 122 | make deploy IMG=your-registry/iam-manager:your-tag 123 | ``` 124 | 125 | ## Code Structure 126 | 127 | Here's an overview of the project structure: 128 | 129 | ``` 130 | . 131 | ├── api/ # API definitions (CRDs) 132 | │ └── v1alpha1/ # API version 133 | ├── cmd/ # Entry points 134 | ├── config/ # Kubernetes YAML manifests 135 | ├── controllers/ # Reconciliation logic 136 | │ └── iamrole_controller.go # Main controller logic 137 | ├── pkg/ # Shared packages 138 | │ ├── awsapi/ # AWS API client wrapper 139 | │ └── k8s/ # Kubernetes helpers 140 | └── hack/ # Development scripts 141 | ``` 142 | 143 | ### Key Components 144 | 145 | - **api/v1alpha1**: Contains the CRD definitions, including the Iamrole type. 146 | - **controllers**: Contains the controller that reconciles the Iamrole custom resources. 147 | - **pkg/awsapi**: Implements the AWS API client for IAM operations. 148 | 149 | ## Making Changes 150 | 151 | ### Adding a New Feature 152 | 153 | 1. Create a new branch: `git checkout -b feature/your-feature-name` 154 | 2. Make your changes 155 | 3. Add tests for your changes 156 | 4. Run tests: `make test` 157 | 5. Build and verify: `make` 158 | 6. Commit changes with DCO signature: `git commit -s -m "Your commit message"` 159 | 7. Push changes: `git push origin feature/your-feature-name` 160 | 8. Create a pull request 161 | 162 | ### Adding New API Fields 163 | 164 | To add new fields to the Iamrole CRD: 165 | 166 | 1. Modify the `api/v1alpha1/iamrole_types.go` file 167 | 2. Run code generation: `make generate` 168 | 3. Update CRDs: `make manifests` 169 | 4. Update the controller reconciliation logic to handle the new fields 170 | 171 | ## Debugging 172 | 173 | ### Running the Controller Locally 174 | 175 | For easier debugging, you can run the controller outside the cluster: 176 | 177 | ```bash 178 | # Run the controller locally 179 | make run 180 | ``` 181 | 182 | ### Remote Debugging 183 | 184 | You can use Delve for remote debugging: 185 | 186 | ```bash 187 | # Install Delve if you don't have it 188 | go install github.com/go-delve/delve/cmd/dlv@latest 189 | 190 | # Run with Delve 191 | dlv debug ./cmd/manager/main.go -- --kubeconfig=$HOME/.kube/config 192 | ``` 193 | 194 | ### Verbose Logging 195 | 196 | To enable debug logs: 197 | 198 | ```bash 199 | # When running locally 200 | make run ARGS="--zap-log-level=debug" 201 | 202 | # In a deployed controller 203 | kubectl edit deployment iam-manager-controller-manager -n iam-manager-system 204 | # Add environment variable LOG_LEVEL=debug 205 | ``` 206 | 207 | ## Code Generation 208 | 209 | iam-manager uses kubebuilder and controller-gen for code generation. 210 | 211 | ### Kubebuilder and controller-gen 212 | 213 | IAM Manager follows the [Kubebuilder](https://book.kubebuilder.io/) project structure and conventions. The project was initially scaffolded using Kubebuilder, which set up: 214 | 215 | - API types in `api/v1alpha1/` 216 | - Controller logic in `controllers/` 217 | - Configuration files in `config/` 218 | - Main entry point in `cmd/manager/main.go` 219 | 220 | When you make changes to the API types, you need to regenerate various files: 221 | 222 | ```bash 223 | # Generate CRDs 224 | make manifests 225 | 226 | # Generate code (deepcopy methods, etc.) 227 | make generate 228 | ``` 229 | 230 | ### Adding New API Types 231 | 232 | To add a new Custom Resource Definition: 233 | 234 | ```bash 235 | # Use kubebuilder to scaffold a new API 236 | kubebuilder create api --group iammanager --version v1alpha1 --kind YourNewResource 237 | 238 | # This will create: 239 | # - api/v1alpha1/yournewresource_types.go 240 | # - controllers/yournewresource_controller.go 241 | # - And update main.go to include the new controller 242 | ``` 243 | 244 | After scaffolding, you'll need to: 245 | 1. Define your API schema in the `_types.go` file 246 | 2. Implement the reconciliation logic in the controller 247 | 3. Regenerate the manifests and code as described above 248 | 249 | ## Working with Webhooks 250 | 251 | iam-manager uses validating webhooks to enforce security policies. To modify webhook logic: 252 | 253 | 1. Edit the validation logic in `api/v1alpha1/iamrole_webhook.go` 254 | 2. Regenerate manifests: `make manifests` 255 | 3. Deploy the changes: `make deploy` 256 | 257 | ## Continuous Integration 258 | 259 | The project uses GitHub Actions for CI. When you submit a PR, the CI will: 260 | 261 | 1. Run unit tests 262 | 2. Build the controller image 263 | 3. Verify code generation is up-to-date 264 | 4. Check code style 265 | 266 | Make sure all CI checks pass before requesting a review. 267 | 268 | ## Releasing 269 | 270 | To create a new release: 271 | 272 | 1. Update version tags in all relevant files 273 | 2. Run tests and ensure they pass 274 | 3. Create a git tag: `git tag -a v0.x.y -m "Release v0.x.y"` 275 | 4. Push the tag: `git push origin v0.x.y` 276 | 5. Create a release on GitHub with release notes 277 | -------------------------------------------------------------------------------- /docs/images/Lock.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keikoproj/iam-manager/bb28308fdb875506e77d11ca30a74fbe443db493/docs/images/Lock.jpg -------------------------------------------------------------------------------- /docs/images/camera.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keikoproj/iam-manager/bb28308fdb875506e77d11ca30a74fbe443db493/docs/images/camera.jpg -------------------------------------------------------------------------------- /docs/images/guard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keikoproj/iam-manager/bb28308fdb875506e77d11ca30a74fbe443db493/docs/images/guard.png -------------------------------------------------------------------------------- /docs/images/iam-manager-arch.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keikoproj/iam-manager/bb28308fdb875506e77d11ca30a74fbe443db493/docs/images/iam-manager-arch.jpeg -------------------------------------------------------------------------------- /docs/install.md: -------------------------------------------------------------------------------- 1 | ### Installation: 2 | 3 | Simplest way to install iam-manager along with the role required for it to do the job is to run [install.sh](hack/install.sh) command. 4 | 5 | Update the allowed policies in [allowed_policies.txt](hack/allowed_policies.txt) and config map properties [config_map](hack/iammanager.keikoproj.io_iamroles-configmap.yaml) as per your environment before you run install.sh. 6 | 7 | Note: You must be cluster admin and have exported KUBECONFIG and also has Administrator access to underlying AWS account and have the credentials exported. 8 | 9 | example: 10 | ```bash 11 | export KUBECONFIG=/Users/myhome/.kube/admin@eks-dev2-k8s 12 | export AWS_PROFILE=admin_123456789012_account 13 | ./install.sh [cluster_name] [aws_region] [aws_profile] 14 | ./install.sh eks-dev2-k8s us-west-2 aws_profile 15 | ``` 16 | 17 | #### Enable Webhook? 18 | iam-manager uses Dynamic Admission control (admission webhooks) to validate the requests against the whitelisted policies and rejects the requests before it gets inserted into etcd. This is the cleanest approach to avoid any more invalid/junk data into etcd. 19 | To enable webhooks, 20 | 1. You must be completed the installation section before you proceed further. 21 | 2. You must have [cert-manager](https://cert-manager.io/docs/) installed on the cluster to manage the certificates. 22 | ```kubectl apply -f cert-manager/cert-manager-v0.12.0.yaml --validate=false``` 23 | 3. Apply webhook spec 24 | ```kubectl apply -f hack/iam-manager_with_webhook.yaml``` 25 | 4. Update the "webhook.enabled" property in config map to true. 26 | 27 | ##### iam-manager with kiam 28 | This installation is where pods can assume the role only via kiam. Kiam server runs on master nodes and any role created must be trusted by master node instance profile to be assumed by kiam. 29 | 30 | example: 31 | ```bash 32 | export KUBECONFIG=/Users/myhome/.kube/admin@eks-dev2-k8s 33 | export AWS_PROFILE=admin_123456789012_account 34 | ./update_with_kiam.sh [cluster_name] [aws_region] [aws_profile] [masters_nodes_instance_profile] 35 | ./update_with_kiam.sh eks-dev2-k8s us-west-2 aws_profile arn:aws:iam::123456789012:role/masters.eks-dev2-k8s 36 | 37 | ``` 38 | 39 | -------------------------------------------------------------------------------- /docs/quickstart.md: -------------------------------------------------------------------------------- 1 | # IAM Manager Quickstart Guide 2 | 3 | This guide will help you quickly get started with iam-manager by walking through the installation process and creating your first IAM role. 4 | 5 | ## Prerequisites 6 | 7 | Before you begin, ensure you have: 8 | 9 | - A Kubernetes cluster (v1.16+) 10 | - `kubectl` configured with admin access to your cluster 11 | - AWS CLI configured with appropriate permissions to create/modify IAM roles 12 | - An IAM role or user with permissions to create and manage IAM roles 13 | 14 | ## Installation 15 | 16 | ### 1. Clone the Repository 17 | 18 | ```bash 19 | git clone https://github.com/keikoproj/iam-manager.git 20 | cd iam-manager 21 | ``` 22 | 23 | ### 2. Create Required AWS Resources 24 | 25 | You need to create the necessary AWS resources, including permission boundaries, before deploying iam-manager. 26 | 27 | ```bash 28 | # Set your AWS account ID and region 29 | export AWS_ACCOUNT_ID=123456789012 30 | export AWS_REGION=us-west-2 31 | 32 | # Create the AWS resources using CloudFormation 33 | aws cloudformation create-stack \ 34 | --stack-name iam-manager-resources \ 35 | --template-body file://hack/iam-manager-cfn.yaml \ 36 | --capabilities CAPABILITY_NAMED_IAM \ 37 | --parameters ParameterKey=ClusterName,ParameterValue=your-cluster-name 38 | ``` 39 | 40 | This creates: 41 | - Permission boundaries for IAM roles 42 | - IAM policy for the iam-manager controller 43 | - Trust relationships for your cluster 44 | 45 | ### 3. Update the ConfigMap 46 | 47 | Edit the ConfigMap to match your environment: 48 | 49 | ```bash 50 | # Open the ConfigMap YAML file 51 | vim config/default/iammanager.keikoproj.io_iamroles-configmap.yaml 52 | 53 | # Update the following values: 54 | # - AWS account ID 55 | # - AWS region 56 | # - Cluster name 57 | # - OIDC provider URL (for EKS with IRSA) 58 | ``` 59 | 60 | ### 4. Deploy the Controller 61 | 62 | ```bash 63 | # Apply CRDs 64 | kubectl apply -f config/crd/bases/ 65 | 66 | # Deploy the controller 67 | make deploy 68 | ``` 69 | 70 | ### 5. Verify the Installation 71 | 72 | ```bash 73 | kubectl get pods -n iam-manager-system 74 | ``` 75 | 76 | You should see the iam-manager-controller-manager pod running. 77 | 78 | ## Creating Your First IAM Role 79 | 80 | ### Basic IAM Role 81 | 82 | Create a file named `my-first-role.yaml`: 83 | 84 | ```yaml 85 | apiVersion: iammanager.keikoproj.io/v1alpha1 86 | kind: Iamrole 87 | metadata: 88 | name: my-first-role 89 | namespace: default 90 | spec: 91 | PolicyDocument: 92 | Statement: 93 | - Effect: "Allow" 94 | Action: 95 | - "s3:GetObject" 96 | - "s3:ListBucket" 97 | Resource: 98 | - "arn:aws:s3:::my-bucket/*" 99 | - "arn:aws:s3:::my-bucket" 100 | Sid: "AllowS3Access" 101 | AssumeRolePolicyDocument: 102 | Version: "2012-10-17" 103 | Statement: 104 | - Effect: "Allow" 105 | Action: "sts:AssumeRole" 106 | Principal: 107 | AWS: 108 | - "arn:aws:iam::123456789012:role/your-trusted-role" 109 | ``` 110 | 111 | Apply the role to your cluster: 112 | 113 | ```bash 114 | kubectl apply -f my-first-role.yaml 115 | ``` 116 | 117 | ### Check Role Status 118 | 119 | ```bash 120 | kubectl get iamrole my-first-role -n default -o yaml 121 | ``` 122 | 123 | You should see the status field populated with information about your role, including its ARN and whether it's ready. 124 | 125 | ## Creating an IAM Role for Service Accounts (IRSA) 126 | 127 | If you're using EKS and want to leverage IRSA, create a role with an annotation: 128 | 129 | ```yaml 130 | apiVersion: iammanager.keikoproj.io/v1alpha1 131 | kind: Iamrole 132 | metadata: 133 | name: app-service-account-role 134 | namespace: default 135 | annotations: 136 | iam.amazonaws.com/irsa-service-account: my-service-account 137 | spec: 138 | PolicyDocument: 139 | Statement: 140 | - Effect: "Allow" 141 | Action: 142 | - "s3:GetObject" 143 | - "s3:ListBucket" 144 | Resource: 145 | - "arn:aws:s3:::my-bucket/*" 146 | - "arn:aws:s3:::my-bucket" 147 | Sid: "AllowS3Access" 148 | ``` 149 | 150 | Apply it to your cluster: 151 | 152 | ```bash 153 | kubectl apply -f irsa-role.yaml 154 | ``` 155 | 156 | ## Using IAM Roles in Your Applications 157 | 158 | ### For Standard IAM Roles 159 | 160 | Add the ARN to your application's AWS SDK configuration or use the AWS SDK's profile feature to assume the role. 161 | 162 | ### For IRSA Roles 163 | 164 | 1. Ensure your pod uses the service account specified in the IRSA annotation: 165 | 166 | ```yaml 167 | apiVersion: apps/v1 168 | kind: Deployment 169 | metadata: 170 | name: my-app 171 | spec: 172 | template: 173 | spec: 174 | serviceAccountName: my-service-account # This must match the IRSA annotation 175 | containers: 176 | - name: app 177 | image: my-app:latest 178 | ``` 179 | 180 | 2. The AWS SDK will automatically use the IAM role's credentials when making API calls from this pod. 181 | 182 | ## Next Steps 183 | 184 | - Explore more [IAM Manager Examples](../examples/) 185 | - Learn about [Configuration Options](configmap-properties.md) 186 | - Read the [Architecture Documentation](architecture.md) 187 | - Set up [AWS Integration](aws-integration.md) for advanced scenarios 188 | - Review [AWS Security](AWS_Security.md) features 189 | 190 | ## Troubleshooting 191 | 192 | If you encounter issues, check the [Troubleshooting Guide](troubleshooting.md) or view the controller logs: 193 | 194 | ```bash 195 | kubectl logs -n iam-manager-system deployment/iam-manager-controller-manager 196 | ``` 197 | -------------------------------------------------------------------------------- /examples/basic-iam-role.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: iammanager.keikoproj.io/v1alpha1 2 | kind: Iamrole 3 | metadata: 4 | name: basic-s3-reader 5 | namespace: default 6 | spec: 7 | # Policy document that defines the permissions for this role 8 | PolicyDocument: 9 | Statement: 10 | - Effect: "Allow" 11 | Action: 12 | - "s3:GetObject" 13 | - "s3:ListBucket" 14 | Resource: 15 | - "arn:aws:s3:::example-bucket/*" 16 | - "arn:aws:s3:::example-bucket" 17 | Sid: "AllowS3ReadAccess" 18 | 19 | # Trust policy that defines who can assume this role 20 | AssumeRolePolicyDocument: 21 | Version: "2012-10-17" 22 | Statement: 23 | - Effect: "Allow" 24 | Action: "sts:AssumeRole" 25 | Principal: 26 | AWS: 27 | # Replace with the ARN of the entity that should be able to assume this role 28 | - "arn:aws:iam::123456789012:role/KubernetesNode" 29 | -------------------------------------------------------------------------------- /examples/complex-s3-role.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: iammanager.keikoproj.io/v1alpha1 2 | kind: Iamrole 3 | metadata: 4 | name: complex-s3-manager 5 | namespace: default 6 | spec: 7 | # Policy document that defines the permissions for this role 8 | # This example shows a more complex policy with multiple statements and conditions 9 | PolicyDocument: 10 | Statement: 11 | - Effect: "Allow" 12 | Action: 13 | - "s3:GetObject" 14 | - "s3:PutObject" 15 | - "s3:DeleteObject" 16 | - "s3:ListBucket" 17 | Resource: 18 | - "arn:aws:s3:::app-data-bucket" 19 | - "arn:aws:s3:::app-data-bucket/data/*" 20 | Sid: "S3DataAccess" 21 | # Using conditions to restrict access 22 | Condition: 23 | StringLike: 24 | "s3:prefix": 25 | - "data/" 26 | - "uploads/" 27 | 28 | - Effect: "Allow" 29 | Action: 30 | - "s3:ListAllMyBuckets" 31 | Resource: 32 | - "*" 33 | Sid: "ListBuckets" 34 | 35 | - Effect: "Allow" 36 | Action: 37 | - "kms:Decrypt" 38 | - "kms:GenerateDataKey" 39 | Resource: 40 | - "arn:aws:kms:*:*:key/1234abcd-12ab-34cd-56ef-1234567890ab" 41 | Sid: "KMSAccess" 42 | 43 | # Trust policy that defines who can assume this role 44 | AssumeRolePolicyDocument: 45 | Version: "2012-10-17" 46 | Statement: 47 | - Effect: "Allow" 48 | Action: "sts:AssumeRole" 49 | Principal: 50 | AWS: 51 | - "arn:aws:iam::123456789012:role/KubernetesNode" 52 | # Adding condition to restrict which source accounts can assume the role 53 | Condition: 54 | StringEquals: 55 | "aws:SourceAccount": "123456789012" 56 | -------------------------------------------------------------------------------- /examples/irsa-role.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: iammanager.keikoproj.io/v1alpha1 2 | kind: Iamrole 3 | metadata: 4 | name: irsa-dynamodb-app 5 | namespace: default 6 | annotations: 7 | # This annotation specifies which service account will be associated with this role 8 | iam.amazonaws.com/irsa-service-account: app-service-account 9 | spec: 10 | # Policy document that defines the permissions for this role 11 | PolicyDocument: 12 | Statement: 13 | - Effect: "Allow" 14 | Action: 15 | - "dynamodb:GetItem" 16 | - "dynamodb:PutItem" 17 | - "dynamodb:DeleteItem" 18 | - "dynamodb:Query" 19 | - "dynamodb:Scan" 20 | Resource: 21 | - "arn:aws:dynamodb:*:*:table/app-data-table" 22 | Sid: "AllowDynamoDBAccess" 23 | - Effect: "Allow" 24 | Action: 25 | - "logs:CreateLogGroup" 26 | - "logs:CreateLogStream" 27 | - "logs:PutLogEvents" 28 | Resource: 29 | - "arn:aws:logs:*:*:*" 30 | Sid: "AllowLogging" 31 | 32 | # When using IRSA, the trust policy is automatically configured by the controller 33 | # based on the OIDC provider of your EKS cluster and the specified service account 34 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/keikoproj/iam-manager 2 | 3 | go 1.24 4 | 5 | require ( 6 | github.com/aws/aws-sdk-go v1.55.7 7 | github.com/go-logr/logr v1.4.2 8 | github.com/golang/mock v1.6.0 9 | github.com/onsi/ginkgo/v2 v2.23.4 10 | github.com/onsi/gomega v1.37.0 11 | github.com/pborman/uuid v1.2.1 12 | github.com/pkg/errors v0.9.1 13 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c 14 | k8s.io/api v0.32.5 15 | k8s.io/apimachinery v0.32.5 16 | k8s.io/client-go v0.32.5 17 | k8s.io/klog v1.0.0 18 | sigs.k8s.io/controller-runtime v0.20.4 19 | ) 20 | 21 | require ( 22 | cel.dev/expr v0.18.0 // indirect 23 | github.com/antlr4-go/antlr/v4 v4.13.0 // indirect 24 | github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a // indirect 25 | github.com/beorn7/perks v1.0.1 // indirect 26 | github.com/blang/semver/v4 v4.0.0 // indirect 27 | github.com/cenkalti/backoff/v4 v4.3.0 // indirect 28 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 29 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 30 | github.com/emicklei/go-restful/v3 v3.11.0 // indirect 31 | github.com/evanphx/json-patch/v5 v5.9.11 // indirect 32 | github.com/felixge/httpsnoop v1.0.4 // indirect 33 | github.com/fsnotify/fsnotify v1.7.0 // indirect 34 | github.com/fxamacker/cbor/v2 v2.7.0 // indirect 35 | github.com/go-logr/stdr v1.2.2 // indirect 36 | github.com/go-logr/zapr v1.3.0 // indirect 37 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 38 | github.com/go-openapi/jsonreference v0.20.2 // indirect 39 | github.com/go-openapi/swag v0.23.0 // indirect 40 | github.com/go-task/slim-sprig/v3 v3.0.0 // indirect 41 | github.com/gogo/protobuf v1.3.2 // indirect 42 | github.com/golang/protobuf v1.5.4 // indirect 43 | github.com/google/btree v1.1.3 // indirect 44 | github.com/google/cel-go v0.22.0 // indirect 45 | github.com/google/gnostic-models v0.6.8 // indirect 46 | github.com/google/go-cmp v0.7.0 // indirect 47 | github.com/google/gofuzz v1.2.0 // indirect 48 | github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect 49 | github.com/google/uuid v1.6.0 // indirect 50 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect 51 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 52 | github.com/jmespath/go-jmespath v0.4.0 // indirect 53 | github.com/josharian/intern v1.0.0 // indirect 54 | github.com/json-iterator/go v1.1.12 // indirect 55 | github.com/kr/pretty v0.3.1 // indirect 56 | github.com/kr/text v0.2.0 // indirect 57 | github.com/mailru/easyjson v0.7.7 // indirect 58 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 59 | github.com/modern-go/reflect2 v1.0.2 // indirect 60 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 61 | github.com/prometheus/client_golang v1.19.1 // indirect 62 | github.com/prometheus/client_model v0.6.1 // indirect 63 | github.com/prometheus/common v0.55.0 // indirect 64 | github.com/prometheus/procfs v0.15.1 // indirect 65 | github.com/rogpeppe/go-internal v1.12.0 // indirect 66 | github.com/spf13/cobra v1.8.1 // indirect 67 | github.com/spf13/pflag v1.0.5 // indirect 68 | github.com/stoewer/go-strcase v1.3.0 // indirect 69 | github.com/x448/float16 v0.8.4 // indirect 70 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect 71 | go.opentelemetry.io/otel v1.28.0 // indirect 72 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 // indirect 73 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 // indirect 74 | go.opentelemetry.io/otel/metric v1.28.0 // indirect 75 | go.opentelemetry.io/otel/sdk v1.28.0 // indirect 76 | go.opentelemetry.io/otel/trace v1.28.0 // indirect 77 | go.opentelemetry.io/proto/otlp v1.3.1 // indirect 78 | go.uber.org/automaxprocs v1.6.0 // indirect 79 | go.uber.org/multierr v1.11.0 // indirect 80 | go.uber.org/zap v1.27.0 // indirect 81 | golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect 82 | golang.org/x/net v0.38.0 // indirect 83 | golang.org/x/oauth2 v0.23.0 // indirect 84 | golang.org/x/sync v0.12.0 // indirect 85 | golang.org/x/sys v0.32.0 // indirect 86 | golang.org/x/term v0.30.0 // indirect 87 | golang.org/x/text v0.23.0 // indirect 88 | golang.org/x/time v0.7.0 // indirect 89 | golang.org/x/tools v0.31.0 // indirect 90 | gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect 91 | google.golang.org/genproto/googleapis/api v0.0.0-20240826202546-f6391c0de4c7 // indirect 92 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240826202546-f6391c0de4c7 // indirect 93 | google.golang.org/grpc v1.65.0 // indirect 94 | google.golang.org/protobuf v1.36.5 // indirect 95 | gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect 96 | gopkg.in/inf.v0 v0.9.1 // indirect 97 | gopkg.in/yaml.v3 v3.0.1 // indirect 98 | k8s.io/apiextensions-apiserver v0.32.1 // indirect 99 | k8s.io/apiserver v0.32.1 // indirect 100 | k8s.io/component-base v0.32.1 // indirect 101 | k8s.io/klog/v2 v2.130.1 // indirect 102 | k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect 103 | k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect 104 | sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.0 // indirect 105 | sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect 106 | sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect 107 | sigs.k8s.io/yaml v1.4.0 // indirect 108 | ) 109 | -------------------------------------------------------------------------------- /hack/allowed_policies.txt: -------------------------------------------------------------------------------- 1 | "s3:*,sts:*,ec2:Describe,acm:Describe,acm:List,acm:Get,route53:Get,route53:List,route53:Create,route53:Delete,route53:Change,kms:Decrypt,kms:Encrypt,kms:ReEncrypt,kms:GenerateDataKey,kms:DescribeKey,dynamodb:*,secretsmanager:GetSecretValue,es:*,sqs:SendMessage,sqs:ReceiveMessage,sqs:DeleteMessage,sns:Publish,sqs:GetQueueAttributes,sqs:GetQueueUrl" -------------------------------------------------------------------------------- /hack/boilerplate.go.txt: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ -------------------------------------------------------------------------------- /hack/cert.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: cert-manager.io/v1alpha2 2 | kind: Certificate 3 | metadata: 4 | labels: 5 | app: iam-manager 6 | name: iam-manager-serving-cert 7 | namespace: cert-manager 8 | spec: 9 | commonName: iam-manager-webhook-service.iam-manager-system.svc 10 | dnsNames: 11 | - iam-manager-webhook-service.iam-manager-system.svc.cluster.local 12 | issuerRef: 13 | kind: Issuer 14 | name: iam-manager-selfsigned-issuer 15 | secretName: webhook-server-cert 16 | renewBefore: "2159h55m0s" 17 | --- 18 | apiVersion: cert-manager.io/v1alpha2 19 | kind: Issuer 20 | metadata: 21 | labels: 22 | app: iam-manager 23 | name: iam-manager-selfsigned-issuer 24 | namespace: cert-manager 25 | spec: 26 | selfSigned: {} -------------------------------------------------------------------------------- /hack/iam-manager-cfn.yaml: -------------------------------------------------------------------------------- 1 | # aws cloudformation validate-template --template-body file://template.yaml 2 | AWSTemplateFormatVersion : 2010-09-09 3 | Transform: AWS::Serverless-2016-10-31 4 | Description: iam-manager template 5 | Metadata: 6 | Name: iam-manager 7 | Version: 1.0.0 8 | Parameters: 9 | DeploymentType: 10 | Description: Deployment type of iam-manager whether to use kiam by iam-manager or use direct instance profile 11 | Type: String 12 | AllowedValues: ["instance", "kiam"] 13 | Default: "instance" 14 | ConstraintDescription: "Must specify kiam or instance" 15 | ParamK8sTrustRole: 16 | Description: Role to be assumed in case of kiam 17 | Type: String 18 | ParamK8sClusterName: 19 | Description: Cluster Name to be included in the names 20 | Type: String 21 | Deafult: "cluster" 22 | AllowedPolicyList: 23 | Description: Allowed IAM policy list 24 | Type: CommaDelimitedList 25 | Default: "" 26 | Conditions: 27 | CreateInstanceProfile: !Equals [!Ref DeploymentType, instance ] 28 | CreateIAMRole: !Equals [!Ref DeploymentType, kiam ] 29 | Resources: 30 | ### Permission Boundary to be attached #### 31 | IAMManagerPermissionBoundary: 32 | Type: 'AWS::IAM::ManagedPolicy' 33 | Properties: 34 | Description: "PermissionBoundary to be used by iam-manager" 35 | ManagedPolicyName: !Sub "k8s-iam-manager-${ParamK8sClusterName}-permission-boundary" 36 | PolicyDocument: 37 | Version: 2012-10-17 38 | Statement: 39 | - Effect: Allow 40 | Action: !Ref AllowedPolicyList 41 | Resource: "*" 42 | Sid: "AllowJustThisAccess" 43 | ### IAM Policy to be attached to iam-manager role ### 44 | IAMManagerPolicy: 45 | Type: 'AWS::IAM::Policy' 46 | DependsOn: IAMManagerAccessRole 47 | Properties: 48 | PolicyName: !Sub "k8s-iam-manager-${ParamK8sClusterName}-policy" 49 | PolicyDocument: 50 | Version: 2012-10-17 51 | Statement: 52 | - Effect: Deny 53 | Action: 54 | - "iam:DeleteRolePermissionsBoundary" 55 | Resource: !Ref IAMManagerPermissionBoundary 56 | Sid: "DenySelfPermissionBoundaryDelete" 57 | - Effect: "Allow" 58 | Action: 59 | - "iam:CreateRole" 60 | Resource: !Sub "arn:aws:iam::${AWS::AccountId}:role/k8s-*" 61 | Condition: 62 | StringEquals: 63 | iam:PermissionsBoundary: !Sub "arn:aws:iam::${AWS::AccountId}:policy/k8s-iam-manager-${ParamK8sClusterName}-permission-boundary" 64 | Sid: "AllowOnlyWithPermBoundary" 65 | - Effect: "Allow" 66 | Action: 67 | - "iam:AttachRolePolicy" 68 | - "iam:AddPermissionBoundary" 69 | - "iam:CreatePolicy" 70 | - "iam:DeletePolicy" 71 | - "iam:DetachRolePolicy" 72 | - "iam:PutRolePolicy" 73 | - "iam:PutRolePermissionsBoundary" 74 | - "iam:UpdateAssumeRolePolicy" 75 | - "iam:DeleteRolePolicy" 76 | - "iam:DeletePolicy" 77 | - "iam:UpdateRole" 78 | - "iam:DeleteRole" 79 | - "iam:GetRole" 80 | - "iam:GetRolePolicy" 81 | - "iam:GetPolicy" 82 | - "iam:ListRoles" 83 | - "iam:ListRolePolicies" 84 | - "iam:ListAttachedRolePolicies" 85 | - "iam:ListPolicies" 86 | Resource: !Sub "arn:aws:iam::${AWS::AccountId}:role/k8s-*" 87 | Condition: 88 | StringEquals: 89 | iam:ResourceTag/managedBy: iam-manager 90 | Sid: "AllowOnlyWithTag" 91 | - Effect: "Allow" 92 | Action: 93 | - "iam:TagRole" 94 | - "iam:UntagRole" 95 | - "iam:ListRoleTags" 96 | Resource: !Sub "arn:aws:iam::${AWS::AccountId}:role/k8s-*" 97 | Sid: "Allow" 98 | - Effect: "Allow" 99 | Action: 100 | - "iam:CreateOpenIDConnectProvider" 101 | - "eks:DescribeCluster" 102 | Resource: "*" 103 | Sid: "IRSANeededPermissions" 104 | Roles: 105 | - !Ref IAMManagerAccessRole 106 | ##### IAM Role to be assumed #### 107 | IAMManagerAccessRole: 108 | Type: AWS::IAM::Role 109 | Properties: 110 | RoleName: !Sub "k8s-iam-manager-${ParamK8sClusterName}-role" 111 | AssumeRolePolicyDocument: 112 | Version: "2012-10-17" 113 | Id: "IamAssumeRole" 114 | Statement: 115 | Effect: "Allow" 116 | Sid: "AllowAssumeRole" 117 | Action: "sts:AssumeRole" 118 | Principal: 119 | AWS: !If [CreateIAMRole, !Sub "${ParamK8sTrustRole}", !Ref "AWS::NoValue"] 120 | Service: !If [CreateInstanceProfile, "ec2.amazonaws.com", !Ref "AWS::NoValue"] 121 | Path: "/" 122 | IAMManagerInstanceProfile: 123 | Type: 'AWS::IAM::InstanceProfile' 124 | Condition: CreateInstanceProfile 125 | Properties: 126 | InstanceProfileName: !Sub "k8s-iam-manager-${ParamK8sClusterName}-instance-profile" 127 | Path: / 128 | Roles: 129 | - !Ref IAMManagerAccessRole 130 | ####### Outputs 131 | Outputs: 132 | IAMManagerAccessRole: 133 | Description: IAM Role created for iam-manager 134 | Value: !GetAtt IAMManagerAccessRole.Arn 135 | -------------------------------------------------------------------------------- /hack/iammanager.keikoproj.io_iamroles-configmap.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: iamroles-v1alpha1-configmap 5 | namespace: dev 6 | data: 7 | iam.policy.action.prefix.whitelist: "s3:,sts:,ec2:Describe,acm:Describe,acm:List,acm:Get,route53:Get,route53:List,route53:Create,route53:Delete,route53:Change,kms:Decrypt,kms:Encrypt,kms:ReEncrypt,kms:GenerateDataKey,kms:DescribeKey,dynamodb:,secretsmanager:GetSecretValue,es:,sqs:SendMessage,sqs:ReceiveMessage,sqs:DeleteMessage,SNS:Publish,sqs:GetQueueAttributes,sqs:GetQueueUrl" 8 | iam.policy.resource.blacklist: "kops" 9 | iam.policy.s3.restricted.resource: "*" 10 | aws.region: "us-west-2" 11 | aws.MasterRole: "masters.cluster.k8s.local" 12 | iam.managed.policies: "shared.cluster.k8s.local" -------------------------------------------------------------------------------- /hack/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | # Script to install iam-manager on a Kubernetes cluster 5 | # 6 | # Usage: 7 | # ./install.sh [cluster_name] [aws_region] [aws_profile] 8 | # 9 | # Arguments: 10 | # cluster_name - Name of the Kubernetes cluster 11 | # aws_region - AWS region where resources will be created 12 | # aws_profile - AWS profile to use for authentication 13 | 14 | # Display usage information 15 | function usage() { 16 | echo "Usage: $0 " 17 | echo "" 18 | echo "Arguments:" 19 | echo " cluster_name - Name of the Kubernetes cluster" 20 | echo " aws_region - AWS region where resources will be created (e.g., us-west-2)" 21 | echo " aws_profile - AWS profile to use for authentication" 22 | echo "" 23 | echo "Example:" 24 | echo " $0 my-eks-cluster us-west-2 my-aws-profile" 25 | exit 1 26 | } 27 | 28 | # Check if all required parameters are provided 29 | if [ $# -ne 3 ]; then 30 | echo "Error: Missing required parameters." 31 | usage 32 | fi 33 | 34 | # Validate parameters 35 | if [ -z "$1" ]; then 36 | echo "Error: Cluster name cannot be empty." 37 | usage 38 | fi 39 | 40 | if [ -z "$2" ]; then 41 | echo "Error: AWS region cannot be empty." 42 | usage 43 | fi 44 | 45 | if [ -z "$3" ]; then 46 | echo "Error: AWS profile cannot be empty." 47 | usage 48 | fi 49 | 50 | # Assign parameters to named variables for better readability 51 | CLUSTER_NAME=$1 52 | AWS_REGION=$2 53 | AWS_PROFILE=$3 54 | 55 | # Check if AWS profile exists 56 | if ! aws configure list-profiles 2>/dev/null | grep -q "^$AWS_PROFILE$"; then 57 | echo "Warning: AWS profile '$AWS_PROFILE' not found in your AWS config." 58 | echo "Please make sure the profile exists or check your AWS configuration." 59 | read -p "Continue anyway? (y/n): " CONTINUE 60 | if [[ ! $CONTINUE =~ ^[Yy]$ ]]; then 61 | echo "Installation aborted." 62 | exit 1 63 | fi 64 | fi 65 | 66 | # Verify kubectl is installed and configured 67 | if ! command -v kubectl &> /dev/null; then 68 | echo "Error: kubectl is not installed or not in PATH." 69 | echo "Please install kubectl and try again." 70 | exit 1 71 | fi 72 | 73 | # Check if the current kubectl context is pointing to the correct cluster 74 | CURRENT_CONTEXT=$(kubectl config current-context 2>/dev/null || echo "none") 75 | echo "Current kubectl context: $CURRENT_CONTEXT" 76 | read -p "Is this the correct Kubernetes cluster? (y/n): " CORRECT_CLUSTER 77 | if [[ ! $CORRECT_CLUSTER =~ ^[Yy]$ ]]; then 78 | echo "Please set the correct kubectl context and try again." 79 | exit 1 80 | fi 81 | 82 | echo "Installing iam-manager for cluster: $CLUSTER_NAME in region: $AWS_REGION using AWS profile: $AWS_PROFILE" 83 | echo "---" 84 | 85 | # Split cluster name by "." delimiter to avoid naming syntax issues 86 | CLUSTER_SHORT_NAME=$(echo $CLUSTER_NAME | cut -d. -f1) 87 | 88 | # Get allowed policies from file 89 | POLICY_LIST=$(cat allowed_policies.txt) 90 | if [ -z "$POLICY_LIST" ]; then 91 | echo "Warning: allowed_policies.txt is empty. The permission boundary will not have any allowed actions." 92 | read -p "Continue anyway? (y/n): " CONTINUE_EMPTY 93 | if [[ ! $CONTINUE_EMPTY =~ ^[Yy]$ ]]; then 94 | echo "Installation aborted." 95 | exit 1 96 | fi 97 | fi 98 | 99 | echo "Allowed policies: $POLICY_LIST" 100 | 101 | # Create CloudFormation stack 102 | echo "Creating CloudFormation stack for IAM resources..." 103 | aws cloudformation create-stack \ 104 | --stack-name iam-manager-$CLUSTER_SHORT_NAME-cfn \ 105 | --template-body file://iam-manager-cfn.yaml \ 106 | --parameters \ 107 | ParameterKey=ParamK8sClusterName,ParameterValue=$CLUSTER_NAME \ 108 | ParameterKey=AllowedPolicyList,ParameterValue="$POLICY_LIST" \ 109 | --capabilities CAPABILITY_IAM CAPABILITY_NAMED_IAM CAPABILITY_AUTO_EXPAND \ 110 | --on-failure DELETE \ 111 | --region $AWS_REGION \ 112 | --profile $AWS_PROFILE 113 | 114 | echo "Waiting for CloudFormation stack creation to complete..." 115 | aws cloudformation wait stack-create-complete \ 116 | --stack-name iam-manager-$CLUSTER_SHORT_NAME-cfn \ 117 | --region $AWS_REGION \ 118 | --profile $AWS_PROFILE 119 | 120 | # Install iam-manager in the cluster 121 | echo "Installing iam-manager Kubernetes resources..." 122 | kubectl apply -f iam-manager/iam-manager.yaml 123 | 124 | # Install ConfigMap 125 | echo "Applying iam-manager ConfigMap..." 126 | kubectl apply -f iam-manager/iammanager.keikoproj.io_iamroles-configmap.yaml 127 | 128 | echo "---" 129 | echo "Installation complete!" 130 | echo "You can verify the installation by running:" 131 | echo " kubectl get pods -n iam-manager-system" 132 | -------------------------------------------------------------------------------- /hack/update_with_kiam.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ## $1 => cluster name 3 | ## $2 => region 4 | ## $3 => aws_profile 5 | ## $4 => master node instance profile ARN 6 | 7 | ##Split cluster name by "." delimeter to avoid naming syntax issues 8 | cluster=$(echo $1 | cut -d. -f1) 9 | 10 | ## Execute CFN using awscli command 11 | policyList=`cat allowed_policies.txt` 12 | echo $policyList 13 | aws cloudformation update-stack --stack-name iam-manager-$cluster-cfn --use-previous-template --parameters ParameterKey=DeploymentType,ParameterValue=kiam ParameterKey=ParamK8sTrustRole,ParameterValue=$4 ParameterKey=ParamK8sClusterName,ParameterValue=$1 ParameterKey=AllowedPolicyList,ParameterValue=$policyList --capabilities CAPABILITY_IAM CAPABILITY_NAMED_IAM CAPABILITY_AUTO_EXPAND --on-failure DELETE --region $2 --profile $3 14 | 15 | ## install config map 16 | kubectl apply -f iam-manager/iammanager.keikoproj.io_iamroles-configmap.yaml 17 | 18 | ## add kiam annotation to deployment spec 19 | kubens iam-manager-system 20 | kubectl patch deployment/iam-manager-controller-manager -p '{"spec":{"template":{"metadata":{"annotations":{"iam.amazonaws.com/role": "k8s-iam-manager-'$1'-role"}}}}}' --ns iam-manager-system 21 | 22 | ## Test and verify 23 | -------------------------------------------------------------------------------- /internal/config/constants.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | // Global constants 4 | const ( 5 | // InlinePolicyName defines user managed inline policy 6 | InlinePolicyName = "custom" 7 | 8 | // IamManagerNamespaceName is the namespace name where iam-manager controllers are running 9 | IamManagerNamespaceName = "iam-manager-system" 10 | 11 | // IamManagerConfigMapName is the config map name for iam-manager namespace 12 | IamManagerConfigMapName = "iam-manager-iamroles-v1alpha1-configmap" 13 | ) 14 | 15 | const ( 16 | // iam policy action prefix 17 | propertyIamPolicyWhitelist = "iam.policy.action.prefix.whitelist" 18 | 19 | // iam policy to blacklist resource 20 | propertyIamPolicyBlacklist = "iam.policy.resource.blacklist" 21 | 22 | // iam policy for restricting s3 resources 23 | propertyIamPolicyS3Restricted = "iam.policy.s3.restricted.resource" 24 | 25 | // aws region 26 | propertyAwsRegion = "aws.region" 27 | 28 | //enable webhook property 29 | propertyAWSAccountID = "aws.accountId" 30 | 31 | // user managed policies 32 | propertyManagedPolicies = "iam.managed.policies" 33 | 34 | // user managed permission boundary policy 35 | propertyPermissionBoundary = "iam.managed.permission.boundary.policy" 36 | 37 | //enable webhook property 38 | propertyWebhookEnabled = "webhook.enabled" 39 | 40 | //golang-templated pattern to use for iam role name generation 41 | propertyIamRolePattern = "iam.role.pattern" 42 | 43 | //max allowed aws iam roles per namespace 44 | propertyMaxIamRoles = "iam.role.max.limit.per.namespace" 45 | 46 | //propertyDesiredStateFrequency is a configurable param to make sure to check the external state (in seconds). default to 30 mins (1800 seconds) 47 | propertyDesiredStateFrequency = "controller.desired.frequency" 48 | 49 | //propertyClusterName can be used to set cluster name 50 | propertyClusterName = "k8s.cluster.name" 51 | 52 | //propertyIRSAEnabled can be used to enable/disable IRSA support by IAM-Manager 53 | propertyIRSAEnabled = "iam.irsa.enabled" 54 | 55 | //propertyK8sClusterOIDCIssuerUrl can be used to provide OIDC issuer url 56 | propertyK8sClusterOIDCIssuerUrl = "k8s.cluster.oidc.issuer.url" 57 | 58 | //propertyDefaultTrustPolicy can be used to provide default trust policy 59 | propertyDefaultTrustPolicy = "iam.default.trust.policy" 60 | 61 | //propertyIRSAaRegionalEndpointDisabled can be used to disable sts regional endpoints for service accounts, and use global endpoint in us-east-1 instead 62 | propertyIRSARegionalEndpointDisabled = "iam.irsa.regional.endpoint.disabled" 63 | ) 64 | 65 | const ( 66 | separator = "," 67 | 68 | OIDCAudience = "sts.amazonaws.com" 69 | 70 | IRSAAnnotation = "iam.amazonaws.com/irsa-service-account" 71 | 72 | IamManagerPrivilegedNamespaceAnnotation = "iammanager.keikoproj.io/privileged" 73 | 74 | IamManagerTagsAnnotation = "iammanager.keikoproj.io/tags" 75 | 76 | IRSARegionalEndpointAnnotation = "eks.amazonaws.com/sts-regional-endpoints" 77 | ) 78 | 79 | const ( 80 | ControllerMinimumDesiredFrequency = 1800 81 | ) 82 | -------------------------------------------------------------------------------- /internal/config/properties_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/golang/mock/gomock" 9 | "gopkg.in/check.v1" 10 | "k8s.io/api/core/v1" 11 | ) 12 | 13 | type PropertiesSuite struct { 14 | t *testing.T 15 | ctx context.Context 16 | mockCtrl *gomock.Controller 17 | } 18 | 19 | func TestPropertiesSuite(t *testing.T) { 20 | check.Suite(&PropertiesSuite{t: t}) 21 | check.TestingT(t) 22 | } 23 | 24 | func (s *PropertiesSuite) SetUpTest(c *check.C) { 25 | s.ctx = context.Background() 26 | s.mockCtrl = gomock.NewController(s.t) 27 | } 28 | 29 | func (s *PropertiesSuite) TearDownTest(c *check.C) { 30 | s.mockCtrl.Finish() 31 | } 32 | 33 | // test local properties for local environment 34 | func (s *PropertiesSuite) TestLoadPropertiesLocalEnvSuccess(c *check.C) { 35 | Props = nil 36 | err := LoadProperties("LOCAL") 37 | c.Assert(err, check.IsNil) 38 | c.Assert(Props, check.NotNil) 39 | c.Assert(Props.AWSAccountID(), check.Equals, "123456789012") 40 | } 41 | 42 | // test failure when env is not local and cm is empty 43 | // should not return nil pointer 44 | func (s *PropertiesSuite) TestLoadPropertiesFailedNoCM(c *check.C) { 45 | Props = nil 46 | err := LoadProperties("") 47 | c.Assert(err, check.NotNil) 48 | c.Assert(err.Error(), check.Equals, "config map cannot be nil") 49 | } 50 | 51 | func (s *PropertiesSuite) TestLoadPropertiesFailedNilCM(c *check.C) { 52 | Props = nil 53 | err := LoadProperties("", nil) 54 | c.Assert(err, check.NotNil) 55 | c.Assert(err.Error(), check.Equals, "config map cannot be nil") 56 | } 57 | 58 | func (s *PropertiesSuite) TestLoadPropertiesSuccess(c *check.C) { 59 | Props = nil 60 | cm := &v1.ConfigMap{ 61 | Data: map[string]string{ 62 | "iam.managed.permission.boundary.policy": "iam-manager-permission-boundary", 63 | "aws.accountId": "123456789012", 64 | "iam.role.max.limit.per.namespace": "5", 65 | "aws.region": "us-east-2", 66 | "webhook.enabled": "true", 67 | }, 68 | } 69 | err := LoadProperties("", cm) 70 | c.Assert(err, check.IsNil) 71 | c.Assert(Props.AWSRegion(), check.Equals, "us-east-2") 72 | c.Assert(Props.MaxRolesAllowed(), check.Equals, 5) 73 | c.Assert(Props.IsWebHookEnabled(), check.Equals, true) 74 | c.Assert(Props.AWSAccountID(), check.Equals, "123456789012") 75 | c.Assert(strings.HasPrefix(Props.ManagedPermissionBoundaryPolicy(), "arn:aws:iam:"), check.Equals, true) 76 | } 77 | 78 | func (s *PropertiesSuite) TestLoadPropertiesSuccessWithDefaults(c *check.C) { 79 | Props = nil 80 | cm := &v1.ConfigMap{ 81 | Data: map[string]string{ 82 | "iam.managed.permission.boundary.policy": "iam-manager-permission-boundary", 83 | "aws.accountId": "123456789012", 84 | }, 85 | } 86 | err := LoadProperties("", cm) 87 | c.Assert(err, check.IsNil) 88 | c.Assert(Props.AWSRegion(), check.Equals, "us-west-2") 89 | c.Assert(Props.MaxRolesAllowed(), check.Equals, 1) 90 | c.Assert(Props.ControllerDesiredFrequency(), check.Equals, 1800) 91 | c.Assert(Props.IsWebHookEnabled(), check.Equals, false) 92 | c.Assert(Props.AWSAccountID(), check.Equals, "123456789012") 93 | c.Assert(strings.HasPrefix(Props.ManagedPermissionBoundaryPolicy(), "arn:aws:iam:"), check.Equals, true) 94 | c.Assert(Props.IamRolePattern(), check.Equals, "k8s-{{ .ObjectMeta.Name }}") 95 | //when an emty string passed split strings gives you array of 1 with "" 96 | c.Assert(len(Props.ManagedPolicies()), check.Equals, 1) 97 | c.Assert(Props.ManagedPolicies()[0], check.Equals, "") 98 | 99 | } 100 | 101 | func (s *PropertiesSuite) TestLoadPropertiesSuccessWithDefaultsManagedPoliciesWithNoPrefix(c *check.C) { 102 | Props = nil 103 | cm := &v1.ConfigMap{ 104 | Data: map[string]string{ 105 | "iam.managed.permission.boundary.policy": "iam-manager-permission-boundary", 106 | "aws.accountId": "123456789012", 107 | "iam.managed.policies": "DescribeEC2", 108 | }, 109 | } 110 | err := LoadProperties("", cm) 111 | c.Assert(err, check.IsNil) 112 | c.Assert(Props.AWSRegion(), check.Equals, "us-west-2") 113 | c.Assert(Props.MaxRolesAllowed(), check.Equals, 1) 114 | c.Assert(Props.ControllerDesiredFrequency(), check.Equals, 1800) 115 | c.Assert(Props.IsWebHookEnabled(), check.Equals, false) 116 | c.Assert(Props.AWSAccountID(), check.Equals, "123456789012") 117 | c.Assert(strings.HasPrefix(Props.ManagedPermissionBoundaryPolicy(), "arn:aws:iam:"), check.Equals, true) 118 | //when an emty string passed split strings gives you array of 1 with "" 119 | c.Assert(len(Props.ManagedPolicies()), check.Equals, 1) 120 | c.Assert(Props.ManagedPolicies()[0], check.Equals, "arn:aws:iam::123456789012:policy/DescribeEC2") 121 | 122 | } 123 | 124 | func (s *PropertiesSuite) TestLoadPropertiesSuccessWithCustom(c *check.C) { 125 | Props = nil 126 | cm := &v1.ConfigMap{ 127 | Data: map[string]string{ 128 | "iam.managed.permission.boundary.policy": "iam-manager-permission-boundary", 129 | "aws.accountId": "123456789012", 130 | "iam.role.derive.from.namespace": "true", 131 | "controller.desired.frequency": "30", 132 | "iam.role.max.limit.per.namespace": "5", 133 | "iam.role.pattern": "pfx-{{ .ObjectMeta.Name }}", 134 | "iam.irsa.regional.endpoint.disabled": "true", 135 | }, 136 | } 137 | err := LoadProperties("", cm) 138 | c.Assert(err, check.IsNil) 139 | c.Assert(Props.MaxRolesAllowed(), check.Equals, 5) 140 | c.Assert(Props.ControllerDesiredFrequency(), check.Equals, 30) 141 | c.Assert(Props.IamRolePattern(), check.Equals, "pfx-{{ .ObjectMeta.Name }}") 142 | c.Assert(Props.IsIRSARegionalEndpointDisabled(), check.Equals, true) 143 | } 144 | 145 | func (s *PropertiesSuite) TestGetAllowedPolicyAction(c *check.C) { 146 | value := Props.AllowedPolicyAction() 147 | c.Assert(value, check.NotNil) 148 | } 149 | 150 | func (s *PropertiesSuite) TestGetRestrictedPolicyResources(c *check.C) { 151 | value := Props.RestrictedPolicyResources() 152 | c.Assert(value, check.NotNil) 153 | } 154 | 155 | func (s *PropertiesSuite) TestGetRestrictedS3Resources(c *check.C) { 156 | value := Props.RestrictedS3Resources() 157 | c.Assert(value, check.NotNil) 158 | } 159 | 160 | func (s *PropertiesSuite) TestGetManagedPolicies(c *check.C) { 161 | value := Props.ManagedPolicies() 162 | c.Assert(value, check.NotNil) 163 | } 164 | 165 | func (s *PropertiesSuite) TestGetAWSAccountID(c *check.C) { 166 | value := Props.AWSAccountID() 167 | c.Assert(value, check.NotNil) 168 | } 169 | 170 | func (s *PropertiesSuite) TestGetAWSRegion(c *check.C) { 171 | value := Props.AWSRegion() 172 | c.Assert(value, check.NotNil) 173 | } 174 | 175 | func (s *PropertiesSuite) TestGetManagedPermissionBoundaryPolicy(c *check.C) { 176 | value := Props.ManagedPermissionBoundaryPolicy() 177 | c.Assert(value, check.NotNil) 178 | } 179 | 180 | func (s *PropertiesSuite) TestIsWebhookEnabled(c *check.C) { 181 | value := Props.IsWebHookEnabled() 182 | c.Assert(value, check.Equals, false) 183 | } 184 | 185 | func (s *PropertiesSuite) TestControllerDesiredFrequency(c *check.C) { 186 | value := Props.ControllerDesiredFrequency() 187 | c.Assert(value, check.Equals, 0) 188 | } 189 | 190 | func (s *PropertiesSuite) TestIsIRSAEnabled(c *check.C) { 191 | value := Props.IsIRSAEnabled() 192 | c.Assert(value, check.Equals, false) 193 | } 194 | 195 | func (s *PropertiesSuite) TestControllerClusterName(c *check.C) { 196 | value := Props.ClusterName() 197 | c.Assert(value, check.Equals, "k8s_test_keiko") 198 | } 199 | 200 | func (s *PropertiesSuite) TestControllerOIDCIssuerUrl(c *check.C) { 201 | value := Props.OIDCIssuerUrl() 202 | c.Assert(value, check.Equals, "https://google.com/OIDC") 203 | } 204 | 205 | func (s *PropertiesSuite) TestControllerDefaultTrustPolicy(c *check.C) { 206 | def := `{"Version": "2012-10-17", "Statement": [{"Effect": "Allow","Principal": {"Federated": "arn:aws:iam::AWS_ACCOUNT_ID:oidc-provider/OIDC_PROVIDER"},"Action": "sts:AssumeRoleWithWebIdentity","Condition": {"StringEquals": {"OIDC_PROVIDER:sub": "system:serviceaccount:{{.NamespaceName}}:SERVICE_ACCOUNT_NAME"}}}, {"Effect": "Allow","Principal": {"AWS": ["arn:aws:iam::{{.AccountID}}:role/trust_role"]},"Action": "sts:AssumeRole"}]}` 207 | value := Props.DefaultTrustPolicy() 208 | c.Assert(value, check.Equals, def) 209 | } 210 | 211 | func (s *PropertiesSuite) TestIsIRSARegionalEndpointDisabled(c *check.C) { 212 | value := Props.IsIRSARegionalEndpointDisabled() 213 | c.Assert(value, check.Equals, false) 214 | } 215 | -------------------------------------------------------------------------------- /internal/controllers/iamrole_controller_test.go: -------------------------------------------------------------------------------- 1 | package controllers_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo/v2" 5 | . "github.com/onsi/gomega" 6 | "sigs.k8s.io/controller-runtime/pkg/event" 7 | 8 | iammanagerv1alpha1 "github.com/keikoproj/iam-manager/api/v1alpha1" 9 | . "github.com/keikoproj/iam-manager/internal/controllers" 10 | ) 11 | 12 | var _ = Describe("IamroleController", func() { 13 | Describe("When checking a StatusUpdatePredicate", func() { 14 | instance := StatusUpdatePredicate{} 15 | 16 | Context("Where status update request made", func() { 17 | It("Should return false", func() { 18 | new := &iammanagerv1alpha1.Iamrole{ 19 | Status: iammanagerv1alpha1.IamroleStatus{ 20 | RoleName: "role1", 21 | RetryCount: 2, 22 | State: iammanagerv1alpha1.Error, 23 | }, 24 | } 25 | 26 | old := &iammanagerv1alpha1.Iamrole{ 27 | Status: iammanagerv1alpha1.IamroleStatus{ 28 | RoleName: "role1", 29 | RetryCount: 1, 30 | State: iammanagerv1alpha1.Error, 31 | }, 32 | } 33 | // 34 | failEvt1 := event.UpdateEvent{ObjectOld: old, ObjectNew: new} 35 | failEvt3 := event.UpdateEvent{ObjectOld: nil, ObjectNew: new} 36 | failEvt5 := event.UpdateEvent{ObjectOld: old, ObjectNew: nil} 37 | 38 | Expect(instance.Update(failEvt1)).To(BeFalse()) 39 | Expect(instance.Update(failEvt3)).To(BeFalse()) 40 | Expect(instance.Update(failEvt5)).To(BeFalse()) 41 | 42 | }) 43 | }) 44 | 45 | Context("Where status create request made", func() { 46 | It("Should return true", func() { 47 | 48 | Expect(instance.Create(event.CreateEvent{})).To(BeTrue()) 49 | }) 50 | }) 51 | 52 | Context("Where status delete request made", func() { 53 | It("Should return true", func() { 54 | 55 | Expect(instance.Delete(event.DeleteEvent{})).To(BeTrue()) 56 | }) 57 | }) 58 | 59 | }) 60 | }) 61 | -------------------------------------------------------------------------------- /internal/controllers/iamrole_reconcile_manager.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | api "github.com/keikoproj/iam-manager/api/v1alpha1" 8 | "github.com/keikoproj/iam-manager/internal/config" 9 | "github.com/keikoproj/iam-manager/pkg/logging" 10 | "k8s.io/apimachinery/pkg/types" 11 | ctrl "sigs.k8s.io/controller-runtime" 12 | ) 13 | 14 | /** 15 | * This will start a go routine that will run every "ControllerDesiredFrequency" seconds. 16 | */ 17 | func (r *IamroleReconciler) StartControllerReconcileCronJob(ctx context.Context) error { 18 | log := logging.Logger(ctx, "controllers", "iamrole_controller", "StartControllerReconcileCronJob") 19 | 20 | log.Info("Starting the cronjob") 21 | 22 | // If the controllerDesiredFrequency is less than the minimum, use the minimum. 23 | // This is to prevent the controller from running too frequently. 24 | // DesiredFrequency can be configured in iks-config 25 | controllerDesiredFrequency := config.ControllerMinimumDesiredFrequency 26 | if controllerDesiredFrequency < config.Props.ControllerDesiredFrequency() { 27 | controllerDesiredFrequency = config.Props.ControllerDesiredFrequency() 28 | } 29 | 30 | log.Info("StartControllerReconcileCronJob", "controllerDesiredFrequency", controllerDesiredFrequency) 31 | 32 | ticker := time.NewTicker(time.Duration(controllerDesiredFrequency) * time.Second) 33 | 34 | for { 35 | select { 36 | case <-ticker.C: 37 | start := time.Now() 38 | log.Info("StartControllerReconcileCronJob - start to fetch IAM roles", "time", time.Now()) 39 | 40 | r.ReconcileAllReadyStateIamRoles(ctx) 41 | 42 | log.Info("Reconciled all iam-roles", "time", time.Now(), "duration", time.Since(start)) 43 | case <-ctx.Done(): 44 | log.Info("Application graceful shutdown", "time", time.Now()) 45 | return nil 46 | } 47 | } 48 | } 49 | 50 | func (r *IamroleReconciler) ReconcileAllReadyStateIamRoles(ctx context.Context) { 51 | log := logging.Logger(ctx, "controllers", "iamrole_controller", "Worker") 52 | 53 | var err error 54 | var res ctrl.Result 55 | var iamRoles []*api.Iamrole 56 | var iamrole *api.Iamrole 57 | 58 | if iamRoles, err = api.ListIamRoles(context.Background(), r.Client); err != nil { 59 | log.Error(err, "StartControllerReconcileCronJob", "unable to list iamroles CR") 60 | return 61 | } 62 | 63 | for _, prefetchedIamRole := range iamRoles { 64 | log.Info("Reconcile start", "iamRole", prefetchedIamRole.Name) 65 | if iamrole, err = api.GetIamRole(context.Background(), r.Client, prefetchedIamRole.Name, prefetchedIamRole.Namespace); err != nil { 66 | log.Error(err, "unable to get iamrole resource", "iamRole", prefetchedIamRole.Name, "namespace", prefetchedIamRole.Namespace) 67 | continue 68 | } 69 | 70 | if iamrole.Status.State != "Ready" { 71 | log.Info("Reconcile skipped because its state is not ready", "iamRole", iamrole.Name, "state", iamrole.Status.State) 72 | continue 73 | } 74 | 75 | req := ctrl.Request{NamespacedName: types.NamespacedName{Namespace: iamrole.Namespace, Name: iamrole.Name}} 76 | res, err = r.HandleReconcile(ctx, req, iamrole) 77 | log.Info("Reconcile result", "result", res, "error", err) 78 | 79 | // sleep for 2 seconds for politeness 80 | time.Sleep(2 * time.Second) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /internal/controllers/metrics_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | 16 | package controllers_test 17 | 18 | import ( 19 | "context" 20 | "fmt" 21 | "time" 22 | 23 | . "github.com/onsi/ginkgo/v2" 24 | . "github.com/onsi/gomega" 25 | 26 | "k8s.io/apimachinery/pkg/runtime" 27 | ctrl "sigs.k8s.io/controller-runtime" 28 | "sigs.k8s.io/controller-runtime/pkg/client" 29 | "sigs.k8s.io/controller-runtime/pkg/metrics/filters" 30 | metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" 31 | ) 32 | 33 | var _ = Describe("Metrics Server Configuration", func() { 34 | var stopFunc context.CancelFunc 35 | var ctx context.Context 36 | var metricsOpts metricsserver.Options 37 | var testManager ctrl.Manager 38 | 39 | BeforeEach(func() { 40 | // Create context with cancel function 41 | ctx, stopFunc = context.WithCancel(context.Background()) 42 | 43 | // Random metrics port to avoid conflicts 44 | metricsPort := fmt.Sprintf(":%d", 9090+GinkgoParallelProcess()) 45 | 46 | // Create metrics options with secure serving and authentication 47 | metricsOpts = metricsserver.Options{ 48 | BindAddress: metricsPort, 49 | SecureServing: true, 50 | FilterProvider: filters.WithAuthenticationAndAuthorization, 51 | } 52 | 53 | // Set up manager with our metrics options 54 | var err error 55 | testScheme := runtime.NewScheme() 56 | testManager, err = ctrl.NewManager(cfg, ctrl.Options{ 57 | Scheme: testScheme, 58 | Metrics: metricsOpts, 59 | Client: client.Options{ 60 | Cache: &client.CacheOptions{}, 61 | }, 62 | }) 63 | Expect(err).NotTo(HaveOccurred()) 64 | 65 | // Start the manager in background 66 | go func() { 67 | // We expect this to eventually fail due to missing resources in the test environment, 68 | // but we just want to verify that the manager can be created with secure metrics 69 | _ = testManager.Start(ctx) 70 | }() 71 | 72 | // Allow some time for the manager to start 73 | time.Sleep(100 * time.Millisecond) 74 | }) 75 | 76 | AfterEach(func() { 77 | // Clean up by stopping the manager 78 | stopFunc() 79 | time.Sleep(100 * time.Millisecond) 80 | }) 81 | 82 | It("should configure metrics server with secure serving and authentication", func() { 83 | // Verify metrics configuration 84 | Expect(metricsOpts.SecureServing).To(BeTrue(), "Metrics server should be configured with secure serving") 85 | Expect(metricsOpts.BindAddress).NotTo(BeEmpty(), "Metrics server should have a bind address") 86 | Expect(metricsOpts.FilterProvider).NotTo(BeNil(), "Metrics server should have authentication filter") 87 | 88 | // Verify the manager was created successfully with the metrics configuration 89 | Expect(testManager).NotTo(BeNil(), "Manager should be created with secure metrics configuration") 90 | }) 91 | }) 92 | -------------------------------------------------------------------------------- /internal/controllers/suite_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | 16 | package controllers_test 17 | 18 | import ( 19 | "fmt" 20 | "path/filepath" 21 | "runtime" 22 | "testing" 23 | 24 | . "github.com/onsi/ginkgo/v2" 25 | . "github.com/onsi/gomega" 26 | 27 | iammanagerv1alpha1 "github.com/keikoproj/iam-manager/api/v1alpha1" 28 | "k8s.io/client-go/kubernetes/scheme" 29 | "k8s.io/client-go/rest" 30 | "sigs.k8s.io/controller-runtime/pkg/client" 31 | "sigs.k8s.io/controller-runtime/pkg/envtest" 32 | logf "sigs.k8s.io/controller-runtime/pkg/log" 33 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 34 | // +kubebuilder:scaffold:imports 35 | ) 36 | 37 | // These tests use Ginkgo (BDD-style Go testing framework). Refer to 38 | // http://onsi.github.io/ginkgo/ to learn more about Ginkgo. 39 | 40 | var cfg *rest.Config 41 | var k8sClient client.Client 42 | var testEnv *envtest.Environment 43 | 44 | func TestAPIs(t *testing.T) { 45 | RegisterFailHandler(Fail) 46 | 47 | RunSpecsWithDefaultAndCustomReporters(t, 48 | "Controller Suite", 49 | []Reporter{}) 50 | } 51 | 52 | var _ = BeforeSuite(func() { 53 | logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter))) 54 | 55 | By("bootstrapping test environment") 56 | testEnv = &envtest.Environment{ 57 | CRDDirectoryPaths: []string{filepath.Join("../../", "config", "crd", "bases")}, 58 | ErrorIfCRDPathMissing: true, 59 | 60 | // The BinaryAssetsDirectory is only required if you want to run the tests directly 61 | // without call the makefile target test. If not informed it will look for the 62 | // default path defined in controller-runtime which is /usr/local/kubebuilder/. 63 | // Note that you must have the required binaries setup under the bin directory to perform 64 | // the tests directly. When we run make test it will be setup and used automatically. 65 | BinaryAssetsDirectory: filepath.Join("../../", "bin", "k8s", 66 | fmt.Sprintf("1.28.0-%s-%s", runtime.GOOS, runtime.GOARCH)), 67 | } 68 | 69 | var err error 70 | cfg, err = testEnv.Start() 71 | Expect(err).ToNot(HaveOccurred()) 72 | Expect(cfg).ToNot(BeNil()) 73 | 74 | err = iammanagerv1alpha1.AddToScheme(scheme.Scheme) 75 | Expect(err).NotTo(HaveOccurred()) 76 | 77 | // +kubebuilder:scaffold:scheme 78 | 79 | k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) 80 | Expect(err).ToNot(HaveOccurred()) 81 | Expect(k8sClient).ToNot(BeNil()) 82 | 83 | }) 84 | 85 | var _ = AfterSuite(func() { 86 | By("tearing down the test environment") 87 | err := testEnv.Stop() 88 | Expect(err).ToNot(HaveOccurred()) 89 | }) 90 | -------------------------------------------------------------------------------- /internal/utils/oidc.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "context" 5 | "crypto/sha1" 6 | "crypto/tls" 7 | "crypto/x509" 8 | "errors" 9 | "fmt" 10 | "net/url" 11 | "strings" 12 | 13 | "github.com/keikoproj/iam-manager/api/v1alpha1" 14 | "github.com/keikoproj/iam-manager/internal/config" 15 | "github.com/keikoproj/iam-manager/pkg/logging" 16 | ) 17 | 18 | // GetIdpServerCertThumbprint gets the Thumbbprint of the certificate which will be used to generate OIDC tokens 19 | // This was taken from AWS repo https://github.com/aws/containers-roadmap/issues/23#issuecomment-530887531 comment 20 | // https://play.golang.org/p/iSobu11ahUi 21 | func GetIdpServerCertThumbprint(ctx context.Context, url string) (string, error) { 22 | log := logging.Logger(ctx, "internal.utils.oidc", "GetIdpServerCertThumbprint") 23 | log.Info("Calculating Idp Server cert Thumbprint") 24 | 25 | thumbprint := "" 26 | hostName, err := parseURL(ctx, url) 27 | if err != nil { 28 | log.Error(err, "Unable to get the host") 29 | return thumbprint, err 30 | } 31 | conn, err := tls.Dial("tcp", hostName, &tls.Config{ 32 | InsecureSkipVerify: true, 33 | }) 34 | if err != nil { 35 | log.Error(err, "Unable to dial remote host") 36 | return thumbprint, err 37 | } 38 | //Close the connection 39 | defer conn.Close() 40 | 41 | cs := conn.ConnectionState() 42 | numCerts := len(cs.PeerCertificates) 43 | var root *x509.Certificate 44 | // Important! Get the last cert in the chain, which is the root CA. 45 | if numCerts >= 1 { 46 | root = cs.PeerCertificates[numCerts-1] 47 | } else { 48 | log.Error(err, "Error getting cert list from connection for Idp Cert Thumbprint calculation") 49 | return thumbprint, err 50 | } 51 | thumbprint = fmt.Sprintf("%x", sha1.Sum(root.Raw)) 52 | // print out the fingerprint 53 | log.Info("Successfully able to retrieve Idp Server cert thumbprint", "thumbprint", thumbprint) 54 | return thumbprint, nil 55 | } 56 | 57 | // parseURL verifies the url and returns hostname and port 58 | func parseURL(ctx context.Context, idpUrl string) (string, error) { 59 | log := logging.Logger(ctx, "internal.utils.oidc", "parseURL") 60 | resp, err := url.Parse(idpUrl) 61 | if err != nil { 62 | log.Error(err, "unable to parse the idp url") 63 | return "", err 64 | } 65 | 66 | if resp.Scheme != "https" { 67 | log.Error(errors.New("OIDC IDP url must start with https"), "OIDC IDP url must start with https", "obtained", resp.Scheme) 68 | return "", err 69 | } 70 | 71 | port := resp.Port() 72 | 73 | if resp.Port() == "" { 74 | port = "443" 75 | } 76 | hostName := fmt.Sprintf("%s:%s", resp.Host, port) 77 | log.Info("url parsed successfully", "hostName", hostName) 78 | return hostName, nil 79 | } 80 | 81 | // ParseIRSAAnnotation parses IAM role to see if the role to be used in IRSA method 82 | func ParseIRSAAnnotation(ctx context.Context, iamRole *v1alpha1.Iamrole) (bool, []string) { 83 | var exists, annoStr = parseAnnotations(ctx, config.IRSAAnnotation, iamRole.Annotations) 84 | if exists { 85 | array := strings.Split(annoStr, ",") 86 | for i := 0; i < len(array); i++ { 87 | array[i] = strings.Trim(array[i], " ") 88 | } 89 | return exists, array 90 | } 91 | return false, []string{} 92 | } 93 | -------------------------------------------------------------------------------- /internal/utils/oidc_test.go: -------------------------------------------------------------------------------- 1 | package utils_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/golang/mock/gomock" 8 | "gopkg.in/check.v1" 9 | "k8s.io/apimachinery/pkg/apis/meta/v1" 10 | 11 | "github.com/keikoproj/iam-manager/api/v1alpha1" 12 | "github.com/keikoproj/iam-manager/internal/config" 13 | "github.com/keikoproj/iam-manager/internal/utils" 14 | ) 15 | 16 | type OIDCTestSuite struct { 17 | t *testing.T 18 | ctx context.Context 19 | mockCtrl *gomock.Controller 20 | } 21 | 22 | func TestOIDCTestSuite(t *testing.T) { 23 | check.Suite(&OIDCTestSuite{t: t}) 24 | check.TestingT(t) 25 | } 26 | 27 | func (s *OIDCTestSuite) SetUpTest(c *check.C) { 28 | s.ctx = context.Background() 29 | s.mockCtrl = gomock.NewController(s.t) 30 | } 31 | 32 | func (s *OIDCTestSuite) TearDownTest(c *check.C) { 33 | s.mockCtrl.Finish() 34 | } 35 | 36 | func (s *OIDCTestSuite) TestParseIRSAAnnotationEmpty(c *check.C) { 37 | input := &v1alpha1.Iamrole{ 38 | ObjectMeta: v1.ObjectMeta{ 39 | Name: "iam-role", 40 | }, 41 | } 42 | flag, saName := utils.ParseIRSAAnnotation(s.ctx, input) 43 | c.Assert(flag, check.Equals, false) 44 | c.Assert(saName, check.HasLen, 0) 45 | } 46 | 47 | func (s *OIDCTestSuite) TestParseIRSAAnnotationValid(c *check.C) { 48 | input := &v1alpha1.Iamrole{ 49 | ObjectMeta: v1.ObjectMeta{ 50 | Name: "iam-role", 51 | Namespace: "k8s-namespace-dev", 52 | Annotations: map[string]string{ 53 | config.IRSAAnnotation: "default", 54 | }, 55 | }, 56 | } 57 | flag, saNames := utils.ParseIRSAAnnotation(s.ctx, input) 58 | c.Assert(flag, check.Equals, true) 59 | c.Assert(saNames[0], check.Equals, "default") 60 | } 61 | 62 | func (s *OIDCTestSuite) TestParseIRSAAnnotationValidArray(c *check.C) { 63 | input := &v1alpha1.Iamrole{ 64 | ObjectMeta: v1.ObjectMeta{ 65 | Name: "iam-role", 66 | Namespace: "k8s-namespace-dev", 67 | Annotations: map[string]string{ 68 | config.IRSAAnnotation: "default, my-sa", 69 | }, 70 | }, 71 | } 72 | flag, saNames := utils.ParseIRSAAnnotation(s.ctx, input) 73 | c.Assert(flag, check.Equals, true) 74 | c.Assert(saNames[0], check.Equals, "default") 75 | c.Assert(saNames[1], check.Equals, "my-sa") 76 | 77 | } 78 | 79 | func (s *OIDCTestSuite) TestParseIRSAAnnotationOtherAnnotations(c *check.C) { 80 | input := &v1alpha1.Iamrole{ 81 | ObjectMeta: v1.ObjectMeta{ 82 | Name: "iam-role", 83 | Annotations: map[string]string{ 84 | "iam.amazonaws.com/role": "default", 85 | }, 86 | }, 87 | } 88 | flag, saNames := utils.ParseIRSAAnnotation(s.ctx, input) 89 | c.Assert(flag, check.Equals, false) 90 | c.Assert(len(saNames), check.Equals, 0) 91 | } 92 | 93 | func (s *OIDCTestSuite) TestGetIdpServerCertThumbprintSuccess(c *check.C) { 94 | _, err := utils.GetIdpServerCertThumbprint(s.ctx, "https://www.google.com") 95 | c.Assert(err, check.IsNil) 96 | } 97 | 98 | func (s *OIDCTestSuite) TestGetIdpServerCertThumbprintNotURL(c *check.C) { 99 | _, err := utils.GetIdpServerCertThumbprint(s.ctx, "#8123not_even_a_url") 100 | c.Assert(err, check.NotNil) 101 | } 102 | 103 | func (s *OIDCTestSuite) TestGetIdpServerCertThumbprintNotHttps(c *check.C) { 104 | _, err := utils.GetIdpServerCertThumbprint(s.ctx, "http://www.google.com") 105 | c.Assert(err, check.NotNil) 106 | } 107 | -------------------------------------------------------------------------------- /pkg/awsapi/eks.go: -------------------------------------------------------------------------------- 1 | package awsapi 2 | 3 | //go:generate mockgen -destination=mocks/mock_eksiface.go -package=mock_awsapi github.com/aws/aws-sdk-go/service/eks/eksiface EKSAPI 4 | 5 | import ( 6 | "context" 7 | 8 | "github.com/aws/aws-sdk-go/aws" 9 | "github.com/aws/aws-sdk-go/aws/awserr" 10 | "github.com/aws/aws-sdk-go/aws/session" 11 | "github.com/aws/aws-sdk-go/service/eks" 12 | "github.com/aws/aws-sdk-go/service/eks/eksiface" 13 | 14 | "github.com/keikoproj/iam-manager/pkg/logging" 15 | ) 16 | 17 | type EKSIface interface { 18 | DescribeCluster(ctx context.Context, clusterName string) 19 | } 20 | 21 | type EKS struct { 22 | Client eksiface.EKSAPI 23 | } 24 | 25 | func NewEKS(region string) *EKS { 26 | sess, err := session.NewSession(&aws.Config{Region: aws.String(region)}) 27 | if err != nil { 28 | panic(err) 29 | } 30 | return &EKS{ 31 | Client: eks.New(sess), 32 | } 33 | } 34 | 35 | // DescribeCluster function provides cluster info 36 | func (e *EKS) DescribeCluster(ctx context.Context, clusterName string) (*eks.DescribeClusterOutput, error) { 37 | log := logging.Logger(ctx, "awsapi", "eks", "DescribeCluster") 38 | log.WithValues("clusterName", clusterName) 39 | log.V(1).Info("Initiating api call") 40 | 41 | input := &eks.DescribeClusterInput{ 42 | Name: aws.String(clusterName), 43 | } 44 | resp, err := e.Client.DescribeCluster(input) 45 | 46 | if err != nil { 47 | if aerr, ok := err.(awserr.Error); ok { 48 | switch aerr.Code() { 49 | case eks.ErrCodeResourceNotFoundException: 50 | log.Error(err, eks.ErrCodeResourceNotFoundException) 51 | case eks.ErrCodeClientException: 52 | log.Error(err, eks.ErrCodeClientException) 53 | case eks.ErrCodeServerException: 54 | log.Error(err, eks.ErrCodeServerException) 55 | case eks.ErrCodeServiceUnavailableException: 56 | log.Error(err, eks.ErrCodeServiceUnavailableException) 57 | default: 58 | log.Error(err, aerr.Error()) 59 | } 60 | } else { 61 | // Print the error, cast err to awserr.Error to get the Code and 62 | // Message from an error. 63 | log.Error(err, err.Error()) 64 | } 65 | return nil, err 66 | } 67 | 68 | log.Info("Successfully retrieved cluster info") 69 | return resp, nil 70 | } 71 | -------------------------------------------------------------------------------- /pkg/awsapi/eks_test.go: -------------------------------------------------------------------------------- 1 | package awsapi_test 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "testing" 7 | 8 | "github.com/aws/aws-sdk-go/aws" 9 | "github.com/aws/aws-sdk-go/aws/awserr" 10 | "github.com/aws/aws-sdk-go/service/eks" 11 | "github.com/golang/mock/gomock" 12 | "gopkg.in/check.v1" 13 | 14 | "github.com/keikoproj/iam-manager/internal/config" 15 | "github.com/keikoproj/iam-manager/pkg/awsapi" 16 | "github.com/keikoproj/iam-manager/pkg/awsapi/mocks" 17 | ) 18 | 19 | type EKSAPISuite struct { 20 | t *testing.T 21 | ctx context.Context 22 | mockCtrl *gomock.Controller 23 | mockE *mock_awsapi.MockEKSAPI 24 | mockEKS awsapi.EKS 25 | } 26 | 27 | func TestEKSAPITestSuite(t *testing.T) { 28 | check.Suite(&EKSAPISuite{t: t}) 29 | check.TestingT(t) 30 | } 31 | 32 | func (s *EKSAPISuite) SetUpTest(c *check.C) { 33 | s.ctx = context.Background() 34 | s.mockCtrl = gomock.NewController(s.t) 35 | s.mockE = mock_awsapi.NewMockEKSAPI(s.mockCtrl) 36 | s.mockEKS = awsapi.EKS{ 37 | Client: s.mockE, 38 | } 39 | 40 | _ = config.LoadProperties("LOCAL") 41 | } 42 | 43 | func (s *EKSAPISuite) TearDownTest(c *check.C) { 44 | s.mockCtrl.Finish() 45 | } 46 | 47 | func (s *EKSAPISuite) TestDescribeClusterSuccess(c *check.C) { 48 | s.mockE.EXPECT().DescribeCluster(&eks.DescribeClusterInput{Name: aws.String("valid_cluster")}).Times(1).Return(&eks.DescribeClusterOutput{ 49 | Cluster: &eks.Cluster{ 50 | Name: aws.String("valid_cluster"), 51 | }, 52 | }, nil) 53 | _, err := s.mockEKS.DescribeCluster(s.ctx, "valid_cluster") 54 | c.Assert(err, check.IsNil) 55 | } 56 | 57 | func (s *EKSAPISuite) TestDescribeClusterNotFound(c *check.C) { 58 | s.mockE.EXPECT().DescribeCluster(&eks.DescribeClusterInput{Name: aws.String("not_found_cluster")}).Times(1).Return(nil, awserr.New(eks.ErrCodeResourceNotFoundException, "", errors.New(eks.ErrCodeResourceNotFoundException))) 59 | _, err := s.mockEKS.DescribeCluster(s.ctx, "not_found_cluster") 60 | c.Assert(err, check.NotNil) 61 | } 62 | 63 | func (s *EKSAPISuite) TestDescribeClusterClientException(c *check.C) { 64 | s.mockE.EXPECT().DescribeCluster(&eks.DescribeClusterInput{Name: aws.String("wrong_cluster")}).Times(1).Return(nil, awserr.New(eks.ErrCodeClientException, "", errors.New(eks.ErrCodeClientException))) 65 | _, err := s.mockEKS.DescribeCluster(s.ctx, "wrong_cluster") 66 | c.Assert(err, check.NotNil) 67 | } 68 | 69 | func (s *EKSAPISuite) TestDescribeClusterServerException(c *check.C) { 70 | s.mockE.EXPECT().DescribeCluster(&eks.DescribeClusterInput{Name: aws.String("wrong_server_cluster")}).Times(1).Return(nil, awserr.New(eks.ErrCodeServerException, "", errors.New(eks.ErrCodeServerException))) 71 | _, err := s.mockEKS.DescribeCluster(s.ctx, "wrong_server_cluster") 72 | c.Assert(err, check.NotNil) 73 | } 74 | 75 | func (s *EKSAPISuite) TestDescribeClusterServiceUnavailableException(c *check.C) { 76 | s.mockE.EXPECT().DescribeCluster(&eks.DescribeClusterInput{Name: aws.String("service_unavailable")}).Times(1).Return(nil, awserr.New(eks.ErrCodeServiceUnavailableException, "", errors.New(eks.ErrCodeServiceUnavailableException))) 77 | _, err := s.mockEKS.DescribeCluster(s.ctx, "service_unavailable") 78 | c.Assert(err, check.NotNil) 79 | } 80 | -------------------------------------------------------------------------------- /pkg/awsapi/sts.go: -------------------------------------------------------------------------------- 1 | package awsapi 2 | 3 | //go:generate mockgen -destination=mocks/mock_stsiface.go -package=mock_awsapi github.com/aws/aws-sdk-go/service/sts/stsiface STSAPI 4 | ////go:generate mockgen -destination=mocks/mock_sts.go -package=mock_awsapi github.com/keikoproj/iam-manager/pkg/awsapi STSIface 5 | 6 | import ( 7 | "context" 8 | 9 | "github.com/aws/aws-sdk-go/aws" 10 | "github.com/aws/aws-sdk-go/aws/session" 11 | "github.com/aws/aws-sdk-go/service/sts" 12 | "github.com/aws/aws-sdk-go/service/sts/stsiface" 13 | 14 | "github.com/keikoproj/iam-manager/pkg/logging" 15 | ) 16 | 17 | type STSIface interface { 18 | GetAccountID(ctx context.Context) (string, error) 19 | } 20 | 21 | type STS struct { 22 | Client stsiface.STSAPI 23 | } 24 | 25 | func NewSTS(region string) *STS { 26 | sess, err := session.NewSession(&aws.Config{Region: aws.String(region)}) 27 | if err != nil { 28 | panic(err) 29 | } 30 | return &STS{ 31 | Client: sts.New(sess), 32 | } 33 | } 34 | 35 | // GetAccountID loads aws accountID from sts caller identity 36 | func (i *STS) GetAccountID(ctx context.Context) (string, error) { 37 | log := logging.Logger(context.Background(), "awsapi", "iam", "GetAccountID") 38 | 39 | // get caller identity in order to fetch aws account ID 40 | result, err := i.Client.GetCallerIdentity(&sts.GetCallerIdentityInput{}) 41 | if err != nil { 42 | log.Error(err, "failed to get caller's identity") 43 | return "", err 44 | } 45 | return *result.Account, nil 46 | } 47 | -------------------------------------------------------------------------------- /pkg/awsapi/sts_test.go: -------------------------------------------------------------------------------- 1 | package awsapi_test 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "testing" 7 | 8 | "github.com/aws/aws-sdk-go/service/iam" 9 | "github.com/aws/aws-sdk-go/service/sts" 10 | "github.com/golang/mock/gomock" 11 | "gopkg.in/check.v1" 12 | 13 | "github.com/keikoproj/iam-manager/internal/config" 14 | "github.com/keikoproj/iam-manager/pkg/awsapi" 15 | mock_awsapi "github.com/keikoproj/iam-manager/pkg/awsapi/mocks" 16 | ) 17 | 18 | type STSAPISuite struct { 19 | t *testing.T 20 | ctx context.Context 21 | mockCtrl *gomock.Controller 22 | mockI *mock_awsapi.MockSTSAPI 23 | mockSTS awsapi.STS 24 | } 25 | 26 | func TestSTSAPITestSuite(t *testing.T) { 27 | check.Suite(&IAMAPISuite{t: t}) 28 | check.TestingT(t) 29 | } 30 | 31 | func (s *STSAPISuite) SetUpTest(c *check.C) { 32 | s.ctx = context.Background() 33 | s.mockCtrl = gomock.NewController(s.t) 34 | s.mockI = mock_awsapi.NewMockSTSAPI(s.mockCtrl) 35 | s.mockSTS = awsapi.STS{ 36 | Client: s.mockI, 37 | } 38 | 39 | _ = config.LoadProperties("LOCAL") 40 | } 41 | 42 | func (s *STSAPISuite) TearDownTest(c *check.C) { 43 | s.mockCtrl.Finish() 44 | } 45 | 46 | func (s *STSAPISuite) TestGetAccountIDSuccess(c *check.C) { 47 | s.mockI.EXPECT().GetCallerIdentity(&sts.GetCallerIdentityInput{}).Times(1).Return(&sts.GetCallerIdentityOutput{}, nil) 48 | accountID, err := s.mockSTS.GetAccountID(s.ctx) 49 | c.Assert(err, check.IsNil) 50 | c.Assert(accountID, check.NotNil) 51 | } 52 | 53 | func (s *STSAPISuite) TestGetAccountIDFailed(c *check.C) { 54 | s.mockI.EXPECT().GetCallerIdentity(&sts.GetCallerIdentityInput{}).Times(1).Return(&sts.GetCallerIdentityOutput{}, errors.New(iam.ErrCodeNoSuchEntityException)) 55 | accountID, err := s.mockSTS.GetAccountID(s.ctx) 56 | c.Assert(err, check.NotNil) 57 | c.Assert(accountID, check.Equals, "") 58 | } 59 | -------------------------------------------------------------------------------- /pkg/k8s/client.go: -------------------------------------------------------------------------------- 1 | package k8s 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "time" 8 | 9 | v1 "k8s.io/api/core/v1" 10 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 11 | "k8s.io/apimachinery/pkg/runtime/schema" 12 | "k8s.io/client-go/dynamic" 13 | clientv1 "k8s.io/client-go/informers/core/v1" 14 | "k8s.io/client-go/kubernetes" 15 | "k8s.io/client-go/kubernetes/scheme" 16 | v1core "k8s.io/client-go/kubernetes/typed/core/v1" 17 | "k8s.io/client-go/rest" 18 | "k8s.io/client-go/tools/cache" 19 | "k8s.io/client-go/tools/clientcmd" 20 | "k8s.io/client-go/tools/record" 21 | "k8s.io/klog" 22 | "sigs.k8s.io/controller-runtime/pkg/client" 23 | 24 | "github.com/keikoproj/iam-manager/pkg/logging" 25 | ) 26 | 27 | type Client struct { 28 | cl kubernetes.Interface 29 | dCl dynamic.Interface 30 | rCl client.Client 31 | } 32 | 33 | // NewK8sClient gets the new k8s go client 34 | func NewK8sClient() (*Client, error) { 35 | config, err := rest.InClusterConfig() 36 | if err != nil { 37 | // Do i need to panic here? 38 | //How do i test this from local? 39 | //Lets get it from local config file 40 | config, err = clientcmd.BuildConfigFromFlags("", os.Getenv("KUBECONFIG")) 41 | } 42 | client, err := kubernetes.NewForConfig(config) 43 | if err != nil { 44 | return nil, err 45 | } 46 | 47 | dClient, err := dynamic.NewForConfig(config) 48 | if err != nil { 49 | return nil, err 50 | } 51 | 52 | cl := &Client{ 53 | cl: client, 54 | dCl: dClient, 55 | } 56 | return cl, nil 57 | } 58 | 59 | // NewK8sClient gets the new k8s go client 60 | func NewK8sClientDoOrDie() *Client { 61 | config, err := rest.InClusterConfig() 62 | if err != nil { 63 | // Do i need to panic here? 64 | //How do i test this from local? 65 | //Lets get it from local config file 66 | config, err = clientcmd.BuildConfigFromFlags("", os.Getenv("KUBECONFIG")) 67 | } 68 | client, err := kubernetes.NewForConfig(config) 69 | if err != nil { 70 | panic(err) 71 | } 72 | 73 | dClient, err := dynamic.NewForConfig(config) 74 | if err != nil { 75 | panic(err) 76 | } 77 | 78 | cl := &Client{ 79 | cl: client, 80 | dCl: dClient, 81 | } 82 | return cl 83 | } 84 | 85 | // NewK8sManagerClient func will be used in future and all others should migrate to this 86 | func NewK8sManagerClient(client client.Client) *Client { 87 | cl := &Client{ 88 | rCl: client, 89 | } 90 | return cl 91 | 92 | } 93 | 94 | // Iface defines required functions to be implemented by receivers 95 | type Iface interface { 96 | IamrolesCount(ctx context.Context, ns string) 97 | GetConfigMap(ctx context.Context, ns string, name string) *v1.ConfigMap 98 | SetUpEventHandler(ctx context.Context) record.EventRecorder 99 | GetNamespace(ctx context.Context, ns string) *v1.Namespace 100 | CreateOrUpdateServiceAccount(ctx context.Context, saName string, ns string) error 101 | } 102 | 103 | // IamrolesCount function lists the "Iamrole" for a provided namespace 104 | func (c *Client) IamrolesCount(ctx context.Context, ns string) (int, error) { 105 | log := logging.Logger(ctx, "k8s", "client", "IamrolesCount") 106 | log.WithValues("namespace", ns) 107 | log.V(1).Info("list api call") 108 | iamCR := schema.GroupVersionResource{ 109 | Group: "iammanager.keikoproj.io", 110 | Version: "v1alpha1", 111 | Resource: "iamroles", 112 | } 113 | 114 | roleList, err := c.dCl.Resource(iamCR).Namespace(ns).List(ctx, metav1.ListOptions{}) 115 | if err != nil { 116 | log.Error(err, "unable to list iamroles resources") 117 | return 0, err 118 | } 119 | log.Info("Total number of roles", "count", len(roleList.Items)) 120 | return len(roleList.Items), nil 121 | } 122 | 123 | func (c *Client) GetConfigMap(ctx context.Context, ns string, name string) *v1.ConfigMap { 124 | log := logging.Logger(ctx, "k8s", "client", "GetConfigMap") 125 | log.WithValues("namespace", ns) 126 | log.Info("Retrieving config map") 127 | res, err := c.cl.CoreV1().ConfigMaps(ns).Get(ctx, name, metav1.GetOptions{}) 128 | if err != nil { 129 | log.Error(err, "unable to get config map") 130 | panic(err) 131 | } 132 | 133 | return res 134 | } 135 | 136 | // GetNamespace gets the namespace metadata. This will be used to validate if the namespace is annotated for privileged namespace. 137 | func (c *Client) GetNamespace(ctx context.Context, ns string) (*v1.Namespace, error) { 138 | log := logging.Logger(ctx, "k8s", "client", "GetNamespace") 139 | log.WithValues("namespace", ns) 140 | log.Info("Retrieving Namespace") 141 | resp := &v1.Namespace{} 142 | resp, err := c.cl.CoreV1().Namespaces().Get(ctx, ns, metav1.GetOptions{}) 143 | if err != nil { 144 | log.Error(err, "unable to get the namespace details") 145 | return nil, err 146 | } 147 | 148 | if err != nil { 149 | log.Error(err, "unable to get namespace metadata") 150 | return nil, err 151 | } 152 | return resp, nil 153 | } 154 | 155 | func (c *Client) ClientInterface() kubernetes.Interface { 156 | return c.cl 157 | } 158 | 159 | // GetConfigMapInformer returns shared informer for given config map 160 | func GetConfigMapInformer(ctx context.Context, nsName string, cmName string) cache.SharedIndexInformer { 161 | log := logging.Logger(context.Background(), "pkg.k8s.client", "GetConfigMapInformer") 162 | clientset, err := NewK8sClient() 163 | if err != nil { 164 | log.Error(err, "failed to get clientset") 165 | return nil 166 | } 167 | 168 | listOptions := func(options *metav1.ListOptions) { 169 | options.FieldSelector = fmt.Sprintf("metadata.name=%s", cmName) 170 | } 171 | 172 | // default resync period 24 hours 173 | cmInformer := clientv1.NewFilteredConfigMapInformer(clientset.ClientInterface(), nsName, 24*time.Hour, cache.Indexers{}, listOptions) 174 | return cmInformer 175 | } 176 | 177 | // SetUpEventHandler sets up event handler with client-go recorder instead of creating events directly 178 | func (c *Client) SetUpEventHandler(ctx context.Context) record.EventRecorder { 179 | log := logging.Logger(ctx, "k8s", "client", "SetUpEventHandler") 180 | //This was re-written based on job-controller in kuberentest repo 181 | //For more info refer: https://github.com/kubernetes/kubernetes/blob/master/pkg/controller/job/job_controller.go 182 | eventBroadcaster := record.NewBroadcaster() 183 | eventBroadcaster.StartLogging(klog.Infof) 184 | eventBroadcaster.StartRecordingToSink(&v1core.EventSinkImpl{Interface: c.cl.CoreV1().Events("")}) 185 | log.V(1).Info("Successfully added event broadcaster") 186 | return eventBroadcaster.NewRecorder(scheme.Scheme, v1.EventSource{Component: "iam-manager"}) 187 | } 188 | 189 | // GetServiceAccount returns the service account with a given name in a given namespace 190 | func (c *Client) GetServiceAccount(ctx context.Context, ns string, name string) *v1.ServiceAccount { 191 | log := logging.Logger(ctx, "k8s", "client", "GetServiceAccount") 192 | log.WithValues("namespace", ns) 193 | log.Info("Retrieving service account") 194 | sa := &v1.ServiceAccount{} 195 | err := c.rCl.Get(ctx, client.ObjectKey{Name: name, Namespace: ns}, sa) 196 | if err != nil { 197 | log.Info("unable to get service account", "saName", name, "namespace", ns) 198 | return nil 199 | } 200 | 201 | return sa 202 | } 203 | -------------------------------------------------------------------------------- /pkg/k8s/rbac.go: -------------------------------------------------------------------------------- 1 | package k8s 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | corev1 "k8s.io/api/core/v1" 9 | apierr "k8s.io/apimachinery/pkg/api/errors" 10 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 11 | "k8s.io/apimachinery/pkg/types" 12 | "sigs.k8s.io/controller-runtime/pkg/client" 13 | 14 | "github.com/keikoproj/iam-manager/pkg/logging" 15 | ) 16 | 17 | // CreateServiceAccount adds the service account 18 | func (c *Client) CreateOrUpdateServiceAccount(ctx context.Context, saName string, ns string, roleARN string, regionalEndpointDisabled bool) error { 19 | log := logging.Logger(ctx, "pkg.k8s", "rbac", "CreateOrUpdateServiceAccount") 20 | 21 | sa := &corev1.ServiceAccount{ 22 | ObjectMeta: v1.ObjectMeta{ 23 | Name: saName, 24 | Namespace: ns, 25 | Annotations: map[string]string{ 26 | "eks.amazonaws.com/role-arn": roleARN, 27 | }, 28 | }, 29 | } 30 | if !regionalEndpointDisabled { 31 | sa.ObjectMeta.Annotations["eks.amazonaws.com/sts-regional-endpoints"] = "true" 32 | } 33 | 34 | //_, err := c.cl.CoreV1().ServiceAccounts(ns).Create(sa) 35 | log.V(1).Info("Service Account creation is in progress") 36 | err := c.rCl.Create(ctx, sa) 37 | if err != nil { 38 | if !apierr.IsAlreadyExists(err) { 39 | msg := fmt.Sprintf("Failed to create service account %s in namespace %s due to %v", sa.Name, ns, err) 40 | log.Error(err, msg) 41 | return errors.New(msg) 42 | } 43 | annotationByte, _ := json.Marshal(sa.Annotations) 44 | patchStr := fmt.Sprintf(`{"metadata":{"annotations":%s}}`, string(annotationByte)) 45 | log.Info("Service account already exists. Trying to update", "serviceAccount", 46 | sa.Name, "namespace", ns, "annotation", patchStr) 47 | // Use patch to avoid creating new token each reconcile 48 | err = c.rCl.Patch(ctx, sa, client.RawPatch(types.MergePatchType, []byte(patchStr))) 49 | if err != nil { 50 | msg := fmt.Sprintf("Failed to update service account %s due to %v", sa.Name, err) 51 | log.Error(err, msg) 52 | return errors.New(msg) 53 | } 54 | } 55 | log.Info("Service account got created successfully", "serviceAccount", sa.Name, "namespace", ns) 56 | return nil 57 | } 58 | -------------------------------------------------------------------------------- /pkg/logging/logging.go: -------------------------------------------------------------------------------- 1 | package logging 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/go-logr/logr" 7 | ctrl "sigs.k8s.io/controller-runtime" 8 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 9 | ) 10 | 11 | //Set up with Controller based on the level 12 | 13 | //Get the request id 14 | 15 | // New function sets the logging level based on the flag and also sets it with controller 16 | func New(debug ...bool) { 17 | enabled := false 18 | if len(debug) == 0 { 19 | enabled = true 20 | } else { 21 | enabled = debug[0] 22 | } 23 | ctrl.SetLogger(zap.New(func(o *zap.Options) { 24 | o.Development = enabled 25 | })) 26 | } 27 | 28 | // Logger with 29 | func Logger(ctx context.Context, names ...string) logr.Logger { 30 | logk := ctrl.Log 31 | for _, name := range names { 32 | logk.WithName(name) 33 | } 34 | rId := ctx.Value("request_id") 35 | if rId != nil { 36 | logk.WithValues("request_id", rId) 37 | } 38 | 39 | return logk 40 | } 41 | --------------------------------------------------------------------------------