├── .editorconfig ├── .github ├── dependabot.yml ├── release-drafter.yml └── workflows │ ├── draft-release.yml │ ├── lint-pr.yml │ ├── release.yml │ └── validate.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .vscode └── extensions.json ├── CONTRIBUTING.md ├── LICENSE.md ├── MAINTAINERS.md ├── README.md ├── bin └── generate-thumbprint.sh ├── examples ├── default │ ├── .terraform.lock.hcl │ ├── README.md │ ├── main.tf │ ├── outputs.tf │ ├── providers.tf │ └── variables.tf ├── repositories │ ├── repo-ecr │ │ └── .github │ │ │ └── workflows │ │ │ └── ecr.yml │ └── repo-s3 │ │ └── .github │ │ └── workflows │ │ └── s3.yml └── variables.tf ├── main.tf ├── modules └── provider │ ├── README.md │ ├── main.tf │ ├── outputs.tf │ └── variables.tf ├── outputs.tf ├── variables.tf └── versions.tf /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | end_of_line = lf 3 | indent_style = space 4 | indent_size = 2 5 | insert_final_newline = true 6 | 7 | [*.md] 8 | trim_trailing_whitespace = true 9 | -------------------------------------------------------------------------------- /.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://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "github-actions" 9 | # Workflow files stored in the 10 | # default location of `.github/workflows` 11 | directory: "/" 12 | schedule: 13 | interval: "weekly" 14 | 15 | - package-ecosystem: "terraform" 16 | directory: "/" 17 | schedule: 18 | interval: "weekly" 19 | 20 | - package-ecosystem: "terraform" 21 | directory: "/examples/default" 22 | schedule: 23 | interval: "weekly" 24 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name-template: "v$RESOLVED_VERSION" 2 | tag-template: "v$RESOLVED_VERSION" 3 | exclude-labels: 4 | - "skip-changelog" 5 | categories: 6 | - title: "💥 Breaking changes" 7 | labels: 8 | - "major" 9 | - title: "✨ Features" 10 | labels: 11 | - "minor" 12 | - title: "🐛 Bug Fixes" 13 | labels: 14 | - "patch" 15 | - title: "📚 Documentation" 16 | labels: 17 | - "docs" 18 | - title: "📦 Dependencies" 19 | labels: 20 | - "dependencies" 21 | 22 | change-template: "- $TITLE @$AUTHOR (#$NUMBER)" 23 | change-title-escapes: '\<*_&' 24 | sort-by: title 25 | sort-direction: ascending 26 | 27 | version-resolver: 28 | major: 29 | labels: 30 | - "major" 31 | minor: 32 | labels: 33 | - "minor" 34 | patch: 35 | labels: 36 | - "patch" 37 | 38 | autolabeler: 39 | - label: "patch" 40 | title: 41 | - "/fix/i" 42 | - label: "minor" 43 | title: 44 | - "/feat/i" 45 | - label: "major" 46 | title: 47 | - "/BREAKING_CHANGE/i" 48 | - label: "docs" 49 | title: 50 | - "/docs/i" 51 | - label: "skip-changelog" 52 | title: 53 | - "/chore/i" 54 | 55 | template: | 56 | ## Changes 57 | 58 | $CHANGES 59 | -------------------------------------------------------------------------------- /.github/workflows/draft-release.yml: -------------------------------------------------------------------------------- 1 | name: Release Drafter 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request_target: 8 | types: [opened, reopened, synchronize, edited, labeled] 9 | 10 | jobs: 11 | label_pr: 12 | if: github.event_name == 'pull_request_target' 13 | name: Label PR with release labels 14 | permissions: 15 | pull-requests: write 16 | contents: read 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: release-drafter/release-drafter@v6 20 | env: 21 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 22 | 23 | update_release_draft: 24 | if: github.event_name != 'pull_request_target' 25 | name: Update draft release 26 | permissions: write-all 27 | runs-on: ubuntu-latest 28 | steps: 29 | - uses: release-drafter/release-drafter@v6 30 | env: 31 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 32 | -------------------------------------------------------------------------------- /.github/workflows/lint-pr.yml: -------------------------------------------------------------------------------- 1 | name: "Lint PR" 2 | 3 | permissions: 4 | pull-requests: read 5 | 6 | on: 7 | pull_request_target: 8 | types: 9 | - opened 10 | - edited 11 | - synchronize 12 | 13 | jobs: 14 | main: 15 | name: Validate PR title 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: amannn/action-semantic-pull-request@v5.1.0 19 | env: 20 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 21 | with: 22 | validateSingleCommit: true 23 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | permissions: write-all 4 | 5 | on: 6 | workflow_dispatch: 7 | inputs: 8 | version: 9 | description: "Set initial version or override calculated version. Use the following convention for the version: v.." 10 | required: false 11 | 12 | jobs: 13 | release: 14 | name: Release 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Dry run release draft to calculate next version 18 | uses: release-drafter/release-drafter@v6 19 | id: draft 20 | with: 21 | prerelease: false 22 | env: 23 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 24 | 25 | - name: Check for changes 26 | id: release 27 | run: | 28 | isRelease=${{ github.event.inputs.version || endsWith(steps.draft.outputs.tag_name,'No changes%0A') }} 29 | major=${version%%.*} 30 | echo "::set-output name=is_release::${isRelease}" 31 | - name: Set version 32 | if: ${{ steps.release.outputs.is_release }} 33 | id: version 34 | run: | 35 | version=${{ github.event.inputs.version || steps.draft.outputs.tag_name }} 36 | major=${version%%.*} 37 | echo "::set-output name=version::${version}" 38 | echo "::set-output name=major::${major}" 39 | - uses: release-drafter/release-drafter@master 40 | if: ${{ steps.release.outputs.is_release }} 41 | with: 42 | version: ${{ steps.version.outputs.version }} 43 | publish: true 44 | env: 45 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 46 | 47 | - name: Checkout code 48 | if: ${{ steps.release.outputs.is_release }} 49 | uses: actions/checkout@v4 50 | with: 51 | ref: main 52 | 53 | - name: Update major tag 54 | if: ${{ steps.release.outputs.is_release }} 55 | uses: actions/github-script@v7 56 | with: 57 | script: | 58 | let newTag = false 59 | try { 60 | const result = await github.rest.git.getRef({ 61 | owner: context.repo.owner, 62 | repo: context.repo.repo, 63 | ref: 'tags/${{ steps.version.outputs.major }}', 64 | }) 65 | console.log(JSON.stringify(result, null, 2)) 66 | } catch (e) { 67 | console.log(e) 68 | newTag = true 69 | } 70 | if (newTag) { 71 | github.rest.git.createRef({ 72 | owner: context.repo.owner, 73 | repo: context.repo.repo, 74 | ref: 'refs/tags/${{ steps.version.outputs.major }}', 75 | sha: context.sha, 76 | }) 77 | } else { 78 | github.rest.git.updateRef({ 79 | owner: context.repo.owner, 80 | repo: context.repo.repo, 81 | ref: 'tags/${{ steps.version.outputs.major }}', 82 | sha: context.sha, 83 | force: true 84 | }) 85 | } 86 | -------------------------------------------------------------------------------- /.github/workflows/validate.yml: -------------------------------------------------------------------------------- 1 | name: "Terraform checks" 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | paths-ignore: 8 | - "*.md" 9 | 10 | jobs: 11 | verify_module: 12 | name: Verify module 13 | strategy: 14 | matrix: 15 | terraform: [1.1.6, "latest"] 16 | runs-on: ubuntu-latest 17 | container: 18 | image: hashicorp/terraform:${{ matrix.terraform }} 19 | steps: 20 | - name: "Checkout" 21 | uses: actions/checkout@v4 22 | - name: terraform init 23 | run: terraform init -get -backend=false -input=false 24 | - if: contains(matrix.terraform, '1.1.') 25 | name: check terraform formatting 26 | run: terraform fmt -recursive -check=true -write=false 27 | - if: contains(matrix.terraform, 'latest') # check formatting for the latest release but avoid failing the build 28 | name: check terraform formatting 29 | run: terraform fmt -recursive -check=true -write=false 30 | continue-on-error: true 31 | - name: validate terraform 32 | run: terraform validate 33 | 34 | verify_examples: 35 | name: Verify examples 36 | strategy: 37 | fail-fast: false 38 | matrix: 39 | terraform: [1.1.6, "latest"] 40 | example: ["default"] 41 | defaults: 42 | run: 43 | working-directory: examples/${{ matrix.example }} 44 | runs-on: ubuntu-latest 45 | container: 46 | image: hashicorp/terraform:${{ matrix.terraform }} 47 | steps: 48 | - uses: actions/checkout@v4 49 | - name: terraform init 50 | run: terraform init -get -backend=false -input=false 51 | - if: contains(matrix.terraform, '1.1.') 52 | name: check terraform formatting 53 | run: terraform fmt -recursive -check=true -write=false 54 | - if: contains(matrix.terraform, 'latest') # check formatting for the latest release but avoid failing the build 55 | name: check terraform formatting 56 | run: terraform fmt -recursive -check=true -write=false 57 | continue-on-error: true 58 | - name: validate terraform 59 | run: terraform validate 60 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/terraform 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=terraform 4 | 5 | ### Terraform ### 6 | # Local .terraform directories 7 | **/.terraform/* 8 | 9 | # .tfstate files 10 | *.tfstate 11 | *.tfstate.* 12 | 13 | # Crash log files 14 | crash.log 15 | crash.*.log 16 | 17 | # Exclude all .tfvars files, which are likely to contain sensitive data, such as 18 | # password, private keys, and other secrets. These should not be part of version 19 | # control as they are data points which are potentially sensitive and subject 20 | # to change depending on the environment. 21 | # 22 | *.tfvars 23 | 24 | # Ignore override files as they are usually used to override resources locally and so 25 | # are not checked in 26 | override.tf 27 | override.tf.json 28 | *_override.tf 29 | *_override.tf.json 30 | 31 | # Include override files you do wish to add to version control using negated pattern 32 | # !example_override.tf 33 | 34 | # Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan 35 | # example: *tfplan* 36 | 37 | # Ignore CLI configuration files 38 | .terraformrc 39 | terraform.rc 40 | 41 | # End of https://www.toptal.com/developers/gitignore/api/terraform 42 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/antonbabenko/pre-commit-terraform 3 | rev: v1.64.0 4 | hooks: 5 | - id: terraform_fmt 6 | - id: terraform_docs 7 | - repo: https://github.com/pre-commit/pre-commit-hooks 8 | rev: v4.1.0 9 | hooks: 10 | - id: check-merge-conflict 11 | - id: end-of-file-fixer 12 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp 6 | "editorconfig.editorconfig", 7 | "yzhang.markdown-all-in-one", 8 | "hashicorp.terraform" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | When contributing we follow semantic versioning and [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/) in our Pull Requests and releases. You must prefix your PR and commit name with a type as below as well as a scope to identify what has changed, e.g. `type(scope): - Description`. Releases will be calculated based on PR labels that are automatically applied based on the PR title. Therefore the PR title needs also to follow the same rules as semantic commit. 4 | 5 | Available types to use in your PR: 6 | 7 | - feat: A new feature 8 | - fix: A bugfix 9 | - docs: Documentation only changes 10 | - style: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc) 11 | - refactor: A code change that neither fixes a bug nor adds a feature 12 | - perf: A code change that improves performance 13 | - test: Adding missing tests or correcting existing tests 14 | - build: Changes that affect the build tool or external dependencies (example scopes: gulp, broccoli, npm) 15 | - ci: Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs) 16 | - chore: Other changes that don't modify src or test files 17 | - revert: Reverts a previous commit 18 | 19 | This is enforced by the workflow [`semantic_commit_check.yaml`](.github/workflows/pr-lint.yaml) which is run on every PR. 20 | 21 | ## Releasing 22 | 23 | Releases are automatically generated based on the PR labels automatically added with [workflow](.github/workflows/release.yml) 24 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) Copyright © 2022 Koninklijke Philips N.V, https://www.philips.com 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /MAINTAINERS.md: -------------------------------------------------------------------------------- 1 | Scott Guymer 2 | Niek Palm 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Terraform module AWS OIDC integration GitHub Actions 2 | 3 | This [Terraform](https://www.terraform.io/) module manages OpenID Connect (OIDC) integration between [GitHub Actions and AWS](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-amazon-web-services). 4 | 5 | ## Description 6 | 7 | The module is strict on the claim checks to avoid that creating an OpenID connect integration opens your AWS account to any GitHub repo. However this strictness is not taking all the risk away. Ensure you familiarize yourself with OpenID Connect and the docs provided by GitHub and AWS. As always think about minimizing the privileges. 8 | 9 | The module can manage the following: 10 | 11 | - The OpenID Connect identity provider for GitHub in your AWS account (via a submodule). 12 | - A role and assume role policy to check to check OIDC claims. 13 | 14 | ### Manage the OIDC identity provider 15 | 16 | The module provides an option for creating an OpenID connect provider. Using the internal `provider` module to create the OpenID Connect provider. This configuration will create the provider and output the ARN. This output can be passed to other instances of the module to setup roles for repositories on the same provider. Alternative you can create the OpenID connect provider via the resource [aws_iam_openid_connect_provider](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_openid_connect_provider) or in case you have an existing one look-up via the data source [aws_iam_openid_connect_provider](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_openid_connect_provider). 17 | 18 | ### Manage roles for a repo 19 | 20 | The module creates a role with an assume role policy to check the OIDC claims for the given repo. Be default the policy is set to only allow actions running on the main branch and deny pull request actions. You can choose based on your need one (or more) of the default conditions to check. Additionally, a list of conditions can be provided. The role can only be assumed when all conditions evaluate to true. The following default conditions can be set. 21 | 22 | - `allow_main` : Allow GitHub Actions only running on the main branch. 23 | - `allow_environment`: Allow GitHub Actions only for environments, by setting `github_environments` you can limit to a dedicated environment. 24 | - `deny_pull_request`: Denies assuming the role for a pull request. 25 | - `allow_all` : Allow GitHub Actions for any claim for the repository. Be careful, this allows forks as well to assume the role! 26 | 27 | ## Required GitHub Workflows Permissions 28 | 29 | When configuring GitHub workflows to use this module, you need to specify the following permissions in your workflow configuration: 30 | 31 | ```yaml 32 | permissions: 33 | id-token: write 34 | ``` 35 | 36 | This permission is required for the GitHub Actions to be able to assume the IAM role created by this module. 37 | 38 | ## Usages 39 | 40 | In case there is not OpenID Connect provider already created in the AWS account, create one via the submodule. 41 | 42 | ```hcl 43 | module "oidc_provider" { 44 | source = "github.com/philips-labs/terraform-aws-github-oidc?ref=//modules/provider" 45 | } 46 | ``` 47 | 48 | Nest you ca pass the output the one or multiple instances of the module. 49 | 50 | ```hcl 51 | module "oidc_repo_s3" { 52 | source = "github.com/philips-labs/terraform-aws-github-oidc?ref=" 53 | 54 | openid_connect_provider_arn = module.oidc_provider.openid_connect_provider.arn 55 | repo = var.repo_s3 56 | role_name = "repo-s3" 57 | 58 | # optional 59 | # override default conditions 60 | default_conditions = ["allow_main"] 61 | 62 | # add extra conditions, will be merged with the default_conditions 63 | conditions = [{ 64 | test = "StringLike" 65 | variable = "token.actions.githubusercontent.com:sub" 66 | values = ["repo:my-org/my-repo:pull_request"] 67 | }] 68 | } 69 | ``` 70 | 71 | ## Examples 72 | 73 | Check out the [example](examples/default/README.md) for a full example of using the module. 74 | 75 | 76 | ## Requirements 77 | 78 | | Name | Version | 79 | |------|---------| 80 | | [terraform](#requirement\_terraform) | >= 1 | 81 | | [aws](#requirement\_aws) | >= 3 | 82 | 83 | ## Providers 84 | 85 | | Name | Version | 86 | |------|---------| 87 | | [aws](#provider\_aws) | >= 3 | 88 | | [random](#provider\_random) | n/a | 89 | 90 | ## Modules 91 | 92 | No modules. 93 | 94 | ## Resources 95 | 96 | | Name | Type | 97 | |------|------| 98 | | [aws_iam_role.main](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | 99 | | [aws_iam_role_policy_attachment.custom](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | 100 | | [random_string.random](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/string) | resource | 101 | | [aws_iam_policy_document.github_actions_assume_role_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | 102 | 103 | ## Inputs 104 | 105 | | Name | Description | Type | Default | Required | 106 | |------|-------------|------|---------|:--------:| 107 | | [account\_ids](#input\_account\_ids) | Root users of these Accounts (id) would be given the permissions to assume the role created by this module. | `list(string)` | `[]` | no | 108 | | [conditions](#input\_conditions) | (Optional) Additonal conditions for checking the OIDC claim. |
list(object({
test = string
variable = string
values = list(string)
}))
| `[]` | no | 109 | | [custom\_principal\_arns](#input\_custom\_principal\_arns) | List of IAM principals ARNs able to assume the role created by this module. | `list(string)` | `[]` | no | 110 | | [default\_conditions](#input\_default\_conditions) | (Optional) Default condtions to apply, at least one of the following is madatory: 'allow\_main', 'allow\_environment', 'deny\_pull\_request' and 'allow\_all'. | `list(string)` |
[
"allow_main",
"deny_pull_request"
]
| no | 111 | | [github\_environments](#input\_github\_environments) | (Optional) Allow GitHub action to deploy to all (default) or to one of the environments in the list. | `list(string)` |
[
"*"
]
| no | 112 | | [github\_oidc\_issuer](#input\_github\_oidc\_issuer) | OIDC issuer for GitHub Actions | `string` | `"token.actions.githubusercontent.com"` | no | 113 | | [openid\_connect\_provider\_arn](#input\_openid\_connect\_provider\_arn) | Set the openid connect provider ARN when the provider is not managed by the module. | `string` | n/a | yes | 114 | | [repo](#input\_repo) | (Optional) GitHub repository to grant access to assume a role via OIDC. When the repo is set, a role will be created. | `string` | `null` | no | 115 | | [role\_max\_session\_duration](#input\_role\_max\_session\_duration) | Maximum session duration (in seconds) that you want to set for the specified role. | `number` | `null` | no | 116 | | [role\_name](#input\_role\_name) | (Optional) role name of the created role, if not provided the `namespace` will be used. | `string` | `null` | no | 117 | | [role\_path](#input\_role\_path) | (Optional) Path for the created role, requires `repo` is set. | `string` | `"/github-actions/"` | no | 118 | | [role\_permissions\_boundary](#input\_role\_permissions\_boundary) | (Optional) Boundary for the created role, requires `repo` is set. | `string` | `null` | no | 119 | | [role\_policy\_arns](#input\_role\_policy\_arns) | List of ARNs of IAM policies to attach to IAM role | `list(string)` | `[]` | no | 120 | 121 | ## Outputs 122 | 123 | | Name | Description | 124 | |------|-------------| 125 | | [conditions](#output\_conditions) | The assume conditions added to the role. | 126 | | [role](#output\_role) | The crated role that can be assumed for the configured repository. | 127 | 128 | 129 | ## Contribution 130 | 131 | We welcome contribution, please checkout the [contribution guide](CONTRIBUTING.md). Be-aware we use [pre commit hooks](https://pre-commit.com/) to update the docs. 132 | 133 | ## Release 134 | 135 | Releases are create automated from the main branch using conventional commit messages. 136 | 137 | ## Contact 138 | 139 | For question you can reach out to one of the [maintainers](./MAINTAINERS.md). 140 | -------------------------------------------------------------------------------- /bin/generate-thumbprint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ## Script to generate the Thumbprint 3 | ## 4 | ## https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_providers_create_oidc_verify-thumbprint.html 5 | ## 6 | ## 7 | HOST=$(curl https://vstoken.actions.githubusercontent.com/.well-known/openid-configuration | 8 | jq -r '.jwks_uri | split("/")[2]') 9 | % echo | openssl s_client -servername $HOST -showcerts -connect $HOST:443 2>/dev/null | 10 | sed -n -e '/BEGIN/h' -e '/BEGIN/,/END/H' -e '$x' -e '$p' | tail +2 | 11 | openssl x509 -fingerprint -noout | 12 | sed -e "s/.*=//" -e "s/://g" | 13 | tr "ABCDEF" "abcdef" 14 | -------------------------------------------------------------------------------- /examples/default/.terraform.lock.hcl: -------------------------------------------------------------------------------- 1 | # This file is maintained automatically by "terraform init". 2 | # Manual edits may be lost in future updates. 3 | 4 | provider "registry.terraform.io/hashicorp/aws" { 5 | version = "5.70.0" 6 | constraints = ">= 3.0.0" 7 | hashes = [ 8 | "h1:LKnWZnujHcQPm3MAk4elP3H9VXNjlO6rNqlO5s330Yg=", 9 | "zh:09cbec93c324e6f03a866244ecb2bae71fdf1f5d3d981e858b745c90606b6b6d", 10 | "zh:19685d9f4c9ddcfa476a9a428c6c612be4a1b4e8e1198fbcbb76436b735284ee", 11 | "zh:3358ee6a2b24c982b7c83fac0af6898644d1bbdabf9c4e0589e91e427641ba88", 12 | "zh:34f9f2936de7384f8ed887abdbcb54aea1ce7b0cf2e85243a3fd3904d024747f", 13 | "zh:4a99546cc2140304c90d9ccb9db01589d4145863605a0fcd90027a643ea3ec5d", 14 | "zh:4da32fec0e10dab5aa3dea3c9fe57adc973cc73a71f5d59da3f65d85d925dc3f", 15 | "zh:659cf94522bc38ce0af70f7b0371b2941a0e0bcad02d17c1a7b264575fe07224", 16 | "zh:6f1c172c9b98bc86e4f0526872098ee3246c2620f7b323ce0c2ce6427987f7d2", 17 | "zh:79bf8fb8f37c308742e287694a9de081ff8502b065a390d1bcfbd241b4eca203", 18 | "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", 19 | "zh:b7a5e1dfd9e179d70a169ddd4db44b56da90309060e27d36b329fe5fb3528e29", 20 | "zh:c2cc728cb18ffd5c4814a10c203452c71f5ab0c46d68f9aa9183183fa60afd87", 21 | "zh:c89bb37d2b8947c9a0d62b0b86ace51542f3327970f4e56a68bf81d9d0b8b65b", 22 | "zh:ef2a61e8112c3b5e70095508aadaadf077e904b62b9cfc22030337f773bba041", 23 | "zh:f714550b858d141ea88579f25247bda2a5ba461337975e77daceaf0bb7a9c358", 24 | ] 25 | } 26 | 27 | provider "registry.terraform.io/hashicorp/random" { 28 | version = "3.1.0" 29 | hashes = [ 30 | "h1:rKYu5ZUbXwrLG1w81k7H3nce/Ys6yAxXhWcbtk36HjY=", 31 | "zh:2bbb3339f0643b5daa07480ef4397bd23a79963cc364cdfbb4e86354cb7725bc", 32 | "zh:3cd456047805bf639fbf2c761b1848880ea703a054f76db51852008b11008626", 33 | "zh:4f251b0eda5bb5e3dc26ea4400dba200018213654b69b4a5f96abee815b4f5ff", 34 | "zh:7011332745ea061e517fe1319bd6c75054a314155cb2c1199a5b01fe1889a7e2", 35 | "zh:738ed82858317ccc246691c8b85995bc125ac3b4143043219bd0437adc56c992", 36 | "zh:7dbe52fac7bb21227acd7529b487511c91f4107db9cc4414f50d04ffc3cab427", 37 | "zh:a3a9251fb15f93e4cfc1789800fc2d7414bbc18944ad4c5c98f466e6477c42bc", 38 | "zh:a543ec1a3a8c20635cf374110bd2f87c07374cf2c50617eee2c669b3ceeeaa9f", 39 | "zh:d9ab41d556a48bd7059f0810cf020500635bfc696c9fc3adab5ea8915c1d886b", 40 | "zh:d9e13427a7d011dbd654e591b0337e6074eef8c3b9bb11b2e39eaaf257044fd7", 41 | "zh:f7605bd1437752114baf601bdf6931debe6dc6bfe3006eb7e9bb9080931dca8a", 42 | ] 43 | } 44 | -------------------------------------------------------------------------------- /examples/default/README.md: -------------------------------------------------------------------------------- 1 | # Managing multiple repo's for a single AWS account 2 | 3 | The module provides an example how to setup roles to to use with OIDC for multiple repositories. 4 | 5 | - A repository with some access to S3 (same as in the [single example](../single-repo/README.md)) 6 | - A repository with access to ECR with the tag `allow-gh-action-access` 7 | - Environment for ECR repo (requires a paid GitHub subscription) 8 | 9 | ## Usages 10 | 11 | Create a GitHub repositories (private) for S3 and ECR and set the variable `repo` to the name of your created repo. Add as secret `AWS_ACCOUNT_ID` and set the value to your account. 12 | 13 | ```bash 14 | terraform init 15 | terraform apply 16 | ``` 17 | 18 | For the S3 repository follow the directions in the single example. On the console the name of the ECR repo and role are printed. Next update [workflow](../repositories/.github/workflows/../../repo-ecr/.github/workflows/ecr.yml) for the repo and role. Add, commit and push. The job should now push a busybox container to your ECR repo. 19 | 20 | Finally you can clean up with `terraform destroy` 21 | 22 | ## Required GitHub Workflows Permissions 23 | 24 | When configuring GitHub workflows to use this module, you need to specify the following permissions in your workflow configuration: 25 | 26 | ```yaml 27 | permissions: 28 | id-token: write 29 | ``` 30 | 31 | This permission is required for the GitHub Actions to be able to assume the IAM role created by this module. 32 | -------------------------------------------------------------------------------- /examples/default/main.tf: -------------------------------------------------------------------------------- 1 | module "oidc_provider" { 2 | source = "../../modules/provider" 3 | } 4 | 5 | data "aws_caller_identity" "current" {} 6 | 7 | module "oidc_repo_s3" { 8 | source = "../../" 9 | 10 | openid_connect_provider_arn = module.oidc_provider.openid_connect_provider.arn 11 | repo = var.repo_s3 12 | role_name = "repo-s3" 13 | account_ids = [data.aws_caller_identity.current.account_id] 14 | } 15 | 16 | module "oidc_repo_ecr" { 17 | source = "../../" 18 | 19 | openid_connect_provider_arn = module.oidc_provider.openid_connect_provider.arn 20 | repo = var.repo_ecr 21 | default_conditions = ["allow_environment"] 22 | github_environments = ["production"] 23 | account_ids = [data.aws_caller_identity.current.account_id] 24 | } 25 | 26 | ########################################## 27 | ## 28 | ## Resources for s3 repo 29 | ## 30 | ########################################## 31 | resource "aws_iam_role_policy" "s3" { 32 | name = "s3-policy" 33 | role = module.oidc_repo_s3.role.name 34 | policy = data.aws_iam_policy_document.s3.json 35 | } 36 | 37 | data "aws_iam_policy_document" "s3" { 38 | statement { 39 | sid = "1" 40 | 41 | actions = [ 42 | "s3:ListBucket", 43 | "s3:GetObject", 44 | ] 45 | 46 | resources = [ 47 | aws_s3_bucket.example.arn, "${aws_s3_bucket.example.arn}*" 48 | ] 49 | 50 | condition { 51 | test = "StringEquals" 52 | variable = "aws:ResourceTag/allow-gh-action" 53 | 54 | values = ["true"] 55 | } 56 | } 57 | } 58 | 59 | resource "random_uuid" "main" { 60 | } 61 | 62 | resource "aws_s3_bucket" "example" { 63 | bucket = random_uuid.main.result 64 | 65 | tags = { 66 | allow-gh-action-access = "true" 67 | } 68 | } 69 | 70 | ########################################## 71 | ## 72 | ## Resources for ecr repo 73 | ## 74 | ########################################## 75 | resource "aws_iam_role_policy" "ec3" { 76 | name = "ec3-policy" 77 | role = module.oidc_repo_ecr.role.name 78 | 79 | policy = data.aws_iam_policy_document.ecr.json 80 | } 81 | 82 | 83 | data "aws_iam_policy_document" "ecr" { 84 | statement { 85 | actions = [ 86 | "ecr:BatchGetImage", 87 | "ecr:BatchCheckLayerAvailability", 88 | "ecr:CompleteLayerUpload", 89 | "ecr:GetDownloadUrlForLayer", 90 | "ecr:InitiateLayerUpload", 91 | "ecr:PutImage", 92 | "ecr:UploadLayerPart", 93 | ] 94 | 95 | # Another option to lock to resources is by locking on tags 96 | resources = ["*"] 97 | condition { 98 | test = "StringEquals" 99 | variable = "aws:ResourceTag/allow-gh-action" 100 | 101 | values = ["true"] 102 | } 103 | } 104 | } 105 | 106 | resource "aws_ecr_repository" "example" { 107 | name = "lunch-and-learn/example" 108 | image_tag_mutability = "IMMUTABLE" 109 | 110 | tags = { 111 | "allow-gh-action-access" = true 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /examples/default/outputs.tf: -------------------------------------------------------------------------------- 1 | output "roles" { 2 | value = { 3 | repos_s3 = module.oidc_repo_s3.role.name 4 | repos_ecr = module.oidc_repo_ecr.role.name 5 | } 6 | } 7 | 8 | output "ecr" { 9 | value = { 10 | repository_url = aws_ecr_repository.example.repository_url 11 | } 12 | } 13 | 14 | output "s3" { 15 | value = { 16 | bucket = aws_s3_bucket.example.bucket 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/default/providers.tf: -------------------------------------------------------------------------------- 1 | provider "aws" { 2 | region = var.aws_region 3 | 4 | default_tags { 5 | tags = { 6 | Environment = "Example multi repo" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/default/variables.tf: -------------------------------------------------------------------------------- 1 | variable "aws_region" { 2 | description = "AWS region." 3 | type = string 4 | default = "eu-west-1" 5 | } 6 | 7 | variable "repo_s3" { 8 | description = "GitHub repository to grant access to assume a role via OIDC." 9 | type = string 10 | } 11 | 12 | variable "repo_ecr" { 13 | description = "GitHub repository to grant access to assume a role via OIDC." 14 | type = string 15 | } 16 | -------------------------------------------------------------------------------- /examples/repositories/repo-ecr/.github/workflows/ecr.yml: -------------------------------------------------------------------------------- 1 | name: Test ECR 2 | on: 3 | push: 4 | 5 | jobs: 6 | test: 7 | permissions: 8 | id-token: write 9 | runs-on: ubuntu-latest 10 | environment: 11 | name: demo 12 | steps: 13 | - name: configure aws credentials 14 | uses: aws-actions/configure-aws-credentials@v1 15 | with: 16 | role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/github-actions/ 17 | role-session-name: gh-actions 18 | aws-region: eu-west-1 19 | 20 | - run: | 21 | docker pull busybox:latest 22 | docker 23 | docker tag busybox:latest ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.eu-west-1.amazonaws.com/busybox:latest 24 | docker push ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.eu-west-1.amazonaws.com/busybox:latest 25 | -------------------------------------------------------------------------------- /examples/repositories/repo-s3/.github/workflows/s3.yml: -------------------------------------------------------------------------------- 1 | name: Test S3 2 | on: 3 | push: 4 | 5 | jobs: 6 | test: 7 | permissions: 8 | id-token: write 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: configure aws credentials 12 | uses: aws-actions/configure-aws-credentials@v1 13 | with: 14 | role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/github-actions/repo-s3 15 | role-session-name: gh-actions 16 | aws-region: eu-west-1 17 | 18 | - run: | 19 | aws s3 ls 20 | -------------------------------------------------------------------------------- /examples/variables.tf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/philips-labs/terraform-aws-github-oidc/4f7ccdece7c303b000efffc3d43f9db42c2b2817/examples/variables.tf -------------------------------------------------------------------------------- /main.tf: -------------------------------------------------------------------------------- 1 | resource "random_string" "random" { 2 | count = var.role_name == null ? 1 : 0 3 | 4 | length = 8 5 | lower = true 6 | special = false 7 | } 8 | 9 | locals { 10 | github_environments = (length(var.github_environments) > 0 && var.repo != null) ? [for e in var.github_environments : "repo:${var.repo}:environment:${e}"] : ["ensurethereisnotmatch"] 11 | role_name = (var.repo != null && var.role_name != null) ? var.role_name : "${substr(replace(var.repo != null ? var.repo : "", "/", "-"), 0, 64 - 8)}-${random_string.random[0].id}" 12 | 13 | variable_sub = "${var.github_oidc_issuer}:sub" 14 | 15 | default_allow_main = contains(var.default_conditions, "allow_main") ? [{ 16 | test = "StringLike" 17 | variable = local.variable_sub 18 | values = ["repo:${var.repo}:ref:refs/heads/${var.repo_mainline_branch}"] 19 | }] : [] 20 | 21 | default_allow_environment = contains(var.default_conditions, "allow_environment") ? [{ 22 | test = "StringLike" 23 | variable = local.variable_sub 24 | values = local.github_environments 25 | }] : [] 26 | 27 | default_allow_all = contains(var.default_conditions, "allow_all") ? [{ 28 | test = "StringLike" 29 | variable = local.variable_sub 30 | values = ["repo:${var.repo}:*"] 31 | }] : [] 32 | 33 | default_deny_pull_request = contains(var.default_conditions, "deny_pull_request") ? [{ 34 | test = "StringNotLike" 35 | variable = local.variable_sub 36 | values = ["repo:${var.repo}:pull_request"] 37 | }] : [] 38 | 39 | conditions = setunion(local.default_allow_main, local.default_allow_environment, local.default_allow_all, local.default_deny_pull_request, var.conditions) 40 | merge_conditions = [ 41 | for k, v in { for c in local.conditions : "${c.test}|${c.variable}" => c... } : # group by test & variable 42 | { 43 | "test" : k, 44 | "values" : flatten([for index, sp in v[*].values : v[index].values if v[index].variable == v[0].variable]) # loop again to build the values inner map 45 | } 46 | ] 47 | 48 | root_principal_arns = [for acc in var.account_ids : "arn:aws:iam::${acc}:root"] 49 | merged_principal_arns = concat(local.root_principal_arns, var.custom_principal_arns) 50 | } 51 | 52 | data "aws_iam_policy_document" "github_actions_assume_role_policy" { 53 | count = var.repo != null ? 1 : 0 54 | 55 | dynamic "statement" { 56 | for_each = length(local.merged_principal_arns) > 0 ? [1] : [] 57 | content { 58 | actions = ["sts:AssumeRole"] 59 | 60 | principals { 61 | type = "AWS" 62 | identifiers = local.merged_principal_arns 63 | } 64 | } 65 | } 66 | 67 | statement { 68 | actions = ["sts:AssumeRoleWithWebIdentity"] 69 | principals { 70 | type = "Federated" 71 | identifiers = [var.openid_connect_provider_arn] 72 | } 73 | 74 | condition { 75 | test = "StringEquals" 76 | variable = "${var.github_oidc_issuer}:aud" 77 | values = ["sts.amazonaws.com"] 78 | } 79 | 80 | dynamic "condition" { 81 | for_each = local.merge_conditions 82 | 83 | content { 84 | test = split("|", condition.value.test)[0] 85 | variable = split("|", condition.value.test)[1] 86 | values = condition.value.values 87 | } 88 | } 89 | } 90 | } 91 | 92 | resource "aws_iam_role" "main" { 93 | count = var.repo != null ? 1 : 0 94 | 95 | name = local.role_name 96 | path = var.role_path 97 | permissions_boundary = var.role_permissions_boundary 98 | assume_role_policy = data.aws_iam_policy_document.github_actions_assume_role_policy[0].json 99 | max_session_duration = var.role_max_session_duration 100 | } 101 | 102 | resource "aws_iam_role_policy_attachment" "custom" { 103 | count = length(var.role_policy_arns) 104 | 105 | role = join("", aws_iam_role.main.*.name) 106 | policy_arn = var.role_policy_arns[count.index] 107 | } 108 | -------------------------------------------------------------------------------- /modules/provider/README.md: -------------------------------------------------------------------------------- 1 | # Terraform (sub) module to crate an OpenID Connect provider for GitHub 2 | 3 | ## Description 4 | 5 | The module creates a OpenID Connect provider for GitHub. See for directions the [README](../../README.md) on top-level. See the [example](../../examples/default/README.md) for how to use the sub module. 6 | 7 | 8 | 9 | ## Requirements 10 | 11 | No requirements. 12 | 13 | ## Providers 14 | 15 | | Name | Version | 16 | |------|---------| 17 | | [aws](#provider\_aws) | n/a | 18 | 19 | ## Modules 20 | 21 | No modules. 22 | 23 | ## Resources 24 | 25 | | Name | Type | 26 | |------|------| 27 | | [aws_iam_openid_connect_provider.github_actions](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_openid_connect_provider) | resource | 28 | 29 | ## Inputs 30 | 31 | | Name | Description | Type | Default | Required | 32 | |------|-------------|------|---------|:--------:| 33 | | [tags](#input\_tags) | A map of tags to add to OIDC identity provider. | `map(string)` | `{}` | no | 34 | | [thumbprint\_list](#input\_thumbprint\_list) | (Optional) A list of server certificate thumbprints for the OpenID Connect (OIDC) identity provider's server certificate(s). | `list(string)` |
[
"6938fd4d98bab03faadb97b34396831e3780aea1",
"1c58a3a8518e8759bf075b76b750d4f2df264fcd"
]
| no | 35 | 36 | ## Outputs 37 | 38 | | Name | Description | 39 | |------|-------------| 40 | | [openid\_connect\_provider](#output\_openid\_connect\_provider) | AWS OpenID Connected identity provider. | 41 | 42 | -------------------------------------------------------------------------------- /modules/provider/main.tf: -------------------------------------------------------------------------------- 1 | resource "aws_iam_openid_connect_provider" "github_actions" { 2 | url = "https://token.actions.githubusercontent.com" 3 | client_id_list = ["sts.amazonaws.com"] 4 | thumbprint_list = var.thumbprint_list 5 | tags = var.tags 6 | } 7 | -------------------------------------------------------------------------------- /modules/provider/outputs.tf: -------------------------------------------------------------------------------- 1 | output "openid_connect_provider" { 2 | description = "AWS OpenID Connected identity provider." 3 | value = aws_iam_openid_connect_provider.github_actions 4 | } 5 | -------------------------------------------------------------------------------- /modules/provider/variables.tf: -------------------------------------------------------------------------------- 1 | ## 2 | ## Thumbprint published by GitHub https://github.blog/changelog/2022-01-13-github-actions-update-on-oidc-based-deployments-to-aws/ 3 | ## can also be generated with the script in ./bin/generate-thumbprint.sh 4 | ## 5 | variable "thumbprint_list" { 6 | description = "(Optional) A list of server certificate thumbprints for the OpenID Connect (OIDC) identity provider's server certificate(s)." 7 | type = list(string) 8 | default = [ 9 | "6938fd4d98bab03faadb97b34396831e3780aea1", 10 | "1c58a3a8518e8759bf075b76b750d4f2df264fcd" 11 | ] 12 | } 13 | 14 | variable "tags" { 15 | description = "A map of tags to add to OIDC identity provider." 16 | type = map(string) 17 | default = {} 18 | } 19 | -------------------------------------------------------------------------------- /outputs.tf: -------------------------------------------------------------------------------- 1 | output "role" { 2 | description = "The crated role that can be assumed for the configured repository." 3 | value = var.repo != null ? aws_iam_role.main[0] : null 4 | } 5 | 6 | output "conditions" { 7 | description = "The assume conditions added to the role." 8 | value = local.merge_conditions 9 | } 10 | -------------------------------------------------------------------------------- /variables.tf: -------------------------------------------------------------------------------- 1 | variable "conditions" { 2 | description = "(Optional) Additonal conditions for checking the OIDC claim." 3 | type = list(object({ 4 | test = string 5 | variable = string 6 | values = list(string) 7 | })) 8 | default = [] 9 | } 10 | 11 | variable "default_conditions" { 12 | description = "(Optional) Default condtions to apply, at least one of the following is madatory: 'allow_main', 'allow_environment', 'deny_pull_request' and 'allow_all'." 13 | type = list(string) 14 | default = ["allow_main", "deny_pull_request"] 15 | validation { 16 | condition = length(setsubtract(var.default_conditions, ["allow_main", "allow_environment", "deny_pull_request", "allow_all"])) == 0 17 | error_message = "Valid configurations are: 'allow_main', 'allow_environment', 'deny_pull_request' and 'allow_all'." 18 | } 19 | validation { 20 | condition = length(var.default_conditions) > 0 21 | error_message = "At least one of the following configuration needs to be set: 'allow_main', 'allow_environment', 'deny_pull_request' and 'allow_all'." 22 | } 23 | } 24 | 25 | variable "github_environments" { 26 | description = "(Optional) Allow GitHub action to deploy to all (default) or to one of the environments in the list." 27 | type = list(string) 28 | default = ["*"] 29 | } 30 | 31 | variable "openid_connect_provider_arn" { 32 | description = "Set the openid connect provider ARN when the provider is not managed by the module." 33 | type = string 34 | } 35 | 36 | variable "repo" { 37 | description = "(Optional) GitHub repository to grant access to assume a role via OIDC. When the repo is set, a role will be created." 38 | type = string 39 | default = null 40 | validation { 41 | condition = var.repo == null || can(regex("^.+\\/.+", var.repo)) 42 | error_message = "Repo name is not matching the pattern /." 43 | } 44 | validation { 45 | condition = var.repo == null || !can(regex("^.*\\*.*$", var.repo)) 46 | error_message = "Wildcards are not allowed." 47 | } 48 | } 49 | 50 | variable "repo_mainline_branch" { 51 | description = "(Optional) Mainline branch of the GitHub repository, defaults to 'main'. This will be the main/default branch that `allow_main` provides access to." 52 | type = string 53 | default = "main" 54 | } 55 | 56 | variable "role_name" { 57 | description = "(Optional) role name of the created role, if not provided the `namespace` will be used." 58 | type = string 59 | default = null 60 | } 61 | 62 | variable "role_path" { 63 | description = "(Optional) Path for the created role, requires `repo` is set." 64 | type = string 65 | default = "/github-actions/" 66 | } 67 | 68 | variable "role_permissions_boundary" { 69 | description = "(Optional) Boundary for the created role, requires `repo` is set." 70 | type = string 71 | default = null 72 | } 73 | 74 | variable "role_policy_arns" { 75 | description = "List of ARNs of IAM policies to attach to IAM role" 76 | type = list(string) 77 | default = [] 78 | } 79 | 80 | variable "role_max_session_duration" { 81 | description = "Maximum session duration (in seconds) that you want to set for the specified role." 82 | type = number 83 | default = null 84 | } 85 | 86 | variable "account_ids" { 87 | description = "Root users of these Accounts (id) would be given the permissions to assume the role created by this module." 88 | type = list(string) 89 | default = [] 90 | } 91 | 92 | variable "custom_principal_arns" { 93 | description = "List of IAM principals ARNs able to assume the role created by this module." 94 | type = list(string) 95 | default = [] 96 | } 97 | 98 | variable "github_oidc_issuer" { 99 | description = "OIDC issuer for GitHub Actions" 100 | type = string 101 | default = "token.actions.githubusercontent.com" 102 | } 103 | -------------------------------------------------------------------------------- /versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1" 3 | 4 | required_providers { 5 | aws = { 6 | source = "hashicorp/aws" 7 | version = ">= 3" 8 | } 9 | } 10 | } 11 | --------------------------------------------------------------------------------