This Terraform module creates a comprehensive set of IAM roles with OpenID Connect (OIDC) trust relationships for secure CI/CD pipeline integration with AWS. The module implements a three-role pattern that provides granular access control for different stages of the CI/CD workflow, ensuring security best practices while enabling automated infrastructure management.
Modern CI/CD pipelines require secure access to AWS resources without storing long-term credentials. Traditional approaches using access keys create security risks and operational overhead. Organizations need a solution that:
- Eliminates the need for long-term AWS credentials in CI/CD systems
- Provides granular access control based on repository, branch, environment, and tag context
- Supports multiple source control providers (GitHub, GitLab)
- Enables secure cross-repository state sharing for complex infrastructure
- Implements least-privilege access with permission boundaries
This module provides a complete OIDC-based authentication solution that creates three distinct IAM roles:
- Read-Only Role: For Terraform plan operations and validation
- Read-Write Role: For Terraform apply operations with branch/environment/tag protection
- State Reader Role: For cross-repository Terraform state access
- GitHub Actions: Native support with
token.actions.githubusercontent.com - GitLab CI/CD: Integration with GitLab's OIDC provider
- Custom Providers: Flexible configuration for any OIDC-compliant provider
- Permission Boundaries: Mandatory permission boundary enforcement
- Conditional Access: Branch, environment, and tag-based access restrictions
- Audience Validation: Strict OIDC audience verification
- Subject Mapping: Configurable subject claim mapping for different providers
- Read-Only Role: Safe for pull request validation and planning (optional, enabled by default)
- Read-Write Role: Protected by branch/environment/tag conditions
- State Reader Role: Enables cross-repository state sharing
- S3 Integration: Automatic S3 bucket permissions for state storage
- DynamoDB Locking: DynamoDB table access for state locking
- Namespace Support: Optional entire namespace access for complex deployments
- Cross-Repository Sharing: Secure state access across multiple repositories
- State File Naming: The state file naming follows a specific pattern:
- Multiple Repositories: When the
repositoriesvariable contains additional repositories (multiple repositories sharing the same role), the state file key usesvar.name(the IAM role name) - Single Repository: When only a single repository is configured, the state file key uses the repository name (extracted from
var.repository) - State File Format:
<account-id>-<region>-tfstate/<state-key><suffix>.tfstate
- Multiple Repositories: When the
- Custom Policies: Support for both inline and managed policy attachments
- Session Duration: Configurable maximum session duration per role
- Repository UUIDs: Support for workspace and repository UUID validation
- State Suffixes: Flexible state file naming with custom suffixes
- Branch Protection: Read-write access limited to specific branches
- Environment Protection: Environment-specific access controls
- Tag Protection: Release and tag-based access restrictions
- Shared Repositories: Cross-repository state access for shared infrastructure
graph TB
subgraph "CI/CD Pipeline"
PR[Pull Request]
MAIN[Merge to Main]
SHARED[Shared Infrastructure]
end
subgraph "AWS IAM Roles"
RO[Read-Only Role<br/>- Terraform Plan<br/>- State Read<br/>- Validation]
RW[Read-Write Role<br/>- Terraform Apply<br/>- State Write<br/>- Protected by Branch/Env/Tag]
SR[State Reader Role<br/>- Cross-Repo State Access<br/>- Read-Only State Access]
end
subgraph "AWS Resources"
S3[S3 State Bucket]
DDB[DynamoDB Lock Table]
RESOURCES[AWS Resources]
end
PR --> RO
MAIN --> RW
SHARED --> SR
RO --> S3
RO --> DDB
RW --> S3
RW --> DDB
RW --> RESOURCES
SR --> S3
sequenceDiagram
participant CI as CI/CD System
participant OIDC as OIDC Provider
participant AWS as AWS STS
participant IAM as IAM Role
CI->>OIDC: Request JWT Token
OIDC->>CI: Return JWT with Claims
CI->>AWS: AssumeRoleWithWebIdentity
AWS->>AWS: Validate JWT & Conditions
AWS->>IAM: Check Trust Policy
IAM->>AWS: Return Role Credentials
AWS->>CI: Return Temporary Credentials
| Provider | OIDC URL | Audience | Subject Mapping |
|---|---|---|---|
| GitHub | https://token.actions.githubusercontent.com |
sts.amazonaws.com |
repo:{repo}:* |
| GitLab | https://gitlab.com |
https://gitlab.com |
project_path:{repo}:* |
This example demonstrates the basic setup for GitHub Actions with default configuration. It creates three IAM roles with appropriate permissions for Terraform state management and applies a permission boundary for security.
module "terraform_roles" {
source = "appvia/oidc/aws//modules/role"
name = "terraform-ci-cd"
description = "IAM roles for Terraform CI/CD pipeline"
repository = "my-org/my-terraform-repo"
# Permission boundary for security
permission_boundary = "TerraformExecutionBoundary"
# Default policies for both roles
default_managed_policies = [
"arn:aws:iam::aws:policy/ReadOnlyAccess"
]
# Read-write specific policies
read_write_policy_arns = [
"arn:aws:iam::aws:policy/PowerUserAccess"
]
tags = {
Environment = "production"
Project = "infrastructure"
Owner = "platform-team"
}
}This example shows how to configure different access levels for different environments and branches, with custom inline policies and enhanced security controls.
module "terraform_roles_advanced" {
source = "appvia/oidc/aws//modules/role"
name = "terraform-multi-env"
description = "Advanced IAM roles for multi-environment Terraform"
repository = "my-org/infrastructure"
# Enhanced protection controls
protected_by = {
branch = "main"
environment = "production"
tag = "v*"
}
# Custom permission boundary
permission_boundary_arn = "arn:aws:iam::123456789012:policy/CustomTerraformBoundary"
# Custom inline policies for read-only role
read_only_inline_policies = {
"S3SpecificAccess" = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"s3:GetObject",
"s3:ListBucket"
]
Resource = [
"arn:aws:s3:::my-specific-bucket",
"arn:aws:s3:::my-specific-bucket/*"
]
}
]
})
}
# Custom inline policies for read-write role
read_write_inline_policies = {
"EC2Management" = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"ec2:*",
"ec2-instance-connect:*"
]
Resource = "*"
}
]
})
}
# Session duration limits
read_only_max_session_duration = 3600 # 1 hour
read_write_max_session_duration = 7200 # 2 hours
# State management configuration
tf_state_suffix = "prod"
enable_key_namespace = true
tags = {
Environment = "production"
Project = "infrastructure"
Owner = "platform-team"
Compliance = "required"
}
}This example demonstrates how to enable cross-repository state sharing for shared infrastructure components.
module "shared_infrastructure_roles" {
source = "appvia/oidc/aws//modules/role"
name = "shared-infrastructure"
description = "Roles for shared infrastructure state access"
repository = "my-org/shared-infrastructure"
# Enable cross-repository state sharing
shared_repositories = [
"my-org/app1",
"my-org/app2",
"my-org/app3"
]
# State reader role gets read access to shared state
read_only_policy_arns = [
"arn:aws:iam::aws:policy/ReadOnlyAccess"
]
tags = {
Environment = "shared"
Project = "infrastructure"
Owner = "platform-team"
}
}Note on State File Naming: When the repositories variable contains additional repositories (multiple repositories using the same role), the state file key will use the name parameter (e.g., shared-infrastructure) instead of the repository name. When only a single repository is configured (using only repository), the state file key uses the repository name. This ensures consistent state file naming when multiple repositories share the same IAM role.
This example shows how to configure a custom OIDC provider for non-standard CI/CD systems.
module "custom_oidc_roles" {
source = "appvia/oidc/aws//modules/role"
name = "custom-ci-cd"
description = "Roles for custom CI/CD system"
repository = "my-org/custom-pipeline"
# Custom OIDC provider configuration
custom_provider = {
url = "https://my-custom-oidc-provider.com"
audiences = ["my-custom-audience"]
subject_reader_mapping = "repo:{repo}:*"
subject_branch_mapping = "repo:{repo}:ref:refs/heads/{ref}"
subject_env_mapping = "repo:{repo}:environment:{env}"
subject_tag_mapping = "repo:{repo}:ref:refs/tags/{ref}"
}
# Additional audiences
additional_audiences = [
"sts.amazonaws.com",
"my-backup-audience"
]
tags = {
Environment = "production"
Project = "custom-pipeline"
Owner = "platform-team"
}
}This example shows how to disable the read-only role when you only need a read-write role (e.g., for simplified CI/CD pipelines where plan and apply happen in the same protected workflow).
module "terraform_roles_rw_only" {
source = "appvia/oidc/aws//modules/role"
name = "terraform-rw-only"
description = "IAM read-write role only for protected deployments"
repository = "my-org/my-terraform-repo"
# Disable read-only role creation
enable_read_only_role = false
# Protection controls - only allow from main branch
protected_by = {
branch = "main"
}
# Permission boundary for security
permission_boundary = "TerraformExecutionBoundary"
# Read-write policies
read_write_policy_arns = [
"arn:aws:iam::aws:policy/PowerUserAccess"
]
tags = {
Environment = "production"
Project = "infrastructure"
Owner = "platform-team"
}
}Scenario: A typical CI/CD pipeline with pull request validation and main branch deployment.
Configuration:
- Read-only role for PR validation and planning
- Read-write role protected by main branch
- Standard GitHub Actions integration
Benefits:
- Secure credential-free authentication
- Automatic branch-based access control
- Standard Terraform state management
Scenario: Complex infrastructure with multiple environments (dev, staging, production).
Configuration:
- Environment-specific protection controls
- Custom policies per environment
- Cross-repository state sharing
Benefits:
- Environment isolation
- Flexible access controls
- Shared infrastructure components
Scenario: Multiple applications sharing common infrastructure components.
Configuration:
- State reader role for cross-repository access
- Shared repository configuration
- Namespace-based state access
Benefits:
- Centralized infrastructure management
- Secure cross-repository access
- Reduced duplication
Scenario: Organizations with strict compliance requirements and audit trails.
Configuration:
- Mandatory permission boundaries
- Custom session duration limits
- Enhanced tagging and monitoring
Benefits:
- Compliance with security standards
- Audit trail capabilities
- Risk mitigation
The module implements secure OIDC trust relationships with the following security features:
- JWT Validation: Strict validation of JWT tokens from OIDC providers
- Audience Verification: Ensures tokens are intended for AWS
- Subject Claim Validation: Validates repository, branch, environment, and tag claims
- Conditional Access: Branch, environment, and tag-based access restrictions
All roles support permission boundaries to prevent privilege escalation:
# Example permission boundary policy
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "*",
"Resource": "*",
"Condition": {
"StringNotEquals": {
"aws:RequestedRegion": ["us-east-1"]
}
}
}
]
}The module implements least privilege principles:
- Read-Only Role: Only S3 read and DynamoDB read permissions
- Read-Write Role: S3 read/write and DynamoDB read/write permissions
- State Reader Role: Cross-repository read-only state access
Built-in audit capabilities:
- CloudTrail Integration: All role assumptions are logged
- Resource Tagging: Comprehensive tagging for cost and compliance tracking
- Session Duration Limits: Configurable maximum session durations
The read-only role includes the following permissions:
- S3 State Access:
s3:GetObject,s3:ListBucketfor Terraform state files - DynamoDB Locking:
dynamodb:GetItem,dynamodb:DescribeTablefor state locking - Custom Policies: Additional read-only policies as configured
The read-write role includes the following permissions:
- S3 State Management: Full S3 access for state file management
- DynamoDB Locking: Full DynamoDB access for state locking
- Custom Policies: Additional read-write policies as configured
The state reader role includes the following permissions:
- Cross-Repository State Access: Read-only access to shared state files
- S3 State Access:
s3:GetObject,s3:ListBucketfor shared state files
The terraform-docs utility is used to generate this README. Follow the below steps to update:
- Make changes to the
.terraform-docs.ymlfile - Fetch the
terraform-docsbinary (https://terraform-docs.io/user-guide/installation/) - Run
terraform-docs markdown table --output-file ${PWD}/README.md --output-mode inject .
| Name | Version |
|---|---|
| terraform | >= 1.0 |
| Name | Version |
|---|---|
| aws | n/a |
No modules.
| Name | Type |
|---|---|
| aws_iam_policy.tfstate_apply | resource |
| aws_iam_policy.tfstate_plan | resource |
| aws_iam_policy.tfstate_remote | resource |
| aws_iam_role.ro | resource |
| aws_iam_role.rw | resource |
| aws_iam_role.sr | resource |
| aws_iam_role_policy_attachment.ro | resource |
| aws_iam_role_policy_attachment.rw | resource |
| aws_iam_role_policy_attachment.tfstate_apply | resource |
| aws_iam_role_policy_attachment.tfstate_plan | resource |
| aws_iam_role_policy_attachment.tfstate_remote | resource |
| aws_caller_identity.current | data source |
| aws_iam_openid_connect_provider.this | data source |
| aws_iam_policy_document.base | data source |
| aws_iam_policy_document.dynamo | data source |
| aws_iam_policy_document.ro | data source |
| aws_iam_policy_document.rw | data source |
| aws_iam_policy_document.sr | data source |
| aws_iam_policy_document.tfstate_apply | data source |
| aws_iam_policy_document.tfstate_plan | data source |
| aws_iam_policy_document.tfstate_remote | data source |
| aws_region.current | data source |
| Name | Description | Type | Default | Required |
|---|---|---|---|---|
| additional_audiences | Additional audiences to be allowed in the OIDC federation mapping | list(string) |
[] |
no |
| common_provider | The name of a common OIDC provider to be used as the trust for the role | string |
"" |
no |
| custom_provider | An object representing an aws_iam_openid_connect_provider resource |
object({ |
null |
no |
| description | Description of the role being created | string |
n/a | yes |
| force_detach_policies | Flag to force detachment of policies attached to the IAM role. | bool |
null |
no |
| name | Name of the role to create | string |
n/a | yes |
| permission_boundary_arn | The ARN of the policy that is used to set the permissions boundary for the IAM role | string |
null |
no |
| protected_branch | The name of the protected branch under which the read-write role can be assumed | string |
"main" |
no |
| protected_tag | The name of the protected tag under which the read-write role can be assume | string |
"*" |
no |
| read_only_inline_policies | Inline policies map with policy name as key and json as value. | map(string) |
{} |
no |
| read_only_max_session_duration | The maximum session duration (in seconds) that you want to set for the specified role | number |
null |
no |
| read_only_policy_arns | List of IAM policy ARNs to attach to the read-only role | list(string) |
[] |
no |
| read_write_inline_policies | Inline policies map with policy name as key and json as value. | map(string) |
{} |
no |
| read_write_max_session_duration | The maximum session duration (in seconds) that you want to set for the specified role | number |
null |
no |
| read_write_policy_arns | List of IAM policy ARNs to attach to the read-write role | list(string) |
[] |
no |
| repository | List of repositories to be allowed i nthe OIDC federation mapping | string |
n/a | yes |
| role_path | Path under which to create IAM role. | string |
null |
no |
| shared_repositories | List of repositories to provide read access to the remote state | list(string) |
[] |
no |
| tags | Tags to apply resoures created by this module | map(string) |
{} |
no |
| Name | Description |
|---|---|
| read_only | n/a |
| read_write | n/a |
| state_reader | n/a |
| Name | Version |
|---|---|
| aws | n/a |
| Name | Description | Type | Default | Required |
|---|---|---|---|---|
| description | Description of the role being created | string |
n/a | yes |
| name | Name of the role to create | string |
n/a | yes |
| tags | Tags to apply resources created by this module | map(string) |
n/a | yes |
| account_id | The AWS account ID to create the role in | string |
null |
no |
| additional_audiences | Additional audiences to be allowed in the OIDC federation mapping | list(string) |
[] |
no |
| common_provider | The name of a common OIDC provider to be used as the trust for the role | string |
"github" |
no |
| custom_provider | An object representing an aws_iam_openid_connect_provider resource |
object({ |
null |
no |
| default_inline_policies | Inline policies map with policy name as key and json as value, attached to both read-only and read-write roles | map(string) |
{} |
no |
| default_managed_policies | List of IAM managed policy ARNs to attach to this role/s, both read-only and read-write | list(string) |
[] |
no |
| enable_key_namespace | Amended the S3 permissions to write to entire key space i.e <REPOSITORY_NAME>/* | bool |
false |
no |
| enable_read_only_role | Indicates we should create a read-only role in addition to the read-write role | bool |
true |
no |
| enable_terraform_state | Indicates we should create the terraform state and lock file permissions | bool |
true |
no |
| force_detach_policies | Flag to force detachment of policies attached to the IAM role. | bool |
true |
no |
| permission_boundary | The name of the policy that is used to set the permissions boundary for the IAM role | string |
null |
no |
| permission_boundary_arn | The full ARN of the permission boundary to attach to the role | string |
null |
no |
| protected_by | The branch, environment and/or tag to protect read write role (used when enable_read_only_role is true) | object({ |
{ |
no |
| read_only_inline_policies | Inline policies map with policy name as key and json as value. | map(string) |
{} |
no |
| read_only_max_session_duration | The maximum session duration (in seconds) that you want to set for the specified role | number |
null |
no |
| read_only_policy_arns | List of IAM policy ARNs to attach to the read-only role | list(string) |
[] |
no |
| read_write_inline_policies | Inline policies map with policy name as key and json as value. | map(string) |
{} |
no |
| read_write_max_session_duration | The maximum session duration (in seconds) that you want to set for the specified role | number |
null |
no |
| read_write_policy_arns | List of IAM policy ARNs to attach to the read-write role | list(string) |
[] |
no |
| region | The region in which the role will be used (defaulting to the provider region) | string |
null |
no |
| repositories | A collection of repositories to bind the permissions (if empty, the repository variable is used) | list(string) |
[] |
no |
| repository | Repository to be allowed in the OIDC federation mapping (used when repositories variable is not set) | string |
null |
no |
| role_path | Path under which to create IAM role. | string |
"/" |
no |
| shared_repositories | List of repositories to provide read access to the terraform remote state | list(string) |
[] |
no |
| tf_state_suffix | A suffix for the terraform state file, e.g. -<tf_state_suffix>.tfstate | string |
"" |
no |
| Name | Description |
|---|---|
| read_only | The ARN of the IAM read-only role |
| read_write | The ARN of the IAM read-write role |
| state_reader | The ARN of the IAM state reader role |