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:
2026-02-01 20:06:28 +00:00
commit 6136cde9bb
145 changed files with 30832 additions and 0 deletions

View File

@@ -0,0 +1,45 @@
# tenant-iam
Terraform module for AWS landing zone pattern.
Create tenant-specific IAM roles with proper isolation.
## Planned Features
- [ ] Tenant admin role (full tenant access)
- [ ] Tenant developer role (limited write)
- [ ] Tenant readonly role (view only)
- [ ] Permissions boundary enforcement
- [ ] Resource-based isolation (tenant prefix)
- [ ] Cross-account trust configuration
## Planned Usage
```hcl
module "tenant_iam" {
source = "../modules/tenant-iam"
tenant_name = "acme-corp"
tenant_id = "acme"
create_admin_role = true
create_developer_role = true
create_readonly_role = true
trusted_principals = [
"arn:aws:iam::111111111111:root" # Identity account
]
allowed_services = ["ec2", "s3", "lambda", "rds"]
resource_prefix = "acme-"
permissions_boundary = aws_iam_policy.tenant_boundary.arn
}
```
## Security
All tenant roles are created with permissions boundaries to prevent:
- Creating IAM users/roles without boundaries
- Accessing other tenants' resources
- Modifying security services

View File

@@ -0,0 +1,279 @@
################################################################################
# Tenant IAM Module
#
# Creates tenant-specific IAM roles with isolation:
# - Tenant admin role with permissions boundary
# - Tenant developer role
# - Tenant readonly role
# - Permissions boundary for tenant isolation
################################################################################
terraform {
required_version = ">= 1.5.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 5.0"
}
}
}
data "aws_caller_identity" "current" {}
data "aws_partition" "current" {}
locals {
account_id = data.aws_caller_identity.current.account_id
partition = data.aws_partition.current.partition
# Resource prefix for tenant isolation
resource_prefix = var.resource_prefix != "" ? var.resource_prefix : "${var.tenant_id}-"
}
################################################################################
# Permissions Boundary
################################################################################
resource "aws_iam_policy" "boundary" {
count = var.create_permissions_boundary ? 1 : 0
name = "${var.tenant_id}-permissions-boundary"
path = var.iam_path
description = "Permissions boundary for ${var.tenant_name} tenant"
policy = jsonencode({
Version = "2012-10-17"
Statement = concat(
# Allow specified services
[{
Sid = "AllowedServices"
Effect = "Allow"
Action = [for svc in var.allowed_services : "${svc}:*"]
Resource = "*"
Condition = {
StringLikeIfExists = {
"aws:ResourceTag/Tenant" = [var.tenant_id, var.tenant_name]
}
}
}],
# Restrict to tenant-prefixed resources where possible
[{
Sid = "RestrictToTenantResources"
Effect = "Allow"
Action = [
"s3:*",
"dynamodb:*",
"lambda:*",
"sqs:*",
"sns:*"
]
Resource = [
"arn:${local.partition}:s3:::${local.resource_prefix}*",
"arn:${local.partition}:dynamodb:*:${local.account_id}:table/${local.resource_prefix}*",
"arn:${local.partition}:lambda:*:${local.account_id}:function:${local.resource_prefix}*",
"arn:${local.partition}:sqs:*:${local.account_id}:${local.resource_prefix}*",
"arn:${local.partition}:sns:*:${local.account_id}:${local.resource_prefix}*"
]
}],
# Deny modifying boundary or escalating privileges
[{
Sid = "DenyBoundaryModification"
Effect = "Deny"
Action = [
"iam:DeletePolicy",
"iam:DeletePolicyVersion",
"iam:CreatePolicyVersion",
"iam:SetDefaultPolicyVersion"
]
Resource = "arn:${local.partition}:iam::${local.account_id}:policy/${var.tenant_id}-permissions-boundary"
}],
# Deny creating roles/users without boundary
[{
Sid = "DenyCreatingRolesWithoutBoundary"
Effect = "Deny"
Action = [
"iam:CreateRole",
"iam:CreateUser"
]
Resource = "*"
Condition = {
StringNotEquals = {
"iam:PermissionsBoundary" = "arn:${local.partition}:iam::${local.account_id}:policy/${var.tenant_id}-permissions-boundary"
}
}
}],
# Deny modifying other tenants' resources
[{
Sid = "DenyAccessToOtherTenants"
Effect = "Deny"
Action = "*"
Resource = "*"
Condition = {
StringNotLike = {
"aws:ResourceTag/Tenant" = [var.tenant_id, var.tenant_name, ""]
}
Null = {
"aws:ResourceTag/Tenant" = "false"
}
}
}],
# Deny disabling security services
[{
Sid = "DenySecurityServiceModification"
Effect = "Deny"
Action = [
"guardduty:DeleteDetector",
"guardduty:DisassociateFromMasterAccount",
"securityhub:DisableSecurityHub",
"config:DeleteConfigurationRecorder",
"config:StopConfigurationRecorder",
"cloudtrail:DeleteTrail",
"cloudtrail:StopLogging"
]
Resource = "*"
}]
)
})
tags = merge(var.tags, {
Name = "${var.tenant_id}-permissions-boundary"
Tenant = var.tenant_name
})
}
################################################################################
# Admin Role
################################################################################
resource "aws_iam_role" "admin" {
count = var.create_admin_role ? 1 : 0
name = "${var.tenant_id}-admin"
path = var.iam_path
permissions_boundary = var.create_permissions_boundary ? aws_iam_policy.boundary[0].arn : var.permissions_boundary_arn
max_session_duration = var.admin_session_duration
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
AWS = var.trusted_principals
}
Condition = var.require_mfa ? {
Bool = {
"aws:MultiFactorAuthPresent" = "true"
}
} : {}
}]
})
tags = merge(var.tags, {
Name = "${var.tenant_id}-admin"
Tenant = var.tenant_name
Role = "admin"
})
}
resource "aws_iam_role_policy_attachment" "admin" {
count = var.create_admin_role ? 1 : 0
role = aws_iam_role.admin[0].name
policy_arn = "arn:${local.partition}:iam::aws:policy/PowerUserAccess"
}
################################################################################
# Developer Role
################################################################################
resource "aws_iam_role" "developer" {
count = var.create_developer_role ? 1 : 0
name = "${var.tenant_id}-developer"
path = var.iam_path
permissions_boundary = var.create_permissions_boundary ? aws_iam_policy.boundary[0].arn : var.permissions_boundary_arn
max_session_duration = var.developer_session_duration
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
AWS = var.trusted_principals
}
}]
})
tags = merge(var.tags, {
Name = "${var.tenant_id}-developer"
Tenant = var.tenant_name
Role = "developer"
})
}
resource "aws_iam_role_policy" "developer" {
count = var.create_developer_role ? 1 : 0
name = "developer-access"
role = aws_iam_role.developer[0].id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "DeveloperAccess"
Effect = "Allow"
Action = [for svc in var.allowed_services : "${svc}:*"]
Resource = "*"
},
{
Sid = "DenyAdmin"
Effect = "Deny"
Action = [
"iam:*",
"organizations:*",
"account:*"
]
Resource = "*"
}
]
})
}
################################################################################
# Readonly Role
################################################################################
resource "aws_iam_role" "readonly" {
count = var.create_readonly_role ? 1 : 0
name = "${var.tenant_id}-readonly"
path = var.iam_path
permissions_boundary = var.create_permissions_boundary ? aws_iam_policy.boundary[0].arn : var.permissions_boundary_arn
max_session_duration = var.readonly_session_duration
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
AWS = var.trusted_principals
}
}]
})
tags = merge(var.tags, {
Name = "${var.tenant_id}-readonly"
Tenant = var.tenant_name
Role = "readonly"
})
}
resource "aws_iam_role_policy_attachment" "readonly" {
count = var.create_readonly_role ? 1 : 0
role = aws_iam_role.readonly[0].name
policy_arn = "arn:${local.partition}:iam::aws:policy/ReadOnlyAccess"
}

View File

@@ -0,0 +1,52 @@
################################################################################
# Tenant IAM - Outputs
################################################################################
output "permissions_boundary_arn" {
value = var.create_permissions_boundary ? aws_iam_policy.boundary[0].arn : var.permissions_boundary_arn
description = "Permissions boundary policy ARN"
}
output "admin_role_arn" {
value = try(aws_iam_role.admin[0].arn, null)
description = "Tenant admin role ARN"
}
output "admin_role_name" {
value = try(aws_iam_role.admin[0].name, null)
description = "Tenant admin role name"
}
output "developer_role_arn" {
value = try(aws_iam_role.developer[0].arn, null)
description = "Tenant developer role ARN"
}
output "developer_role_name" {
value = try(aws_iam_role.developer[0].name, null)
description = "Tenant developer role name"
}
output "readonly_role_arn" {
value = try(aws_iam_role.readonly[0].arn, null)
description = "Tenant readonly role ARN"
}
output "readonly_role_name" {
value = try(aws_iam_role.readonly[0].name, null)
description = "Tenant readonly role name"
}
output "all_role_arns" {
value = {
admin = try(aws_iam_role.admin[0].arn, null)
developer = try(aws_iam_role.developer[0].arn, null)
readonly = try(aws_iam_role.readonly[0].arn, null)
}
description = "Map of all tenant role ARNs"
}
output "resource_prefix" {
value = local.resource_prefix
description = "Resource prefix for tenant naming"
}

View File

@@ -0,0 +1,97 @@
################################################################################
# Tenant IAM - Input Variables
################################################################################
variable "tenant_name" {
type = string
description = "Tenant name (human readable)"
}
variable "tenant_id" {
type = string
description = "Short tenant ID for resource naming"
}
variable "create_permissions_boundary" {
type = bool
default = true
description = "Create permissions boundary policy"
}
variable "permissions_boundary_arn" {
type = string
default = null
description = "Existing permissions boundary ARN (if not creating)"
}
variable "create_admin_role" {
type = bool
default = true
description = "Create tenant admin role"
}
variable "create_developer_role" {
type = bool
default = true
description = "Create tenant developer role"
}
variable "create_readonly_role" {
type = bool
default = true
description = "Create tenant readonly role"
}
variable "trusted_principals" {
type = list(string)
default = []
description = "ARNs allowed to assume tenant roles"
}
variable "allowed_services" {
type = list(string)
default = ["ec2", "s3", "lambda", "dynamodb", "rds", "ecs", "ecr", "logs", "cloudwatch", "events", "sqs", "sns"]
description = "AWS services the tenant can use"
}
variable "resource_prefix" {
type = string
default = ""
description = "Resource naming prefix (defaults to tenant_id-)"
}
variable "iam_path" {
type = string
default = "/tenants/"
description = "IAM path for roles and policies"
}
variable "require_mfa" {
type = bool
default = true
description = "Require MFA for admin role"
}
variable "admin_session_duration" {
type = number
default = 3600
description = "Admin role session duration in seconds"
}
variable "developer_session_duration" {
type = number
default = 14400
description = "Developer role session duration in seconds"
}
variable "readonly_session_duration" {
type = number
default = 14400
description = "Readonly role session duration in seconds"
}
variable "tags" {
type = map(string)
default = {}
description = "Tags to apply to resources"
}