Skip to content

Commit 367b57b

Browse files
authored
docs: overhaul README, fix broken example, tighten variable descriptions (#9)
1 parent 2f18924 commit 367b57b

6 files changed

Lines changed: 217 additions & 59 deletions

File tree

README.md

Lines changed: 140 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,158 @@
1-
# oidc-aws-github
2-
Terraform module to configure GitHub Actions with AWS Identity Provider Open ID Connect (ODIC.)
3-
This allows GitHub Actions to authenticate against AWS without using any long-lived keys.
4-
This module provisions the necessary role and permissions as defined in the
5-
[official GitHub docs](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-amazon-web-services).
6-
7-
## Multiple repo configuration
8-
This module allows you to create roles for lists of repos(subjects) and policies in the AWS account.
9-
Curently it only supports policies in the same account as the role being created.
10-
This is helpful for non-mono repo style groups as well as for large organizations where teams have separate repo ownership for the same AWS account.
11-
12-
## Debugging features
13-
The `assume_role_names` input allows you to assume the OIDC role and act as if you were the GitHub Actions pipeline.
14-
This is very useful for debugging while you're getting things setup.
15-
Note: we recommend removing this once your production ready so that all further changes are only applied via the pipeline.
16-
17-
## Example GitHub Action
1+
# terraform-aws-oidc-github
2+
3+
Provision GitHub Actions → AWS authentication via OpenID Connect, with no long-lived AWS keys.
4+
5+
This module creates the GitHub OIDC identity provider in your AWS account and one IAM role per entry in `role_subject-repos_policies`. Each role trusts a configurable set of GitHub subject claims (specific repos, branches, tags, environments, or pull requests) and attaches the IAM policies you specify. Based on the [official GitHub OIDC for AWS guide](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-amazon-web-services).
6+
7+
## How it fits together
8+
9+
```
10+
┌─────────────────────┐ OIDC token ┌──────────────────────┐ AssumeRoleWithWebIdentity ┌─────────────┐
11+
│ GitHub Actions job │ ───────────────► │ AWS OIDC provider │ ──────────────────────────────► │ IAM role │
12+
│ (this repo+branch) │ │ (created by module) │ │ (per entry) │
13+
└─────────────────────┘ └──────────────────────┘ └─────────────┘
14+
15+
16+
AWS API calls
17+
(scoped by
18+
policy_arns)
19+
```
20+
21+
The trust policy on each role pins the GitHub `sub` claim to the patterns in `subject_repos`, so a workflow running on the wrong repo, branch, environment, or tag cannot assume the role.
22+
23+
## Quickstart
24+
25+
```hcl
26+
module "aws_oidc_github" {
27+
source = "pelotech/oidc-github/aws"
28+
29+
role_subject-repos_policies = {
30+
"deploy-main" = {
31+
subject_repos = ["repo:my-org/my-repo:ref:refs/heads/main"]
32+
policy_arns = ["arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess"]
33+
}
34+
}
35+
}
36+
37+
output "role_arn" {
38+
value = module.aws_oidc_github.iam_role_arns["deploy-main"]
39+
}
40+
```
41+
42+
`terraform apply`, copy the role ARN into your workflow (see [GitHub Actions workflow](#github-actions-workflow) below), and you're done.
43+
44+
## Full example
45+
46+
A multi-role example with comments lives in [`examples/`](./examples). Highlights:
47+
48+
```hcl
49+
module "aws_oidc_github" {
50+
source = "pelotech/oidc-github/aws"
51+
52+
role_subject-repos_policies = {
53+
# Production deploys: only `main`, with full access and an SSO debug escape hatch.
54+
"infra-prod" = {
55+
role_path = "/github/"
56+
subject_repos = ["repo:my-org/infrastructure:ref:refs/heads/main"]
57+
policy_arns = ["arn:aws:iam::aws:policy/AdministratorAccess"]
58+
assume_role_names = ["AWSReservedSSO_AdministratorAccess_xxxxxxxxxxxxxxxx"]
59+
}
60+
61+
# PR previews: any pull request in the same repo, read-only.
62+
"infra-pr-preview" = {
63+
subject_repos = ["repo:my-org/infrastructure:pull_request"]
64+
policy_arns = ["arn:aws:iam::aws:policy/ReadOnlyAccess"]
65+
}
66+
}
67+
}
68+
```
69+
70+
## Subject string cheat sheet
71+
72+
The `subject_repos` list contains GitHub OIDC `sub` claim patterns. Common shapes:
73+
74+
| What you want to allow | Pattern |
75+
| ---------------------------------------- | -------------------------------------------------------- |
76+
| One specific branch | `repo:ORG/REPO:ref:refs/heads/main` |
77+
| Any branch | `repo:ORG/REPO:ref:refs/heads/*` |
78+
| A tag pattern (e.g. release tags) | `repo:ORG/REPO:ref:refs/tags/v*` |
79+
| A GitHub Environment (recommended) | `repo:ORG/REPO:environment:production` |
80+
| Any pull request | `repo:ORG/REPO:pull_request` |
81+
| Any workflow in any repo of an org | `repo:ORG/*` |
82+
83+
GitHub's full claim reference: <https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect#example-subject-claims>.
84+
85+
> **Tip:** Prefer GitHub Environments over branch matching when you can — environments give you reviewer gates, secrets scoping, and protection rules on the GitHub side.
86+
87+
## GitHub Actions workflow
88+
1889
```yaml
1990
jobs:
20-
apply-terraform-main:
91+
deploy:
2192
runs-on: ubuntu-latest
2293
permissions:
23-
id-token: write
24-
contents: read
94+
id-token: write # required to fetch the OIDC token
95+
contents: read # required for actions/checkout
2596
steps:
26-
- uses: actions/checkout@v2
27-
- name: Configure AWS credentials
28-
uses: aws-actions/configure-aws-credentials@v1
97+
- uses: actions/checkout@v4
98+
- uses: aws-actions/configure-aws-credentials@v4
2999
with:
30-
role-to-assume: arn:aws:iam::{account_id}:role/ci/GithubCI-OIDC-TF
100+
role-to-assume: arn:aws:iam::123456789012:role/deploy-main
31101
aws-region: us-west-2
32-
role-duration-seconds: 1200 #can be up to the max set in the terraform module, defaults to 15 min
102+
role-duration-seconds: 3600 # max is var.max_session_duration on the role
103+
- run: aws sts get-caller-identity
33104
```
34105
106+
The `role-to-assume` value is the full ARN of one of the roles this module created — `module.aws_oidc_github.iam_role_arns[<key>]`.
107+
108+
## Debugging with `assume_role_names`
109+
110+
Each entry in `role_subject-repos_policies` accepts an `assume_role_names` list. Any IAM role in the same AWS account named in that list is also allowed to assume the OIDC role via plain `sts:AssumeRole` — useful while you're iterating, because you can run `aws sts assume-role --role-arn ...` from your laptop and act as the workflow.
111+
112+
> Remove `assume_role_names` (or set it to `[]`) before going to production. Once the workflow is stable, the only path to the role should be the OIDC trust.
113+
114+
## Troubleshooting
115+
116+
**`Not authorized to perform sts:AssumeRoleWithWebIdentity`**
117+
Almost always a `sub` claim mismatch. Add `--debug` to `aws-actions/configure-aws-credentials` (or look at the OIDC step output) to see the exact `sub` GitHub sent, then compare it character-for-character to your `subject_repos` patterns. Watch out for branch vs. tag vs. environment vs. pull_request differences.
118+
119+
**`MalformedPolicyDocument: Invalid principal in policy`**
120+
Usually means the OIDC provider hasn't finished creating yet, or the ARN passed into the role's trust policy is wrong. Re-run `terraform apply`.
121+
122+
**`role_path` rejected**
123+
IAM paths must start and end with `/` (e.g. `"/"`, `"/github/"`, `"/teams/platform/"`).
124+
125+
**Wrong AWS account**
126+
The role lives in the account where you applied this module. The `role-to-assume` ARN in the workflow must reference *that* account ID.
127+
128+
**Custom audience**
129+
If you set `audience:` on `aws-actions/configure-aws-credentials`, set the matching `aud_value` here. The default (`sts.amazonaws.com`) matches the action's default.
130+
131+
## Wrappers
132+
133+
The [`wrappers/`](./wrappers) directory contains thin wrapper modules pre-configured to call this module. They're useful if you'd rather declare your roles in `terraform.tfvars`-friendly shapes than write a `module` block. Most users won't need them.
134+
135+
## Contributing & releases
136+
137+
- This repo uses [Conventional Commits](https://www.conventionalcommits.org/) and `release-please` to drive versioning. Use `feat:`, `fix:`, `chore:`, etc. so the changelog and version bumps are correct.
138+
- `pre-commit run --all-files` should pass before pushing — it runs `terraform fmt`, `terraform_tflint`, `yamllint`, and friends.
139+
- Licensed under [MIT](./LICENSE).
140+
35141
<!-- BEGIN_TF_DOCS -->
36142
## Requirements
37143

38144
| Name | Version |
39145
|------|---------|
40-
| <a name="requirement_aws"></a> [aws](#requirement\_aws) | ~> 4.0 |
41-
| <a name="requirement_tls"></a> [tls](#requirement\_tls) | ~> 4.0.3 |
146+
| <a name="requirement_terraform"></a> [terraform](#requirement\_terraform) | >= 1.5.7 |
147+
| <a name="requirement_aws"></a> [aws](#requirement\_aws) | >= 4.0 |
148+
| <a name="requirement_tls"></a> [tls](#requirement\_tls) | >= 4.0.3 |
42149

43150
## Providers
44151

45152
| Name | Version |
46153
|------|---------|
47-
| <a name="provider_aws"></a> [aws](#provider\_aws) | ~> 4.0 |
48-
| <a name="provider_tls"></a> [tls](#provider\_tls) | ~> 4.0.3 |
154+
| <a name="provider_aws"></a> [aws](#provider\_aws) | >= 4.0 |
155+
| <a name="provider_tls"></a> [tls](#provider\_tls) | >= 4.0.3 |
49156

50157
## Modules
51158

@@ -64,10 +171,10 @@ jobs:
64171

65172
| Name | Description | Type | Default | Required |
66173
|------|-------------|------|---------|:--------:|
67-
| <a name="input_aud_value"></a> [aud\_value](#input\_aud\_value) | GitHub Aud | `string` | `"sts.amazonaws.com"` | no |
68-
| <a name="input_github_tls_url"></a> [github\_tls\_url](#input\_github\_tls\_url) | GitHub URL to perform TLS verification against. | `string` | `"https://token.actions.githubusercontent.com"` | no |
69-
| <a name="input_max_session_duration"></a> [max\_session\_duration](#input\_max\_session\_duration) | Maximum session duration in seconds. - by default assume role will be 15 minutes - when calling from actions you'll need to increase up to the maximum allowed hwere | `number` | `3600` | no |
70-
| <a name="input_role_subject-repos_policies"></a> [role\_subject-repos\_policies](#input\_role\_subject-repos\_policies) | role name to repos and policies mapping. role name as the key and object value for repo subjects ie "repo:organization/infrastructure:ref:refs/heads/main" as well as a list of policy arns ie ["Administrator"] and list of roles that can assume the new role for debugging | <pre>map(object({<br> role_path = optional(string)<br> subject_repos = list(string)<br> policy_arns = list(string)<br> assume_role_names = optional(list(string))<br> }))</pre> | n/a | yes |
174+
| <a name="input_aud_value"></a> [aud\_value](#input\_aud\_value) | Audience claim required in the OIDC token. Defaults to the value the official aws-actions/configure-aws-credentials action sends. | `string` | `"sts.amazonaws.com"` | no |
175+
| <a name="input_github_tls_url"></a> [github\_tls\_url](#input\_github\_tls\_url) | GitHub OIDC issuer URL. Override only for GitHub Enterprise Server. | `string` | `"https://token.actions.githubusercontent.com"` | no |
176+
| <a name="input_max_session_duration"></a> [max\_session\_duration](#input\_max\_session\_duration) | Maximum session duration in seconds for every role created. Defaults to 1 hour. Increase up to 43200 (12h) if your workflows need longer sessions. | `number` | `3600` | no |
177+
| <a name="input_role_subject-repos_policies"></a> [role\_subject-repos\_policies](#input\_role\_subject-repos\_policies) | Map of IAM roles to create. The map key is the role name. Each value defines:<br/> - `subject_repos` : OIDC subject claims allowed to assume this role (e.g. "repo:my-org/my-repo:ref:refs/heads/main").<br/> - `policy_arns` : IAM policy ARNs to attach to the role.<br/> - `role_path` : (optional) IAM path for the role. Defaults to "/".<br/> - `assume_role_names` : (optional) IAM role names in the same account that may also assume this role (useful for local debugging). | <pre>map(object({<br/> role_path = optional(string)<br/> subject_repos = list(string)<br/> policy_arns = list(string)<br/> assume_role_names = optional(list(string))<br/> }))</pre> | n/a | yes |
71178

72179
## Outputs
73180

examples/README.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Example: GitHub Actions OIDC roles in AWS
2+
3+
This example provisions:
4+
5+
- A GitHub OIDC identity provider in your AWS account.
6+
- Two IAM roles trusted by that provider:
7+
- `org-infra-main` — full Administrator access, only assumable from the `main` branch of `organization/infrastructure`. Also assumable by an SSO role for local debugging.
8+
- `org-infra-all-branches``AmazonS3ReadOnlyAccess`, assumable from any branch of the same repo.
9+
10+
## Prerequisites
11+
12+
- Terraform `>= 1.5.7`
13+
- AWS credentials with permission to manage IAM and OIDC providers (e.g. via `aws-sso-cli` or `AWS_PROFILE`)
14+
- Update `provider "aws"` in `main.tf` with the region you want the roles created in.
15+
- Replace `organization/infrastructure` with your own `org/repo` and the placeholder SSO role with one from your account.
16+
17+
## Run it
18+
19+
```sh
20+
terraform init
21+
terraform plan
22+
terraform apply
23+
```
24+
25+
## Clean up
26+
27+
```sh
28+
terraform destroy
29+
```
30+
31+
## Next steps
32+
33+
See the [root README](../README.md) for the full input reference, the GitHub Actions workflow snippet, the subject-string cheat sheet, and troubleshooting.

examples/main.tf

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ terraform {
33
required_providers {
44
aws = {
55
source = "hashicorp/aws"
6-
version = "~> 4.0"
6+
version = ">= 4.0"
77
}
88
tls = {
99
source = "hashicorp/tls"
@@ -22,13 +22,18 @@ module "aws_oidc_github" {
2222
providers = {
2323
aws = aws.my_alias
2424
}
25-
subject_policies = {
25+
26+
role_subject-repos_policies = {
27+
# A role scoped to the `main` branch of one repo, granted Administrator,
28+
# and additionally assumable by an SSO role for local debugging.
2629
"org-infra-main" = {
2730
role_path = "/some-role-path/"
2831
subject_repos = ["repo:organization/infrastructure:ref:refs/heads/main"]
2932
policy_arns = ["arn:aws:iam::aws:policy/AdministratorAccess"]
3033
assume_role_names = ["aws-reserved/sso.amazonaws.com/eu-west-2/AWSReservedSSO_SomeManagedpolicy_XXXXXXXXXXXXXXXXX"]
3134
}
35+
36+
# A read-only role usable from any branch of the same repo.
3237
"org-infra-all-branches" = {
3338
subject_repos = ["repo:organization/infrastructure:ref:refs/heads/*"]
3439
policy_arns = ["arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess"]

0 commit comments

Comments
 (0)