mirror of
https://github.com/ghndrx/terraform-foundation.git
synced 2026-02-10 06:45:06 +00:00
feat: Terraform Foundation - AWS Landing Zone
Enterprise-grade multi-tenant AWS cloud foundation. Modules: - GitHub OIDC for keyless CI/CD authentication - IAM account settings and security baseline - AWS Config Rules for compliance - ABAC (Attribute-Based Access Control) - SCPs (Service Control Policies) Features: - Multi-account architecture - Cost optimization patterns - Security best practices - Comprehensive documentation Tech: Terraform, AWS Organizations, IAM Identity Center
This commit is contained in:
229
terraform/modules/github-oidc/README.md
Normal file
229
terraform/modules/github-oidc/README.md
Normal file
@@ -0,0 +1,229 @@
|
||||
# GitHub OIDC Module
|
||||
|
||||
Secure CI/CD access from GitHub Actions to AWS without long-lived credentials.
|
||||
|
||||
## Features
|
||||
|
||||
- 🔐 **OIDC Provider** - Automatic setup of GitHub OIDC trust
|
||||
- 🎯 **Fine-grained access** - Restrict by repo, branch, tag, environment
|
||||
- 📦 **Pre-built templates** - Common patterns for Terraform, ECR, S3, Lambda
|
||||
- 🔧 **Custom roles** - Full flexibility for any use case
|
||||
- 📝 **Policy generation** - Build policies from simple statements
|
||||
|
||||
## Quick Start
|
||||
|
||||
```hcl
|
||||
module "github_oidc" {
|
||||
source = "../modules/github-oidc"
|
||||
|
||||
github_org = "myorg"
|
||||
|
||||
# Custom role
|
||||
roles = {
|
||||
deploy = {
|
||||
repos = ["myrepo"]
|
||||
branches = ["main"]
|
||||
policy_arns = ["arn:aws:iam::aws:policy/PowerUserAccess"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Pre-built Templates
|
||||
|
||||
### Terraform Deployments
|
||||
|
||||
```hcl
|
||||
module "github_oidc" {
|
||||
source = "../modules/github-oidc"
|
||||
|
||||
github_org = "myorg"
|
||||
|
||||
terraform_deploy_role = {
|
||||
enabled = true
|
||||
repos = ["infrastructure"]
|
||||
branches = ["main"]
|
||||
environments = ["production"]
|
||||
state_bucket = "myorg-tf-state"
|
||||
dynamodb_table = "terraform-locks"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### ECR Push
|
||||
|
||||
```hcl
|
||||
module "github_oidc" {
|
||||
source = "../modules/github-oidc"
|
||||
|
||||
github_org = "myorg"
|
||||
|
||||
ecr_push_role = {
|
||||
enabled = true
|
||||
repos = ["backend", "frontend"]
|
||||
branches = ["main", "develop"]
|
||||
ecr_repos = ["backend", "frontend"]
|
||||
allow_create = false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### S3 Static Site Deploy
|
||||
|
||||
```hcl
|
||||
module "github_oidc" {
|
||||
source = "../modules/github-oidc"
|
||||
|
||||
github_org = "myorg"
|
||||
|
||||
s3_deploy_role = {
|
||||
enabled = true
|
||||
repos = ["website"]
|
||||
branches = ["main"]
|
||||
bucket_arns = ["arn:aws:s3:::mysite.com"]
|
||||
cloudfront_arns = ["arn:aws:cloudfront::123456789012:distribution/EXAMPLE"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Lambda Deploy
|
||||
|
||||
```hcl
|
||||
module "github_oidc" {
|
||||
source = "../modules/github-oidc"
|
||||
|
||||
github_org = "myorg"
|
||||
|
||||
lambda_deploy_role = {
|
||||
enabled = true
|
||||
repos = ["serverless-api"]
|
||||
branches = ["main"]
|
||||
function_arns = ["arn:aws:lambda:us-east-1:123456789012:function:my-api"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
### Multiple Custom Roles
|
||||
|
||||
```hcl
|
||||
module "github_oidc" {
|
||||
source = "../modules/github-oidc"
|
||||
|
||||
github_org = "myorg"
|
||||
|
||||
roles = {
|
||||
# Read-only for PRs
|
||||
preview = {
|
||||
repos = ["webapp"]
|
||||
pull_request = true
|
||||
policy_arns = ["arn:aws:iam::aws:policy/ReadOnlyAccess"]
|
||||
}
|
||||
|
||||
# Full deploy for main
|
||||
deploy = {
|
||||
repos = ["webapp"]
|
||||
branches = ["main"]
|
||||
policy_arns = ["arn:aws:iam::aws:policy/PowerUserAccess"]
|
||||
}
|
||||
|
||||
# Tag-based releases
|
||||
release = {
|
||||
repos = ["webapp"]
|
||||
tags = ["v*"]
|
||||
policy_statements = [{
|
||||
actions = ["s3:PutObject", "cloudfront:CreateInvalidation"]
|
||||
resources = ["*"]
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Reusable Workflow Restriction
|
||||
|
||||
```hcl
|
||||
roles = {
|
||||
deploy = {
|
||||
repos = ["*"] # Any repo in org
|
||||
workflow_ref = "myorg/workflows/.github/workflows/deploy.yml@main"
|
||||
policy_arns = ["arn:aws:iam::aws:policy/PowerUserAccess"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Trust Conditions
|
||||
|
||||
```hcl
|
||||
roles = {
|
||||
restricted = {
|
||||
repos = ["myrepo"]
|
||||
branches = ["main"]
|
||||
extra_conditions = {
|
||||
StringEquals = {
|
||||
"token.actions.githubusercontent.com:actor" = ["trusted-user"]
|
||||
}
|
||||
}
|
||||
policy_arns = ["arn:aws:iam::aws:policy/AdministratorAccess"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## GitHub Actions Workflow
|
||||
|
||||
```yaml
|
||||
name: Deploy
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
id-token: write # Required for OIDC
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: aws-actions/configure-aws-credentials@v4
|
||||
with:
|
||||
role-to-assume: arn:aws:iam::123456789012:role/github-deploy
|
||||
aws-region: us-east-1
|
||||
|
||||
- run: aws sts get-caller-identity
|
||||
```
|
||||
|
||||
## Inputs
|
||||
|
||||
| Name | Description | Type | Default |
|
||||
|------|-------------|------|---------|
|
||||
| `create_provider` | Create OIDC provider | `bool` | `true` |
|
||||
| `provider_arn` | Existing provider ARN | `string` | `""` |
|
||||
| `github_org` | GitHub organization | `string` | `""` |
|
||||
| `name_prefix` | Role name prefix | `string` | `"github"` |
|
||||
| `roles` | Custom role configs | `map(object)` | `{}` |
|
||||
| `terraform_deploy_role` | Terraform template | `object` | `{}` |
|
||||
| `ecr_push_role` | ECR template | `object` | `{}` |
|
||||
| `s3_deploy_role` | S3 template | `object` | `{}` |
|
||||
| `lambda_deploy_role` | Lambda template | `object` | `{}` |
|
||||
|
||||
## Outputs
|
||||
|
||||
| Name | Description |
|
||||
|------|-------------|
|
||||
| `provider_arn` | OIDC provider ARN |
|
||||
| `role_arns` | Map of custom role ARNs |
|
||||
| `all_role_arns` | All role ARNs (custom + templates) |
|
||||
| `terraform_role_arn` | Terraform role ARN |
|
||||
| `ecr_role_arn` | ECR role ARN |
|
||||
| `workflow_examples` | Example workflow snippets |
|
||||
|
||||
## Security Considerations
|
||||
|
||||
1. **Principle of least privilege** - Use specific repos/branches, not wildcards
|
||||
2. **Environment protection** - Use GitHub environments for production
|
||||
3. **Permissions boundary** - Consider attaching a boundary for defense-in-depth
|
||||
4. **Audit** - CloudTrail logs all AssumeRoleWithWebIdentity calls
|
||||
54
terraform/modules/github-oidc/examples/basic/main.tf
Normal file
54
terraform/modules/github-oidc/examples/basic/main.tf
Normal file
@@ -0,0 +1,54 @@
|
||||
################################################################################
|
||||
# GitHub OIDC - Basic Example
|
||||
#
|
||||
# Single role with branch restriction
|
||||
################################################################################
|
||||
|
||||
terraform {
|
||||
required_version = ">= 1.5.0"
|
||||
|
||||
required_providers {
|
||||
aws = {
|
||||
source = "hashicorp/aws"
|
||||
version = ">= 5.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
provider "aws" {
|
||||
region = "us-east-1"
|
||||
}
|
||||
|
||||
module "github_oidc" {
|
||||
source = "../../"
|
||||
|
||||
github_org = "example-org"
|
||||
name_prefix = "github"
|
||||
|
||||
roles = {
|
||||
deploy = {
|
||||
repos = ["my-app"]
|
||||
branches = ["main"]
|
||||
policy_statements = [
|
||||
{
|
||||
sid = "S3Access"
|
||||
actions = ["s3:GetObject", "s3:PutObject"]
|
||||
resources = ["arn:aws:s3:::my-bucket/*"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
tags = {
|
||||
Environment = "production"
|
||||
Project = "my-app"
|
||||
}
|
||||
}
|
||||
|
||||
output "role_arn" {
|
||||
value = module.github_oidc.role_arns["deploy"]
|
||||
}
|
||||
|
||||
output "provider_arn" {
|
||||
value = module.github_oidc.provider_arn
|
||||
}
|
||||
126
terraform/modules/github-oidc/examples/multi-role/main.tf
Normal file
126
terraform/modules/github-oidc/examples/multi-role/main.tf
Normal file
@@ -0,0 +1,126 @@
|
||||
################################################################################
|
||||
# GitHub OIDC - Multi-Role Example
|
||||
#
|
||||
# Multiple roles with different permission levels
|
||||
################################################################################
|
||||
|
||||
terraform {
|
||||
required_version = ">= 1.5.0"
|
||||
|
||||
required_providers {
|
||||
aws = {
|
||||
source = "hashicorp/aws"
|
||||
version = ">= 5.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
provider "aws" {
|
||||
region = "us-east-1"
|
||||
}
|
||||
|
||||
# Permissions boundary for defense-in-depth
|
||||
resource "aws_iam_policy" "github_boundary" {
|
||||
name = "GitHubActionsBoundary"
|
||||
description = "Permissions boundary for GitHub Actions roles"
|
||||
|
||||
policy = jsonencode({
|
||||
Version = "2012-10-17"
|
||||
Statement = [
|
||||
{
|
||||
Sid = "AllowedServices"
|
||||
Effect = "Allow"
|
||||
Action = ["s3:*", "ecr:*", "lambda:*", "logs:*", "cloudwatch:*"]
|
||||
Resource = "*"
|
||||
},
|
||||
{
|
||||
Sid = "DenyDangerous"
|
||||
Effect = "Deny"
|
||||
Action = [
|
||||
"iam:CreateUser",
|
||||
"iam:CreateAccessKey",
|
||||
"organizations:*",
|
||||
"account:*"
|
||||
]
|
||||
Resource = "*"
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
module "github_oidc" {
|
||||
source = "../../"
|
||||
|
||||
github_org = "example-org"
|
||||
name_prefix = "github"
|
||||
permissions_boundary = aws_iam_policy.github_boundary.arn
|
||||
|
||||
# Security settings
|
||||
max_session_hours_limit = 2
|
||||
deny_wildcard_repos = true
|
||||
|
||||
roles = {
|
||||
# Read-only for PR validation
|
||||
validate = {
|
||||
repos = ["infrastructure", "application"]
|
||||
pull_request = true
|
||||
policy_arns = ["arn:aws:iam::aws:policy/ReadOnlyAccess"]
|
||||
max_session_hours = 1
|
||||
}
|
||||
|
||||
# Deploy from main branch only
|
||||
deploy = {
|
||||
repos = ["infrastructure"]
|
||||
branches = ["main"]
|
||||
environments = ["production"]
|
||||
policy_statements = [
|
||||
{
|
||||
sid = "DeployAccess"
|
||||
actions = ["s3:*", "cloudfront:*", "lambda:*"]
|
||||
resources = ["*"]
|
||||
}
|
||||
]
|
||||
max_session_hours = 2
|
||||
}
|
||||
|
||||
# Release automation from tags
|
||||
release = {
|
||||
repos = ["application"]
|
||||
tags = ["v*", "release-*"]
|
||||
branches = [] # Only tags
|
||||
policy_statements = [
|
||||
{
|
||||
sid = "ECRPush"
|
||||
actions = ["ecr:*"]
|
||||
resources = ["arn:aws:ecr:*:*:repository/application"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
# Reusable workflow restriction
|
||||
shared = {
|
||||
repos = ["*"] # Any repo
|
||||
workflow_ref = "example-org/shared-workflows/.github/workflows/deploy.yml@main"
|
||||
policy_statements = [
|
||||
{
|
||||
sid = "SharedDeploy"
|
||||
actions = ["s3:PutObject"]
|
||||
resources = ["arn:aws:s3:::artifacts-bucket/*"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
tags = {
|
||||
Environment = "production"
|
||||
CostCenter = "platform"
|
||||
}
|
||||
}
|
||||
|
||||
output "all_roles" {
|
||||
value = module.github_oidc.all_role_arns
|
||||
}
|
||||
|
||||
output "security_status" {
|
||||
value = module.github_oidc.security_recommendations
|
||||
}
|
||||
159
terraform/modules/github-oidc/examples/templates/main.tf
Normal file
159
terraform/modules/github-oidc/examples/templates/main.tf
Normal file
@@ -0,0 +1,159 @@
|
||||
################################################################################
|
||||
# GitHub OIDC - Pre-built Templates Example
|
||||
#
|
||||
# Using pre-built role templates for common patterns
|
||||
################################################################################
|
||||
|
||||
terraform {
|
||||
required_version = ">= 1.5.0"
|
||||
|
||||
required_providers {
|
||||
aws = {
|
||||
source = "hashicorp/aws"
|
||||
version = ">= 5.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
provider "aws" {
|
||||
region = "us-east-1"
|
||||
}
|
||||
|
||||
data "aws_caller_identity" "current" {}
|
||||
|
||||
# Prerequisites - S3 bucket for Terraform state
|
||||
resource "aws_s3_bucket" "terraform_state" {
|
||||
bucket_prefix = "terraform-state-"
|
||||
force_destroy = true # For example only - remove in production
|
||||
|
||||
tags = {
|
||||
Purpose = "terraform-state"
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_s3_bucket_versioning" "terraform_state" {
|
||||
bucket = aws_s3_bucket.terraform_state.id
|
||||
versioning_configuration {
|
||||
status = "Enabled"
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_dynamodb_table" "terraform_locks" {
|
||||
name = "terraform-locks"
|
||||
billing_mode = "PAY_PER_REQUEST"
|
||||
hash_key = "LockID"
|
||||
|
||||
attribute {
|
||||
name = "LockID"
|
||||
type = "S"
|
||||
}
|
||||
|
||||
tags = {
|
||||
Purpose = "terraform-locks"
|
||||
}
|
||||
}
|
||||
|
||||
# ECR repository for container builds
|
||||
resource "aws_ecr_repository" "app" {
|
||||
name = "my-application"
|
||||
image_tag_mutability = "IMMUTABLE"
|
||||
|
||||
image_scanning_configuration {
|
||||
scan_on_push = true
|
||||
}
|
||||
|
||||
tags = {
|
||||
Purpose = "container-registry"
|
||||
}
|
||||
}
|
||||
|
||||
# GitHub OIDC with all templates enabled
|
||||
module "github_oidc" {
|
||||
source = "../../"
|
||||
|
||||
github_org = "example-org"
|
||||
name_prefix = "github"
|
||||
|
||||
# Terraform deployment role
|
||||
terraform_deploy_role = {
|
||||
enabled = true
|
||||
repos = ["infrastructure"]
|
||||
branches = ["main"]
|
||||
environments = ["production"]
|
||||
state_bucket = aws_s3_bucket.terraform_state.id
|
||||
state_bucket_key_prefix = "live/*"
|
||||
dynamodb_table = aws_dynamodb_table.terraform_locks.name
|
||||
allowed_services = ["ec2", "s3", "iam", "lambda", "rds", "vpc"]
|
||||
denied_actions = [
|
||||
"iam:CreateUser",
|
||||
"iam:CreateAccessKey",
|
||||
"organizations:*"
|
||||
]
|
||||
}
|
||||
|
||||
# ECR push role for container builds
|
||||
ecr_push_role = {
|
||||
enabled = true
|
||||
repos = ["my-application", "backend-api"]
|
||||
branches = ["main", "develop"]
|
||||
ecr_repos = [aws_ecr_repository.app.name]
|
||||
allow_create = false
|
||||
allow_delete = false
|
||||
}
|
||||
|
||||
# S3 deploy role for static sites
|
||||
s3_deploy_role = {
|
||||
enabled = true
|
||||
repos = ["frontend"]
|
||||
branches = ["main"]
|
||||
bucket_arns = ["arn:aws:s3:::www.example.com"]
|
||||
allowed_prefixes = ["*"]
|
||||
cloudfront_arns = [] # Add CloudFront distribution ARN if needed
|
||||
}
|
||||
|
||||
# Lambda deploy role for serverless
|
||||
lambda_deploy_role = {
|
||||
enabled = true
|
||||
repos = ["serverless-api"]
|
||||
branches = ["main"]
|
||||
function_arns = ["arn:aws:lambda:us-east-1:${data.aws_caller_identity.current.account_id}:function:api-*"]
|
||||
allow_create = false
|
||||
allow_logs = true
|
||||
}
|
||||
|
||||
tags = {
|
||||
Environment = "production"
|
||||
ManagedBy = "terraform"
|
||||
}
|
||||
}
|
||||
|
||||
# Outputs
|
||||
output "terraform_role_arn" {
|
||||
description = "Role ARN for Terraform deployments"
|
||||
value = module.github_oidc.terraform_role_arn
|
||||
}
|
||||
|
||||
output "ecr_role_arn" {
|
||||
description = "Role ARN for ECR push operations"
|
||||
value = module.github_oidc.ecr_role_arn
|
||||
}
|
||||
|
||||
output "s3_deploy_role_arn" {
|
||||
description = "Role ARN for S3 static site deployments"
|
||||
value = module.github_oidc.s3_deploy_role_arn
|
||||
}
|
||||
|
||||
output "lambda_deploy_role_arn" {
|
||||
description = "Role ARN for Lambda deployments"
|
||||
value = module.github_oidc.lambda_deploy_role_arn
|
||||
}
|
||||
|
||||
output "all_roles" {
|
||||
description = "All created role ARNs"
|
||||
value = module.github_oidc.all_role_arns
|
||||
}
|
||||
|
||||
output "workflow_examples" {
|
||||
description = "Example workflow snippets"
|
||||
value = module.github_oidc.workflow_examples
|
||||
}
|
||||
673
terraform/modules/github-oidc/main.tf
Normal file
673
terraform/modules/github-oidc/main.tf
Normal file
@@ -0,0 +1,673 @@
|
||||
################################################################################
|
||||
# GitHub OIDC Module
|
||||
#
|
||||
# AWS/Terraform/Security Best Practices:
|
||||
# - Least privilege IAM policies
|
||||
# - Input validation
|
||||
# - Explicit denies for dangerous actions
|
||||
# - Session duration limits
|
||||
# - CloudTrail monitoring integration
|
||||
# - Permissions boundary support
|
||||
# - No wildcard repos by default
|
||||
#
|
||||
# Security scanning: tfsec, checkov, tflint-aws
|
||||
################################################################################
|
||||
|
||||
data "aws_caller_identity" "current" {}
|
||||
data "aws_region" "current" {}
|
||||
data "aws_partition" "current" {}
|
||||
|
||||
################################################################################
|
||||
# Local Variables & Validation
|
||||
################################################################################
|
||||
|
||||
locals {
|
||||
account_id = data.aws_caller_identity.current.account_id
|
||||
region = data.aws_region.current.id
|
||||
partition = data.aws_partition.current.partition
|
||||
|
||||
# Validate permissions boundary requirement
|
||||
boundary_check = var.require_permissions_boundary && var.permissions_boundary == null ? tobool("Permissions boundary is required but not set") : true
|
||||
|
||||
# Normalize repo names with org prefix
|
||||
normalize_repo = { for k, v in var.roles : k => merge(v, {
|
||||
repos = [for repo in v.repos :
|
||||
!contains(split("/", repo), "/") && var.github_org != ""
|
||||
? "${var.github_org}/${repo}"
|
||||
: repo
|
||||
]
|
||||
# Cap session duration at limit
|
||||
max_session_hours = min(v.max_session_hours, var.max_session_hours_limit)
|
||||
})}
|
||||
|
||||
# Validate no wildcard repos unless workflow_ref is set
|
||||
wildcard_check = var.deny_wildcard_repos ? alltrue([
|
||||
for k, v in var.roles : !contains(v.repos, "*") || v.workflow_ref != ""
|
||||
]) : true
|
||||
|
||||
_ = local.wildcard_check ? true : tobool("Wildcard repos (*) require workflow_ref restriction or deny_wildcard_repos=false")
|
||||
|
||||
# Common tags
|
||||
common_tags = merge(var.tags, {
|
||||
ManagedBy = "terraform"
|
||||
Module = "github-oidc"
|
||||
})
|
||||
}
|
||||
|
||||
################################################################################
|
||||
# OIDC Provider
|
||||
################################################################################
|
||||
|
||||
data "tls_certificate" "github" {
|
||||
count = var.create_provider ? 1 : 0
|
||||
url = "https://token.actions.githubusercontent.com"
|
||||
}
|
||||
|
||||
resource "aws_iam_openid_connect_provider" "github" {
|
||||
count = var.create_provider ? 1 : 0
|
||||
|
||||
url = "https://token.actions.githubusercontent.com"
|
||||
client_id_list = ["sts.amazonaws.com"]
|
||||
thumbprint_list = [data.tls_certificate.github[0].certificates[0].sha1_fingerprint]
|
||||
|
||||
tags = merge(local.common_tags, {
|
||||
Name = "github-actions-oidc"
|
||||
Description = "GitHub Actions OIDC Identity Provider"
|
||||
})
|
||||
}
|
||||
|
||||
locals {
|
||||
provider_arn = var.create_provider ? aws_iam_openid_connect_provider.github[0].arn : var.provider_arn
|
||||
}
|
||||
|
||||
################################################################################
|
||||
# Custom Roles
|
||||
################################################################################
|
||||
|
||||
resource "aws_iam_role" "github" {
|
||||
for_each = local.normalize_repo
|
||||
|
||||
name = "${var.name_prefix}-${each.key}"
|
||||
path = var.path
|
||||
description = "GitHub Actions: ${join(", ", each.value.repos)}"
|
||||
max_session_duration = each.value.max_session_hours * 3600
|
||||
permissions_boundary = var.permissions_boundary
|
||||
|
||||
assume_role_policy = jsonencode({
|
||||
Version = "2012-10-17"
|
||||
Statement = [{
|
||||
Sid = "GitHubActionsOIDC"
|
||||
Effect = "Allow"
|
||||
Action = "sts:AssumeRoleWithWebIdentity"
|
||||
Principal = {
|
||||
Federated = local.provider_arn
|
||||
}
|
||||
Condition = merge(
|
||||
{
|
||||
StringEquals = {
|
||||
"token.actions.githubusercontent.com:aud" = "sts.amazonaws.com"
|
||||
}
|
||||
StringLike = {
|
||||
"token.actions.githubusercontent.com:sub" = distinct(compact(concat(
|
||||
# Branch-based subjects
|
||||
flatten([for repo in each.value.repos :
|
||||
length(each.value.branches) > 0
|
||||
? [for branch in each.value.branches : "repo:${repo}:ref:refs/heads/${branch}"]
|
||||
: length(each.value.tags) == 0 && length(each.value.environments) == 0 && !each.value.pull_request
|
||||
? ["repo:${repo}:*"]
|
||||
: []
|
||||
]),
|
||||
# Tag-based subjects
|
||||
flatten([for repo in each.value.repos :
|
||||
[for tag in each.value.tags : "repo:${repo}:ref:refs/tags/${tag}"]
|
||||
]),
|
||||
# Environment-based subjects
|
||||
flatten([for repo in each.value.repos :
|
||||
[for env in each.value.environments : "repo:${repo}:environment:${env}"]
|
||||
]),
|
||||
# Pull request subjects
|
||||
each.value.pull_request
|
||||
? [for repo in each.value.repos : "repo:${repo}:pull_request"]
|
||||
: []
|
||||
)))
|
||||
}
|
||||
},
|
||||
# Workflow ref condition (for reusable workflows)
|
||||
each.value.workflow_ref != "" ? {
|
||||
StringEquals = merge(
|
||||
{ "token.actions.githubusercontent.com:aud" = "sts.amazonaws.com" },
|
||||
{ "token.actions.githubusercontent.com:job_workflow_ref" = each.value.workflow_ref }
|
||||
)
|
||||
} : {},
|
||||
# Extra conditions
|
||||
each.value.extra_conditions
|
||||
)
|
||||
}]
|
||||
})
|
||||
|
||||
tags = merge(local.common_tags, {
|
||||
Name = "${var.name_prefix}-${each.key}"
|
||||
GitHubRepos = join(",", slice(each.value.repos, 0, min(5, length(each.value.repos))))
|
||||
Purpose = "github-actions-oidc"
|
||||
})
|
||||
}
|
||||
|
||||
# Managed policy attachments
|
||||
resource "aws_iam_role_policy_attachment" "github" {
|
||||
for_each = {
|
||||
for pair in flatten([
|
||||
for role_name, role in local.normalize_repo : [
|
||||
for policy_arn in role.policy_arns : {
|
||||
role_name = role_name
|
||||
policy_arn = policy_arn
|
||||
}
|
||||
]
|
||||
]) : "${pair.role_name}-${md5(pair.policy_arn)}" => pair
|
||||
}
|
||||
|
||||
role = aws_iam_role.github[each.value.role_name].name
|
||||
policy_arn = each.value.policy_arn
|
||||
}
|
||||
|
||||
# Inline policies (raw JSON)
|
||||
resource "aws_iam_role_policy" "github_inline" {
|
||||
for_each = { for k, v in local.normalize_repo : k => v if v.inline_policy != "" }
|
||||
|
||||
name = "inline"
|
||||
role = aws_iam_role.github[each.key].id
|
||||
policy = each.value.inline_policy
|
||||
}
|
||||
|
||||
# Generated policies from statements
|
||||
resource "aws_iam_role_policy" "github_generated" {
|
||||
for_each = { for k, v in local.normalize_repo : k => v if length(v.policy_statements) > 0 }
|
||||
|
||||
name = "generated"
|
||||
role = aws_iam_role.github[each.key].id
|
||||
policy = jsonencode({
|
||||
Version = "2012-10-17"
|
||||
Statement = [for stmt in each.value.policy_statements : {
|
||||
Sid = stmt.sid != "" ? stmt.sid : null
|
||||
Effect = stmt.effect
|
||||
Action = stmt.actions
|
||||
Resource = stmt.resources
|
||||
Condition = length(stmt.conditions) > 0 ? {
|
||||
for cond in stmt.conditions : cond.test => {
|
||||
"${cond.variable}" = cond.values
|
||||
}
|
||||
} : null
|
||||
}]
|
||||
})
|
||||
}
|
||||
|
||||
################################################################################
|
||||
# Terraform Deploy Role (Template)
|
||||
################################################################################
|
||||
|
||||
locals {
|
||||
tf_role_enabled = try(var.terraform_deploy_role.enabled, false)
|
||||
tf_repos = try(var.terraform_deploy_role.repos, [])
|
||||
tf_repos_normalized = [for repo in local.tf_repos :
|
||||
!contains(split("/", repo), "/") && var.github_org != ""
|
||||
? "${var.github_org}/${repo}"
|
||||
: repo
|
||||
]
|
||||
}
|
||||
|
||||
resource "aws_iam_role" "terraform" {
|
||||
count = local.tf_role_enabled ? 1 : 0
|
||||
|
||||
name = "${var.name_prefix}-terraform"
|
||||
path = var.path
|
||||
description = "GitHub Actions - Terraform deployment"
|
||||
max_session_duration = min(2, var.max_session_hours_limit) * 3600
|
||||
permissions_boundary = var.permissions_boundary
|
||||
|
||||
assume_role_policy = jsonencode({
|
||||
Version = "2012-10-17"
|
||||
Statement = [{
|
||||
Sid = "GitHubActionsTerraform"
|
||||
Effect = "Allow"
|
||||
Action = "sts:AssumeRoleWithWebIdentity"
|
||||
Principal = { Federated = local.provider_arn }
|
||||
Condition = {
|
||||
StringEquals = { "token.actions.githubusercontent.com:aud" = "sts.amazonaws.com" }
|
||||
StringLike = {
|
||||
"token.actions.githubusercontent.com:sub" = concat(
|
||||
flatten([for repo in local.tf_repos_normalized :
|
||||
length(try(var.terraform_deploy_role.branches, [])) > 0
|
||||
? [for branch in var.terraform_deploy_role.branches : "repo:${repo}:ref:refs/heads/${branch}"]
|
||||
: ["repo:${repo}:*"]
|
||||
]),
|
||||
flatten([for repo in local.tf_repos_normalized :
|
||||
[for env in try(var.terraform_deploy_role.environments, []) : "repo:${repo}:environment:${env}"]
|
||||
])
|
||||
)
|
||||
}
|
||||
}
|
||||
}]
|
||||
})
|
||||
|
||||
tags = merge(local.common_tags, {
|
||||
Name = "${var.name_prefix}-terraform"
|
||||
Purpose = "terraform-deployment"
|
||||
})
|
||||
}
|
||||
|
||||
resource "aws_iam_role_policy" "terraform_state" {
|
||||
count = local.tf_role_enabled && try(var.terraform_deploy_role.state_bucket, "") != "" ? 1 : 0
|
||||
|
||||
name = "terraform-state"
|
||||
role = aws_iam_role.terraform[0].id
|
||||
policy = jsonencode({
|
||||
Version = "2012-10-17"
|
||||
Statement = [
|
||||
{
|
||||
Sid = "TerraformStateBucket"
|
||||
Effect = "Allow"
|
||||
Action = [
|
||||
"s3:GetObject",
|
||||
"s3:PutObject",
|
||||
"s3:DeleteObject"
|
||||
]
|
||||
Resource = "arn:${local.partition}:s3:::${var.terraform_deploy_role.state_bucket}/${try(var.terraform_deploy_role.state_bucket_key_prefix, "*")}"
|
||||
},
|
||||
{
|
||||
Sid = "TerraformStateBucketList"
|
||||
Effect = "Allow"
|
||||
Action = ["s3:ListBucket"]
|
||||
Resource = "arn:${local.partition}:s3:::${var.terraform_deploy_role.state_bucket}"
|
||||
Condition = {
|
||||
StringLike = {
|
||||
"s3:prefix" = [try(var.terraform_deploy_role.state_bucket_key_prefix, "*")]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
resource "aws_iam_role_policy" "terraform_lock" {
|
||||
count = local.tf_role_enabled && try(var.terraform_deploy_role.dynamodb_table, "") != "" ? 1 : 0
|
||||
|
||||
name = "terraform-lock"
|
||||
role = aws_iam_role.terraform[0].id
|
||||
policy = jsonencode({
|
||||
Version = "2012-10-17"
|
||||
Statement = [{
|
||||
Sid = "TerraformLockTable"
|
||||
Effect = "Allow"
|
||||
Action = [
|
||||
"dynamodb:GetItem",
|
||||
"dynamodb:PutItem",
|
||||
"dynamodb:DeleteItem"
|
||||
]
|
||||
Resource = "arn:${local.partition}:dynamodb:*:${local.account_id}:table/${var.terraform_deploy_role.dynamodb_table}"
|
||||
}]
|
||||
})
|
||||
}
|
||||
|
||||
# Service-specific permissions (least privilege approach)
|
||||
resource "aws_iam_role_policy" "terraform_services" {
|
||||
count = local.tf_role_enabled && length(try(var.terraform_deploy_role.allowed_services, [])) > 0 ? 1 : 0
|
||||
|
||||
name = "terraform-services"
|
||||
role = aws_iam_role.terraform[0].id
|
||||
policy = jsonencode({
|
||||
Version = "2012-10-17"
|
||||
Statement = [{
|
||||
Sid = "AllowedServices"
|
||||
Effect = "Allow"
|
||||
Action = flatten([for svc in var.terraform_deploy_role.allowed_services : "${svc}:*"])
|
||||
Resource = "*"
|
||||
}]
|
||||
})
|
||||
}
|
||||
|
||||
# Explicit denies for dangerous actions
|
||||
resource "aws_iam_role_policy" "terraform_deny" {
|
||||
count = local.tf_role_enabled && length(try(var.terraform_deploy_role.denied_actions, [])) > 0 ? 1 : 0
|
||||
|
||||
name = "terraform-deny"
|
||||
role = aws_iam_role.terraform[0].id
|
||||
policy = jsonencode({
|
||||
Version = "2012-10-17"
|
||||
Statement = [{
|
||||
Sid = "ExplicitDeny"
|
||||
Effect = "Deny"
|
||||
Action = var.terraform_deploy_role.denied_actions
|
||||
Resource = "*"
|
||||
}]
|
||||
})
|
||||
}
|
||||
|
||||
################################################################################
|
||||
# ECR Push Role (Template)
|
||||
################################################################################
|
||||
|
||||
locals {
|
||||
ecr_role_enabled = try(var.ecr_push_role.enabled, false)
|
||||
ecr_repos_gh = try(var.ecr_push_role.repos, [])
|
||||
ecr_repos_normalized = [for repo in local.ecr_repos_gh :
|
||||
!contains(split("/", repo), "/") && var.github_org != ""
|
||||
? "${var.github_org}/${repo}"
|
||||
: repo
|
||||
]
|
||||
}
|
||||
|
||||
resource "aws_iam_role" "ecr" {
|
||||
count = local.ecr_role_enabled ? 1 : 0
|
||||
|
||||
name = "${var.name_prefix}-ecr-push"
|
||||
path = var.path
|
||||
description = "GitHub Actions - ECR push"
|
||||
max_session_duration = 3600
|
||||
permissions_boundary = var.permissions_boundary
|
||||
|
||||
assume_role_policy = jsonencode({
|
||||
Version = "2012-10-17"
|
||||
Statement = [{
|
||||
Sid = "GitHubActionsECR"
|
||||
Effect = "Allow"
|
||||
Action = "sts:AssumeRoleWithWebIdentity"
|
||||
Principal = { Federated = local.provider_arn }
|
||||
Condition = {
|
||||
StringEquals = { "token.actions.githubusercontent.com:aud" = "sts.amazonaws.com" }
|
||||
StringLike = {
|
||||
"token.actions.githubusercontent.com:sub" = flatten([for repo in local.ecr_repos_normalized :
|
||||
length(try(var.ecr_push_role.branches, [])) > 0
|
||||
? [for branch in var.ecr_push_role.branches : "repo:${repo}:ref:refs/heads/${branch}"]
|
||||
: ["repo:${repo}:*"]
|
||||
])
|
||||
}
|
||||
}
|
||||
}]
|
||||
})
|
||||
|
||||
tags = merge(local.common_tags, {
|
||||
Name = "${var.name_prefix}-ecr-push"
|
||||
Purpose = "ecr-push"
|
||||
})
|
||||
}
|
||||
|
||||
resource "aws_iam_role_policy" "ecr" {
|
||||
count = local.ecr_role_enabled ? 1 : 0
|
||||
|
||||
name = "ecr-push"
|
||||
role = aws_iam_role.ecr[0].id
|
||||
policy = jsonencode({
|
||||
Version = "2012-10-17"
|
||||
Statement = concat(
|
||||
[{
|
||||
Sid = "ECRAuth"
|
||||
Effect = "Allow"
|
||||
Action = "ecr:GetAuthorizationToken"
|
||||
Resource = "*" # Required - GetAuthorizationToken doesn't support resource constraints
|
||||
}],
|
||||
[{
|
||||
Sid = "ECRPush"
|
||||
Effect = "Allow"
|
||||
Action = [
|
||||
"ecr:BatchCheckLayerAvailability",
|
||||
"ecr:GetDownloadUrlForLayer",
|
||||
"ecr:BatchGetImage",
|
||||
"ecr:PutImage",
|
||||
"ecr:InitiateLayerUpload",
|
||||
"ecr:UploadLayerPart",
|
||||
"ecr:CompleteLayerUpload",
|
||||
"ecr:DescribeRepositories",
|
||||
"ecr:DescribeImages"
|
||||
]
|
||||
Resource = [for repo in try(var.ecr_push_role.ecr_repos, []) :
|
||||
"arn:${local.partition}:ecr:*:${local.account_id}:repository/${repo}"
|
||||
]
|
||||
}],
|
||||
try(var.ecr_push_role.allow_create, false) ? [{
|
||||
Sid = "ECRCreate"
|
||||
Effect = "Allow"
|
||||
Action = ["ecr:CreateRepository", "ecr:TagResource"]
|
||||
Resource = "arn:${local.partition}:ecr:*:${local.account_id}:repository/*"
|
||||
}] : [],
|
||||
try(var.ecr_push_role.allow_delete, false) ? [{
|
||||
Sid = "ECRDelete"
|
||||
Effect = "Allow"
|
||||
Action = ["ecr:DeleteRepository", "ecr:BatchDeleteImage"]
|
||||
Resource = [for repo in try(var.ecr_push_role.ecr_repos, []) :
|
||||
"arn:${local.partition}:ecr:*:${local.account_id}:repository/${repo}"
|
||||
]
|
||||
}] : []
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
################################################################################
|
||||
# S3 Deploy Role (Template)
|
||||
################################################################################
|
||||
|
||||
locals {
|
||||
s3_role_enabled = try(var.s3_deploy_role.enabled, false)
|
||||
s3_repos = try(var.s3_deploy_role.repos, [])
|
||||
s3_repos_normalized = [for repo in local.s3_repos :
|
||||
!contains(split("/", repo), "/") && var.github_org != ""
|
||||
? "${var.github_org}/${repo}"
|
||||
: repo
|
||||
]
|
||||
}
|
||||
|
||||
resource "aws_iam_role" "s3_deploy" {
|
||||
count = local.s3_role_enabled ? 1 : 0
|
||||
|
||||
name = "${var.name_prefix}-s3-deploy"
|
||||
path = var.path
|
||||
description = "GitHub Actions - S3 deployment"
|
||||
max_session_duration = 3600
|
||||
permissions_boundary = var.permissions_boundary
|
||||
|
||||
assume_role_policy = jsonencode({
|
||||
Version = "2012-10-17"
|
||||
Statement = [{
|
||||
Sid = "GitHubActionsS3Deploy"
|
||||
Effect = "Allow"
|
||||
Action = "sts:AssumeRoleWithWebIdentity"
|
||||
Principal = { Federated = local.provider_arn }
|
||||
Condition = {
|
||||
StringEquals = { "token.actions.githubusercontent.com:aud" = "sts.amazonaws.com" }
|
||||
StringLike = {
|
||||
"token.actions.githubusercontent.com:sub" = flatten([for repo in local.s3_repos_normalized :
|
||||
length(try(var.s3_deploy_role.branches, [])) > 0
|
||||
? [for branch in var.s3_deploy_role.branches : "repo:${repo}:ref:refs/heads/${branch}"]
|
||||
: ["repo:${repo}:*"]
|
||||
])
|
||||
}
|
||||
}
|
||||
}]
|
||||
})
|
||||
|
||||
tags = merge(local.common_tags, {
|
||||
Name = "${var.name_prefix}-s3-deploy"
|
||||
Purpose = "s3-static-site"
|
||||
})
|
||||
}
|
||||
|
||||
resource "aws_iam_role_policy" "s3_deploy" {
|
||||
count = local.s3_role_enabled ? 1 : 0
|
||||
|
||||
name = "s3-deploy"
|
||||
role = aws_iam_role.s3_deploy[0].id
|
||||
policy = jsonencode({
|
||||
Version = "2012-10-17"
|
||||
Statement = concat(
|
||||
[{
|
||||
Sid = "S3Deploy"
|
||||
Effect = "Allow"
|
||||
Action = [
|
||||
"s3:PutObject",
|
||||
"s3:GetObject",
|
||||
"s3:DeleteObject",
|
||||
"s3:GetObjectAcl",
|
||||
"s3:PutObjectAcl"
|
||||
]
|
||||
Resource = flatten([for bucket in try(var.s3_deploy_role.bucket_arns, []) : [
|
||||
for prefix in try(var.s3_deploy_role.allowed_prefixes, ["*"]) :
|
||||
"${bucket}/${prefix}"
|
||||
]])
|
||||
}],
|
||||
[{
|
||||
Sid = "S3List"
|
||||
Effect = "Allow"
|
||||
Action = ["s3:ListBucket", "s3:GetBucketLocation"]
|
||||
Resource = try(var.s3_deploy_role.bucket_arns, [])
|
||||
}],
|
||||
length(try(var.s3_deploy_role.cloudfront_arns, [])) > 0 ? [{
|
||||
Sid = "CloudFrontInvalidate"
|
||||
Effect = "Allow"
|
||||
Action = "cloudfront:CreateInvalidation"
|
||||
Resource = var.s3_deploy_role.cloudfront_arns
|
||||
}] : []
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
################################################################################
|
||||
# Lambda Deploy Role (Template)
|
||||
################################################################################
|
||||
|
||||
locals {
|
||||
lambda_role_enabled = try(var.lambda_deploy_role.enabled, false)
|
||||
lambda_repos = try(var.lambda_deploy_role.repos, [])
|
||||
lambda_repos_normalized = [for repo in local.lambda_repos :
|
||||
!contains(split("/", repo), "/") && var.github_org != ""
|
||||
? "${var.github_org}/${repo}"
|
||||
: repo
|
||||
]
|
||||
}
|
||||
|
||||
resource "aws_iam_role" "lambda_deploy" {
|
||||
count = local.lambda_role_enabled ? 1 : 0
|
||||
|
||||
name = "${var.name_prefix}-lambda-deploy"
|
||||
path = var.path
|
||||
description = "GitHub Actions - Lambda deployment"
|
||||
max_session_duration = 3600
|
||||
permissions_boundary = var.permissions_boundary
|
||||
|
||||
assume_role_policy = jsonencode({
|
||||
Version = "2012-10-17"
|
||||
Statement = [{
|
||||
Sid = "GitHubActionsLambda"
|
||||
Effect = "Allow"
|
||||
Action = "sts:AssumeRoleWithWebIdentity"
|
||||
Principal = { Federated = local.provider_arn }
|
||||
Condition = {
|
||||
StringEquals = { "token.actions.githubusercontent.com:aud" = "sts.amazonaws.com" }
|
||||
StringLike = {
|
||||
"token.actions.githubusercontent.com:sub" = flatten([for repo in local.lambda_repos_normalized :
|
||||
length(try(var.lambda_deploy_role.branches, [])) > 0
|
||||
? [for branch in var.lambda_deploy_role.branches : "repo:${repo}:ref:refs/heads/${branch}"]
|
||||
: ["repo:${repo}:*"]
|
||||
])
|
||||
}
|
||||
}
|
||||
}]
|
||||
})
|
||||
|
||||
tags = merge(local.common_tags, {
|
||||
Name = "${var.name_prefix}-lambda-deploy"
|
||||
Purpose = "lambda-deployment"
|
||||
})
|
||||
}
|
||||
|
||||
resource "aws_iam_role_policy" "lambda_deploy" {
|
||||
count = local.lambda_role_enabled ? 1 : 0
|
||||
|
||||
name = "lambda-deploy"
|
||||
role = aws_iam_role.lambda_deploy[0].id
|
||||
policy = jsonencode({
|
||||
Version = "2012-10-17"
|
||||
Statement = concat(
|
||||
[{
|
||||
Sid = "LambdaDeploy"
|
||||
Effect = "Allow"
|
||||
Action = [
|
||||
"lambda:UpdateFunctionCode",
|
||||
"lambda:UpdateFunctionConfiguration",
|
||||
"lambda:GetFunction",
|
||||
"lambda:GetFunctionConfiguration",
|
||||
"lambda:PublishVersion",
|
||||
"lambda:ListVersionsByFunction"
|
||||
]
|
||||
Resource = try(var.lambda_deploy_role.function_arns, [])
|
||||
}],
|
||||
try(var.lambda_deploy_role.allow_create, false) ? [{
|
||||
Sid = "LambdaCreate"
|
||||
Effect = "Allow"
|
||||
Action = [
|
||||
"lambda:CreateFunction",
|
||||
"lambda:DeleteFunction",
|
||||
"lambda:TagResource",
|
||||
"lambda:AddPermission",
|
||||
"lambda:RemovePermission"
|
||||
]
|
||||
Resource = "arn:${local.partition}:lambda:*:${local.account_id}:function:*"
|
||||
}] : [],
|
||||
try(var.lambda_deploy_role.allow_create, false) ? [{
|
||||
Sid = "IAMPassRole"
|
||||
Effect = "Allow"
|
||||
Action = "iam:PassRole"
|
||||
Resource = "arn:${local.partition}:iam::${local.account_id}:role/*"
|
||||
Condition = {
|
||||
StringEquals = {
|
||||
"iam:PassedToService" = "lambda.amazonaws.com"
|
||||
}
|
||||
}
|
||||
}] : [],
|
||||
try(var.lambda_deploy_role.allow_logs, true) ? [{
|
||||
Sid = "CloudWatchLogs"
|
||||
Effect = "Allow"
|
||||
Action = [
|
||||
"logs:DescribeLogGroups",
|
||||
"logs:DescribeLogStreams",
|
||||
"logs:GetLogEvents"
|
||||
]
|
||||
Resource = "arn:${local.partition}:logs:*:${local.account_id}:log-group:/aws/lambda/*"
|
||||
}] : []
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
################################################################################
|
||||
# Security Monitoring (Optional)
|
||||
################################################################################
|
||||
|
||||
resource "aws_cloudwatch_log_metric_filter" "oidc_assume_role" {
|
||||
count = var.enable_cloudtrail_logging && var.alarm_sns_topic_arn != "" ? 1 : 0
|
||||
|
||||
name = "github-oidc-assume-role"
|
||||
pattern = "{ ($.eventName = AssumeRoleWithWebIdentity) && ($.requestParameters.roleArn = \"*${var.name_prefix}*\") }"
|
||||
log_group_name = "aws-cloudtrail-logs" # Adjust to your CloudTrail log group
|
||||
|
||||
metric_transformation {
|
||||
name = "GitHubOIDCAssumeRole"
|
||||
namespace = "Security/OIDC"
|
||||
value = "1"
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_cloudwatch_metric_alarm" "oidc_high_usage" {
|
||||
count = var.enable_cloudtrail_logging && var.alarm_sns_topic_arn != "" ? 1 : 0
|
||||
|
||||
alarm_name = "github-oidc-high-usage"
|
||||
alarm_description = "High number of GitHub OIDC role assumptions"
|
||||
comparison_operator = "GreaterThanThreshold"
|
||||
evaluation_periods = 1
|
||||
metric_name = "GitHubOIDCAssumeRole"
|
||||
namespace = "Security/OIDC"
|
||||
period = 300
|
||||
statistic = "Sum"
|
||||
threshold = 100
|
||||
treat_missing_data = "notBreaching"
|
||||
|
||||
alarm_actions = [var.alarm_sns_topic_arn]
|
||||
|
||||
tags = local.common_tags
|
||||
}
|
||||
159
terraform/modules/github-oidc/outputs.tf
Normal file
159
terraform/modules/github-oidc/outputs.tf
Normal file
@@ -0,0 +1,159 @@
|
||||
################################################################################
|
||||
# GitHub OIDC Module - Outputs
|
||||
################################################################################
|
||||
|
||||
output "provider_arn" {
|
||||
value = local.provider_arn
|
||||
description = "GitHub OIDC provider ARN"
|
||||
}
|
||||
|
||||
output "provider_url" {
|
||||
value = "https://token.actions.githubusercontent.com"
|
||||
description = "GitHub OIDC provider URL"
|
||||
}
|
||||
|
||||
# Custom roles
|
||||
output "role_arns" {
|
||||
value = { for k, v in aws_iam_role.github : k => v.arn }
|
||||
description = "Map of custom role names to ARNs"
|
||||
}
|
||||
|
||||
output "role_names" {
|
||||
value = { for k, v in aws_iam_role.github : k => v.name }
|
||||
description = "Map of custom role key to IAM role names"
|
||||
}
|
||||
|
||||
# Template roles
|
||||
output "terraform_role_arn" {
|
||||
value = local.tf_role_enabled ? aws_iam_role.terraform[0].arn : null
|
||||
description = "Terraform deploy role ARN"
|
||||
}
|
||||
|
||||
output "terraform_role_name" {
|
||||
value = local.tf_role_enabled ? aws_iam_role.terraform[0].name : null
|
||||
description = "Terraform deploy role name"
|
||||
}
|
||||
|
||||
output "ecr_role_arn" {
|
||||
value = local.ecr_role_enabled ? aws_iam_role.ecr[0].arn : null
|
||||
description = "ECR push role ARN"
|
||||
}
|
||||
|
||||
output "ecr_role_name" {
|
||||
value = local.ecr_role_enabled ? aws_iam_role.ecr[0].name : null
|
||||
description = "ECR push role name"
|
||||
}
|
||||
|
||||
output "s3_deploy_role_arn" {
|
||||
value = local.s3_role_enabled ? aws_iam_role.s3_deploy[0].arn : null
|
||||
description = "S3 deploy role ARN"
|
||||
}
|
||||
|
||||
output "s3_deploy_role_name" {
|
||||
value = local.s3_role_enabled ? aws_iam_role.s3_deploy[0].name : null
|
||||
description = "S3 deploy role name"
|
||||
}
|
||||
|
||||
output "lambda_deploy_role_arn" {
|
||||
value = local.lambda_role_enabled ? aws_iam_role.lambda_deploy[0].arn : null
|
||||
description = "Lambda deploy role ARN"
|
||||
}
|
||||
|
||||
output "lambda_deploy_role_name" {
|
||||
value = local.lambda_role_enabled ? aws_iam_role.lambda_deploy[0].name : null
|
||||
description = "Lambda deploy role name"
|
||||
}
|
||||
|
||||
# All role ARNs combined
|
||||
output "all_role_arns" {
|
||||
value = merge(
|
||||
{ for k, v in aws_iam_role.github : k => v.arn },
|
||||
local.tf_role_enabled ? { terraform = aws_iam_role.terraform[0].arn } : {},
|
||||
local.ecr_role_enabled ? { ecr = aws_iam_role.ecr[0].arn } : {},
|
||||
local.s3_role_enabled ? { s3_deploy = aws_iam_role.s3_deploy[0].arn } : {},
|
||||
local.lambda_role_enabled ? { lambda_deploy = aws_iam_role.lambda_deploy[0].arn } : {}
|
||||
)
|
||||
description = "All role ARNs (custom + templates)"
|
||||
}
|
||||
|
||||
# Security outputs
|
||||
output "iam_path" {
|
||||
value = var.path
|
||||
description = "IAM path used for roles (useful for permissions boundaries)"
|
||||
}
|
||||
|
||||
output "security_recommendations" {
|
||||
value = {
|
||||
permissions_boundary_set = var.permissions_boundary != null
|
||||
max_session_limited = var.max_session_hours_limit < 12
|
||||
wildcard_repos_denied = var.deny_wildcard_repos
|
||||
cloudtrail_monitoring = var.enable_cloudtrail_logging
|
||||
}
|
||||
description = "Security configuration status"
|
||||
}
|
||||
|
||||
# Workflow configuration helper
|
||||
output "github_actions_config" {
|
||||
value = {
|
||||
aws_region = local.region
|
||||
roles = merge(
|
||||
{ for k, v in aws_iam_role.github : k => {
|
||||
arn = v.arn
|
||||
name = v.name
|
||||
}},
|
||||
local.tf_role_enabled ? { terraform = {
|
||||
arn = aws_iam_role.terraform[0].arn
|
||||
name = aws_iam_role.terraform[0].name
|
||||
}} : {},
|
||||
local.ecr_role_enabled ? { ecr = {
|
||||
arn = aws_iam_role.ecr[0].arn
|
||||
name = aws_iam_role.ecr[0].name
|
||||
}} : {},
|
||||
local.s3_role_enabled ? { s3_deploy = {
|
||||
arn = aws_iam_role.s3_deploy[0].arn
|
||||
name = aws_iam_role.s3_deploy[0].name
|
||||
}} : {},
|
||||
local.lambda_role_enabled ? { lambda_deploy = {
|
||||
arn = aws_iam_role.lambda_deploy[0].arn
|
||||
name = aws_iam_role.lambda_deploy[0].name
|
||||
}} : {}
|
||||
)
|
||||
}
|
||||
description = "Configuration for GitHub Actions workflows"
|
||||
}
|
||||
|
||||
# Example workflow snippets
|
||||
output "workflow_examples" {
|
||||
value = {
|
||||
basic = <<-EOF
|
||||
# .github/workflows/deploy.yml
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- uses: aws-actions/configure-aws-credentials@v4
|
||||
with:
|
||||
role-to-assume: <ROLE_ARN>
|
||||
aws-region: ${local.region}
|
||||
role-session-name: github-actions-${"$"}{{ github.run_id }}
|
||||
EOF
|
||||
|
||||
with_environment = <<-EOF
|
||||
# .github/workflows/deploy.yml
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
environment: production # Requires approval if configured
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
steps:
|
||||
- uses: aws-actions/configure-aws-credentials@v4
|
||||
with:
|
||||
role-to-assume: <ROLE_ARN>
|
||||
aws-region: ${local.region}
|
||||
EOF
|
||||
}
|
||||
description = "Example GitHub Actions workflow snippets"
|
||||
}
|
||||
210
terraform/modules/github-oidc/tests/basic.tftest.hcl
Normal file
210
terraform/modules/github-oidc/tests/basic.tftest.hcl
Normal file
@@ -0,0 +1,210 @@
|
||||
################################################################################
|
||||
# GitHub OIDC Module - Basic Tests
|
||||
# Uses Terraform native testing framework
|
||||
################################################################################
|
||||
|
||||
# Mock AWS provider for unit tests
|
||||
mock_provider "aws" {
|
||||
mock_data "aws_caller_identity" {
|
||||
defaults = {
|
||||
account_id = "123456789012"
|
||||
arn = "arn:aws:iam::123456789012:user/test"
|
||||
user_id = "AIDATEST123456789"
|
||||
}
|
||||
}
|
||||
|
||||
mock_data "aws_region" {
|
||||
defaults = {
|
||||
name = "us-east-1"
|
||||
}
|
||||
}
|
||||
|
||||
mock_data "aws_partition" {
|
||||
defaults = {
|
||||
partition = "aws"
|
||||
dns_suffix = "amazonaws.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Test: Basic role creation
|
||||
run "basic_role_creation" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
github_org = "test-org"
|
||||
roles = {
|
||||
deploy = {
|
||||
repos = ["test-repo"]
|
||||
branches = ["main"]
|
||||
policy_statements = [{
|
||||
sid = "TestAccess"
|
||||
actions = ["s3:GetObject"]
|
||||
resources = ["arn:aws:s3:::test-bucket/*"]
|
||||
}]
|
||||
}
|
||||
}
|
||||
tags = {
|
||||
Environment = "test"
|
||||
}
|
||||
}
|
||||
|
||||
# Verify OIDC provider is created
|
||||
assert {
|
||||
condition = aws_iam_openid_connect_provider.github[0].url == "https://token.actions.githubusercontent.com"
|
||||
error_message = "OIDC provider URL is incorrect"
|
||||
}
|
||||
|
||||
# Verify role is created with correct name
|
||||
assert {
|
||||
condition = aws_iam_role.github["deploy"].name == "github-deploy"
|
||||
error_message = "Role name should be github-deploy"
|
||||
}
|
||||
|
||||
# Verify IAM path is set correctly
|
||||
assert {
|
||||
condition = aws_iam_role.github["deploy"].path == "/github-actions/"
|
||||
error_message = "Role path should be /github-actions/"
|
||||
}
|
||||
}
|
||||
|
||||
# Test: Repository normalization with org prefix
|
||||
run "repo_normalization" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
github_org = "my-org"
|
||||
roles = {
|
||||
test = {
|
||||
repos = ["repo-without-org"] # Should become my-org/repo-without-org
|
||||
branches = ["main"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Role should be created (validates normalization works)
|
||||
assert {
|
||||
condition = aws_iam_role.github["test"].name == "github-test"
|
||||
error_message = "Role should be created with normalized repo"
|
||||
}
|
||||
}
|
||||
|
||||
# Test: Multiple roles with different configurations
|
||||
run "multiple_roles" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
github_org = "test-org"
|
||||
roles = {
|
||||
validate = {
|
||||
repos = ["app"]
|
||||
pull_request = true
|
||||
max_session_hours = 1
|
||||
}
|
||||
deploy = {
|
||||
repos = ["app"]
|
||||
branches = ["main"]
|
||||
max_session_hours = 2
|
||||
}
|
||||
release = {
|
||||
repos = ["app"]
|
||||
tags = ["v*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Verify all roles are created
|
||||
assert {
|
||||
condition = length(aws_iam_role.github) == 3
|
||||
error_message = "Should create 3 roles"
|
||||
}
|
||||
}
|
||||
|
||||
# Test: Terraform deploy template role
|
||||
run "terraform_template_role" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
github_org = "test-org"
|
||||
terraform_deploy_role = {
|
||||
enabled = true
|
||||
repos = ["infra"]
|
||||
branches = ["main"]
|
||||
state_bucket = "my-tf-state"
|
||||
dynamodb_table = "terraform-locks"
|
||||
}
|
||||
}
|
||||
|
||||
# Verify Terraform role is created
|
||||
assert {
|
||||
condition = aws_iam_role.terraform[0].name == "github-terraform"
|
||||
error_message = "Terraform role should be created"
|
||||
}
|
||||
}
|
||||
|
||||
# Test: ECR push template role
|
||||
run "ecr_template_role" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
github_org = "test-org"
|
||||
ecr_push_role = {
|
||||
enabled = true
|
||||
repos = ["app"]
|
||||
branches = ["main"]
|
||||
ecr_repos = ["my-ecr-repo"]
|
||||
}
|
||||
}
|
||||
|
||||
# Verify ECR role is created
|
||||
assert {
|
||||
condition = aws_iam_role.ecr[0].name == "github-ecr-push"
|
||||
error_message = "ECR role should be created"
|
||||
}
|
||||
}
|
||||
|
||||
# Test: Session duration capping
|
||||
run "session_duration_capping" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
github_org = "test-org"
|
||||
max_session_hours_limit = 2
|
||||
roles = {
|
||||
test = {
|
||||
repos = ["app"]
|
||||
branches = ["main"]
|
||||
max_session_hours = 4 # Should be capped to 2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Verify session duration is capped (2 hours = 7200 seconds)
|
||||
assert {
|
||||
condition = aws_iam_role.github["test"].max_session_duration == 7200
|
||||
error_message = "Session duration should be capped at 2 hours (7200 seconds)"
|
||||
}
|
||||
}
|
||||
|
||||
# Test: Existing provider ARN (no provider creation)
|
||||
run "existing_provider" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
create_provider = false
|
||||
provider_arn = "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com"
|
||||
github_org = "test-org"
|
||||
roles = {
|
||||
test = {
|
||||
repos = ["app"]
|
||||
branches = ["main"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Verify no provider is created
|
||||
assert {
|
||||
condition = length(aws_iam_openid_connect_provider.github) == 0
|
||||
error_message = "Should not create provider when create_provider=false"
|
||||
}
|
||||
}
|
||||
203
terraform/modules/github-oidc/tests/security.tftest.hcl
Normal file
203
terraform/modules/github-oidc/tests/security.tftest.hcl
Normal file
@@ -0,0 +1,203 @@
|
||||
################################################################################
|
||||
# GitHub OIDC Module - Security Tests
|
||||
# Validates security best practices are enforced
|
||||
################################################################################
|
||||
|
||||
mock_provider "aws" {
|
||||
mock_data "aws_caller_identity" {
|
||||
defaults = {
|
||||
account_id = "123456789012"
|
||||
}
|
||||
}
|
||||
|
||||
mock_data "aws_region" {
|
||||
defaults = {
|
||||
name = "us-east-1"
|
||||
}
|
||||
}
|
||||
|
||||
mock_data "aws_partition" {
|
||||
defaults = {
|
||||
partition = "aws"
|
||||
dns_suffix = "amazonaws.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Test: Wildcard repos denied by default
|
||||
run "wildcard_repos_denied" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
github_org = "test-org"
|
||||
deny_wildcard_repos = true
|
||||
roles = {
|
||||
test = {
|
||||
repos = ["*"] # Wildcard - should fail without workflow_ref
|
||||
branches = ["main"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
expect_failures = [
|
||||
# This should fail validation because wildcard repos require workflow_ref
|
||||
var.roles
|
||||
]
|
||||
}
|
||||
|
||||
# Test: Wildcard repos allowed with workflow_ref
|
||||
run "wildcard_repos_with_workflow_ref" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
github_org = "test-org"
|
||||
deny_wildcard_repos = true
|
||||
roles = {
|
||||
test = {
|
||||
repos = ["*"]
|
||||
workflow_ref = "test-org/workflows/.github/workflows/deploy.yml@main"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Should succeed because workflow_ref is specified
|
||||
assert {
|
||||
condition = aws_iam_role.github["test"].name == "github-test"
|
||||
error_message = "Should allow wildcard with workflow_ref"
|
||||
}
|
||||
}
|
||||
|
||||
# Test: IAM path isolation
|
||||
run "iam_path_isolation" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
github_org = "test-org"
|
||||
path = "/github-actions/"
|
||||
roles = {
|
||||
test = {
|
||||
repos = ["app"]
|
||||
branches = ["main"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Verify path is set for role isolation
|
||||
assert {
|
||||
condition = aws_iam_role.github["test"].path == "/github-actions/"
|
||||
error_message = "Role should use isolated IAM path"
|
||||
}
|
||||
}
|
||||
|
||||
# Test: Permissions boundary is applied
|
||||
run "permissions_boundary_applied" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
github_org = "test-org"
|
||||
permissions_boundary = "arn:aws:iam::123456789012:policy/TestBoundary"
|
||||
roles = {
|
||||
test = {
|
||||
repos = ["app"]
|
||||
branches = ["main"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Verify permissions boundary is set
|
||||
assert {
|
||||
condition = aws_iam_role.github["test"].permissions_boundary == "arn:aws:iam::123456789012:policy/TestBoundary"
|
||||
error_message = "Permissions boundary should be applied to role"
|
||||
}
|
||||
}
|
||||
|
||||
# Test: Terraform role has explicit denies
|
||||
run "terraform_role_explicit_denies" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
github_org = "test-org"
|
||||
terraform_deploy_role = {
|
||||
enabled = true
|
||||
repos = ["infra"]
|
||||
branches = ["main"]
|
||||
denied_actions = ["iam:CreateUser", "organizations:*"]
|
||||
}
|
||||
}
|
||||
|
||||
# Verify deny policy is created
|
||||
assert {
|
||||
condition = aws_iam_role_policy.terraform_deny[0].name == "terraform-deny"
|
||||
error_message = "Terraform deny policy should be created"
|
||||
}
|
||||
}
|
||||
|
||||
# Test: ECR role requires explicit repos
|
||||
run "ecr_explicit_repos_required" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
github_org = "test-org"
|
||||
ecr_push_role = {
|
||||
enabled = true
|
||||
repos = ["app"]
|
||||
ecr_repos = ["my-ecr-repo"] # Explicit ECR repo required
|
||||
}
|
||||
}
|
||||
|
||||
# Should succeed with explicit ECR repos
|
||||
assert {
|
||||
condition = aws_iam_role.ecr[0].name == "github-ecr-push"
|
||||
error_message = "ECR role should be created with explicit repos"
|
||||
}
|
||||
}
|
||||
|
||||
# Test: Role tags include security metadata
|
||||
run "security_tags" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
github_org = "test-org"
|
||||
roles = {
|
||||
test = {
|
||||
repos = ["app"]
|
||||
branches = ["main"]
|
||||
}
|
||||
}
|
||||
tags = {
|
||||
Environment = "production"
|
||||
}
|
||||
}
|
||||
|
||||
# Verify tags include ManagedBy and Module
|
||||
assert {
|
||||
condition = aws_iam_role.github["test"].tags["ManagedBy"] == "terraform"
|
||||
error_message = "Role should have ManagedBy tag"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = aws_iam_role.github["test"].tags["Module"] == "github-oidc"
|
||||
error_message = "Role should have Module tag"
|
||||
}
|
||||
}
|
||||
|
||||
# Test: Trust policy uses StringLike for subject claims
|
||||
run "trust_policy_string_like" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
github_org = "test-org"
|
||||
roles = {
|
||||
test = {
|
||||
repos = ["app"]
|
||||
branches = ["main", "develop"] # Multiple branches
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Role should be created with proper trust policy
|
||||
assert {
|
||||
condition = aws_iam_role.github["test"].assume_role_policy != ""
|
||||
error_message = "Trust policy should be set"
|
||||
}
|
||||
}
|
||||
248
terraform/modules/github-oidc/variables.tf
Normal file
248
terraform/modules/github-oidc/variables.tf
Normal file
@@ -0,0 +1,248 @@
|
||||
################################################################################
|
||||
# GitHub OIDC Module - Variables
|
||||
# With AWS/Terraform/Security Best Practices Validation
|
||||
################################################################################
|
||||
|
||||
variable "create_provider" {
|
||||
type = bool
|
||||
default = true
|
||||
description = "Create the OIDC provider. Set false if already exists in account."
|
||||
}
|
||||
|
||||
variable "provider_arn" {
|
||||
type = string
|
||||
default = ""
|
||||
description = "Existing OIDC provider ARN (required if create_provider = false)"
|
||||
|
||||
validation {
|
||||
condition = var.provider_arn == "" || can(regex("^arn:aws:iam::[0-9]{12}:oidc-provider/", var.provider_arn))
|
||||
error_message = "Provider ARN must be a valid IAM OIDC provider ARN."
|
||||
}
|
||||
}
|
||||
|
||||
variable "github_org" {
|
||||
type = string
|
||||
default = ""
|
||||
description = "GitHub organization. If set, prepended to repos that don't include org."
|
||||
|
||||
validation {
|
||||
condition = var.github_org == "" || can(regex("^[a-zA-Z0-9][a-zA-Z0-9-]*$", var.github_org))
|
||||
error_message = "GitHub org must be alphanumeric with hyphens (no leading hyphen)."
|
||||
}
|
||||
}
|
||||
|
||||
variable "name_prefix" {
|
||||
type = string
|
||||
default = "github"
|
||||
description = "Prefix for IAM role names"
|
||||
|
||||
validation {
|
||||
condition = can(regex("^[a-zA-Z][a-zA-Z0-9-_]*$", var.name_prefix))
|
||||
error_message = "Name prefix must start with letter, contain only alphanumeric, hyphens, underscores."
|
||||
}
|
||||
}
|
||||
|
||||
variable "path" {
|
||||
type = string
|
||||
default = "/github-actions/"
|
||||
description = "IAM path for roles (enables easier permission boundaries)"
|
||||
|
||||
validation {
|
||||
condition = can(regex("^/[a-zA-Z0-9/_-]*/$", var.path))
|
||||
error_message = "IAM path must start and end with /, contain only alphanumeric, /, -, _."
|
||||
}
|
||||
}
|
||||
|
||||
variable "permissions_boundary" {
|
||||
type = string
|
||||
default = null
|
||||
description = "ARN of permissions boundary to attach to roles (RECOMMENDED for defense-in-depth)"
|
||||
|
||||
validation {
|
||||
condition = var.permissions_boundary == null || can(regex("^arn:aws:iam::[0-9]{12}:policy/", var.permissions_boundary))
|
||||
error_message = "Permissions boundary must be a valid IAM policy ARN."
|
||||
}
|
||||
}
|
||||
|
||||
variable "require_permissions_boundary" {
|
||||
type = bool
|
||||
default = false
|
||||
description = "Require a permissions boundary to be set (security guardrail)"
|
||||
}
|
||||
|
||||
variable "max_session_hours_limit" {
|
||||
type = number
|
||||
default = 4
|
||||
description = "Maximum allowed session duration in hours (caps role max_session_hours)"
|
||||
|
||||
validation {
|
||||
condition = var.max_session_hours_limit >= 1 && var.max_session_hours_limit <= 12
|
||||
error_message = "Max session hours must be between 1 and 12."
|
||||
}
|
||||
}
|
||||
|
||||
variable "deny_wildcard_repos" {
|
||||
type = bool
|
||||
default = true
|
||||
description = "Deny roles that allow all repos (*). Set false only if using workflow_ref restriction."
|
||||
}
|
||||
|
||||
variable "roles" {
|
||||
type = map(object({
|
||||
# Repository configuration
|
||||
repos = list(string) # GitHub repos (owner/repo or just repo if github_org set)
|
||||
branches = optional(list(string), []) # Branch restrictions (empty = all branches)
|
||||
tags = optional(list(string), []) # Tag restrictions (e.g., ["v*", "release-*"])
|
||||
environments = optional(list(string), []) # GitHub environment restrictions
|
||||
|
||||
# Event type restrictions
|
||||
pull_request = optional(bool, false) # Allow from pull_request events
|
||||
workflow_ref = optional(string, "") # Restrict to specific reusable workflow
|
||||
|
||||
# IAM configuration
|
||||
policy_arns = optional(list(string), []) # Managed policy ARNs to attach
|
||||
inline_policy = optional(string, "") # Inline policy JSON
|
||||
policy_statements = optional(list(object({ # Policy statements to generate
|
||||
sid = optional(string, "")
|
||||
effect = optional(string, "Allow")
|
||||
actions = list(string)
|
||||
resources = list(string)
|
||||
conditions = optional(list(object({
|
||||
test = string
|
||||
variable = string
|
||||
values = list(string)
|
||||
})), [])
|
||||
})), [])
|
||||
|
||||
# Session configuration
|
||||
max_session_hours = optional(number, 1) # Maximum session duration (1-12)
|
||||
|
||||
# Extra trust conditions
|
||||
extra_conditions = optional(map(map(list(string))), {}) # Additional assume role conditions
|
||||
}))
|
||||
default = {}
|
||||
description = "Map of role configurations for GitHub Actions"
|
||||
|
||||
validation {
|
||||
condition = alltrue([
|
||||
for k, v in var.roles : length(v.repos) > 0
|
||||
])
|
||||
error_message = "Each role must specify at least one repository."
|
||||
}
|
||||
|
||||
validation {
|
||||
condition = alltrue([
|
||||
for k, v in var.roles : v.max_session_hours >= 1 && v.max_session_hours <= 12
|
||||
])
|
||||
error_message = "Role max_session_hours must be between 1 and 12."
|
||||
}
|
||||
|
||||
validation {
|
||||
condition = alltrue([
|
||||
for k, v in var.roles : alltrue([
|
||||
for repo in v.repos : can(regex("^[a-zA-Z0-9][a-zA-Z0-9-_.]*/[a-zA-Z0-9][a-zA-Z0-9-_.]*$|^[a-zA-Z0-9][a-zA-Z0-9-_.]*$|^\\*$", repo))
|
||||
])
|
||||
])
|
||||
error_message = "Repository names must be valid GitHub repo format (owner/repo or repo)."
|
||||
}
|
||||
}
|
||||
|
||||
# Pre-built role templates
|
||||
variable "terraform_deploy_role" {
|
||||
type = object({
|
||||
enabled = optional(bool, false)
|
||||
repos = optional(list(string), [])
|
||||
branches = optional(list(string), ["main"])
|
||||
environments = optional(list(string), [])
|
||||
state_bucket = optional(string, "")
|
||||
state_bucket_key_prefix = optional(string, "*") # Limit to specific paths
|
||||
dynamodb_table = optional(string, "")
|
||||
allowed_services = optional(list(string), []) # Limit to specific AWS services
|
||||
denied_actions = optional(list(string), [ # Explicit denies for safety
|
||||
"iam:CreateUser",
|
||||
"iam:CreateAccessKey",
|
||||
"iam:DeleteAccountPasswordPolicy",
|
||||
"organizations:*",
|
||||
"account:*"
|
||||
])
|
||||
})
|
||||
default = {}
|
||||
description = "Pre-configured role for Terraform deployments"
|
||||
}
|
||||
|
||||
variable "ecr_push_role" {
|
||||
type = object({
|
||||
enabled = optional(bool, false)
|
||||
repos = optional(list(string), [])
|
||||
branches = optional(list(string), ["main"])
|
||||
ecr_repos = optional(list(string), []) # Specific ECR repos (no default wildcard)
|
||||
allow_create = optional(bool, false)
|
||||
allow_delete = optional(bool, false) # Explicit opt-in for delete
|
||||
})
|
||||
default = {}
|
||||
description = "Pre-configured role for ECR push operations"
|
||||
|
||||
validation {
|
||||
condition = !try(var.ecr_push_role.enabled, false) || length(try(var.ecr_push_role.ecr_repos, [])) > 0
|
||||
error_message = "ECR push role requires explicit ecr_repos list (no wildcards for security)."
|
||||
}
|
||||
}
|
||||
|
||||
variable "s3_deploy_role" {
|
||||
type = object({
|
||||
enabled = optional(bool, false)
|
||||
repos = optional(list(string), [])
|
||||
branches = optional(list(string), ["main"])
|
||||
bucket_arns = optional(list(string), [])
|
||||
allowed_prefixes = optional(list(string), ["*"]) # Limit to specific paths
|
||||
cloudfront_arns = optional(list(string), [])
|
||||
})
|
||||
default = {}
|
||||
description = "Pre-configured role for S3 static site deployments"
|
||||
|
||||
validation {
|
||||
condition = !try(var.s3_deploy_role.enabled, false) || length(try(var.s3_deploy_role.bucket_arns, [])) > 0
|
||||
error_message = "S3 deploy role requires explicit bucket_arns list."
|
||||
}
|
||||
}
|
||||
|
||||
variable "lambda_deploy_role" {
|
||||
type = object({
|
||||
enabled = optional(bool, false)
|
||||
repos = optional(list(string), [])
|
||||
branches = optional(list(string), ["main"])
|
||||
function_arns = optional(list(string), [])
|
||||
allow_create = optional(bool, false)
|
||||
allow_logs = optional(bool, true) # Allow CloudWatch Logs access
|
||||
})
|
||||
default = {}
|
||||
description = "Pre-configured role for Lambda deployments"
|
||||
|
||||
validation {
|
||||
condition = !try(var.lambda_deploy_role.enabled, false) || length(try(var.lambda_deploy_role.function_arns, [])) > 0
|
||||
error_message = "Lambda deploy role requires explicit function_arns list."
|
||||
}
|
||||
}
|
||||
|
||||
variable "enable_cloudtrail_logging" {
|
||||
type = bool
|
||||
default = true
|
||||
description = "Create CloudWatch metric alarms for OIDC role assumptions"
|
||||
}
|
||||
|
||||
variable "alarm_sns_topic_arn" {
|
||||
type = string
|
||||
default = ""
|
||||
description = "SNS topic ARN for security alarms"
|
||||
}
|
||||
|
||||
variable "tags" {
|
||||
type = map(string)
|
||||
default = {}
|
||||
description = "Tags to apply to all resources"
|
||||
|
||||
validation {
|
||||
condition = !contains(keys(var.tags), "Name")
|
||||
error_message = "Name tag is auto-generated, do not specify in tags variable."
|
||||
}
|
||||
}
|
||||
18
terraform/modules/github-oidc/versions.tf
Normal file
18
terraform/modules/github-oidc/versions.tf
Normal file
@@ -0,0 +1,18 @@
|
||||
################################################################################
|
||||
# GitHub OIDC Module - Versions
|
||||
################################################################################
|
||||
|
||||
terraform {
|
||||
required_version = ">= 1.5.0"
|
||||
|
||||
required_providers {
|
||||
aws = {
|
||||
source = "hashicorp/aws"
|
||||
version = ">= 5.0"
|
||||
}
|
||||
tls = {
|
||||
source = "hashicorp/tls"
|
||||
version = ">= 4.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user