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,427 @@
################################################################################
# Workload: Secrets Manager
#
# Secure secret storage:
# - KMS encryption
# - Automatic rotation (RDS, Redshift, DocumentDB, custom Lambda)
# - Cross-account access policies
# - Versioning and recovery
# - Replication to other regions
#
# Use cases: DB credentials, API keys, certificates, config
################################################################################
terraform {
required_version = ">= 1.5"
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 5.0"
}
random = {
source = "hashicorp/random"
version = ">= 3.0"
}
}
backend "s3" {
key = "05-workloads/<TENANT>-<NAME>-secrets/terraform.tfstate"
}
}
################################################################################
# Configuration - UPDATE THESE
################################################################################
locals {
# Naming
tenant = "<TENANT>"
name = "<NAME>"
env = "prod"
prefix = "${local.tenant}/${local.env}"
# KMS encryption (null uses AWS managed key)
kms_key_arn = null
# Recovery window (days) - 0 for immediate deletion
recovery_window_days = 30
# Secrets to create
secrets = {
# Database credentials (auto-generated)
"db/main" = {
description = "Main database credentials"
generate_password = true
password_length = 32
exclude_characters = "\"@/\\"
secret_string_template = jsonencode({
username = "admin"
engine = "postgres"
host = "db.example.internal"
port = 5432
dbname = "main"
})
# RDS rotation
rotation = {
enabled = false
# lambda_arn = "arn:aws:lambda:..." # Rotation Lambda
# days = 30
}
}
# API keys - Update via console or CLI after deployment:
# aws secretsmanager put-secret-value --secret-id <arn> --secret-string '{"publishable_key":"pk_live_xxx","secret_key":"sk_live_xxx"}'
"api/stripe" = {
description = "Stripe API keys"
secret_string = jsonencode({
publishable_key = "pk_live_xxxxxxxxxxxx"
secret_key = "sk_live_xxxxxxxxxxxx"
})
}
# Generic config
"config/app" = {
description = "Application configuration"
secret_string = jsonencode({
feature_flags = {
new_checkout = true
beta_features = false
}
limits = {
max_upload_mb = 100
rate_limit_rpm = 1000
}
})
}
}
# Cross-account access
allowed_accounts = [
# "123456789012", # Dev account
# "234567890123", # Staging account
]
# IAM principals allowed to read secrets
allowed_principals = [
# "arn:aws:iam::123456789012:role/app-role",
]
# Replication to other regions
replica_regions = [
# "us-west-2",
# "eu-west-1",
]
}
################################################################################
# Variables
################################################################################
variable "region" {
type = string
default = "us-east-1"
}
variable "state_bucket" {
type = string
}
################################################################################
# Provider
################################################################################
provider "aws" {
region = var.region
default_tags {
tags = {
Tenant = local.tenant
App = local.name
Environment = local.env
ManagedBy = "terraform"
}
}
}
################################################################################
# Data Sources
################################################################################
data "aws_caller_identity" "current" {}
data "aws_region" "current" {}
################################################################################
# KMS Key (optional - for customer managed encryption)
################################################################################
resource "aws_kms_key" "secrets" {
count = local.kms_key_arn == null ? 1 : 0
description = "KMS key for ${local.prefix} secrets"
deletion_window_in_days = 30
enable_key_rotation = true
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "Enable IAM policies"
Effect = "Allow"
Principal = {
AWS = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root"
}
Action = "kms:*"
Resource = "*"
},
{
Sid = "Allow Secrets Manager"
Effect = "Allow"
Principal = {
Service = "secretsmanager.amazonaws.com"
}
Action = [
"kms:Encrypt",
"kms:Decrypt",
"kms:ReEncrypt*",
"kms:GenerateDataKey*",
"kms:DescribeKey"
]
Resource = "*"
}
]
})
tags = { Name = "${local.prefix}-secrets" }
}
resource "aws_kms_alias" "secrets" {
count = local.kms_key_arn == null ? 1 : 0
name = "alias/${replace(local.prefix, "/", "-")}-secrets"
target_key_id = aws_kms_key.secrets[0].key_id
}
locals {
effective_kms_key = local.kms_key_arn != null ? local.kms_key_arn : aws_kms_key.secrets[0].arn
}
################################################################################
# Random Passwords
################################################################################
resource "random_password" "secrets" {
for_each = { for k, v in local.secrets : k => v if lookup(v, "generate_password", false) }
length = lookup(each.value, "password_length", 32)
special = true
override_special = lookup(each.value, "override_special", "!#$%&*()-_=+[]{}<>:?")
# Exclude problematic characters
min_lower = 1
min_upper = 1
min_numeric = 1
min_special = 1
}
################################################################################
# Secrets
################################################################################
resource "aws_secretsmanager_secret" "secrets" {
for_each = local.secrets
name = "${local.prefix}/${each.key}"
description = lookup(each.value, "description", "Secret for ${each.key}")
kms_key_id = local.effective_kms_key
recovery_window_in_days = local.recovery_window_days
# Replication
dynamic "replica" {
for_each = local.replica_regions
content {
region = replica.value
kms_key_id = null # Use default key in replica region
}
}
tags = { Name = "${local.prefix}/${each.key}" }
}
################################################################################
# Secret Values
################################################################################
resource "aws_secretsmanager_secret_version" "secrets" {
for_each = local.secrets
secret_id = aws_secretsmanager_secret.secrets[each.key].id
secret_string = lookup(each.value, "generate_password", false) ? jsonencode(merge(
jsondecode(lookup(each.value, "secret_string_template", "{}")),
{ password = random_password.secrets[each.key].result }
)) : lookup(each.value, "secret_string", "{}")
}
################################################################################
# Secret Rotation
################################################################################
resource "aws_secretsmanager_secret_rotation" "secrets" {
for_each = { for k, v in local.secrets : k => v if lookup(lookup(v, "rotation", {}), "enabled", false) }
secret_id = aws_secretsmanager_secret.secrets[each.key].id
rotation_lambda_arn = each.value.rotation.lambda_arn
rotation_rules {
automatically_after_days = lookup(each.value.rotation, "days", 30)
}
}
################################################################################
# Resource Policy (Cross-Account Access)
################################################################################
resource "aws_secretsmanager_secret_policy" "cross_account" {
for_each = length(local.allowed_accounts) > 0 || length(local.allowed_principals) > 0 ? local.secrets : {}
secret_arn = aws_secretsmanager_secret.secrets[each.key].arn
policy = jsonencode({
Version = "2012-10-17"
Statement = concat(
length(local.allowed_accounts) > 0 ? [{
Sid = "AllowCrossAccountAccess"
Effect = "Allow"
Principal = {
AWS = [for acct in local.allowed_accounts : "arn:aws:iam::${acct}:root"]
}
Action = [
"secretsmanager:GetSecretValue",
"secretsmanager:DescribeSecret"
]
Resource = "*"
}] : [],
length(local.allowed_principals) > 0 ? [{
Sid = "AllowPrincipalAccess"
Effect = "Allow"
Principal = {
AWS = local.allowed_principals
}
Action = [
"secretsmanager:GetSecretValue",
"secretsmanager:DescribeSecret"
]
Resource = "*"
}] : []
)
})
}
################################################################################
# IAM Policy for Reading Secrets
################################################################################
resource "aws_iam_policy" "read_secrets" {
name = "${replace(local.prefix, "/", "-")}-secrets-read"
description = "Read access to ${local.prefix} secrets"
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "GetSecrets"
Effect = "Allow"
Action = [
"secretsmanager:GetSecretValue",
"secretsmanager:DescribeSecret"
]
Resource = [for s in aws_secretsmanager_secret.secrets : s.arn]
},
{
Sid = "DecryptSecrets"
Effect = "Allow"
Action = [
"kms:Decrypt",
"kms:DescribeKey"
]
Resource = [local.effective_kms_key]
}
]
})
tags = { Name = "${local.prefix}-secrets-read" }
}
resource "aws_iam_policy" "write_secrets" {
name = "${replace(local.prefix, "/", "-")}-secrets-write"
description = "Write access to ${local.prefix} secrets"
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "ManageSecrets"
Effect = "Allow"
Action = [
"secretsmanager:GetSecretValue",
"secretsmanager:DescribeSecret",
"secretsmanager:PutSecretValue",
"secretsmanager:UpdateSecret"
]
Resource = [for s in aws_secretsmanager_secret.secrets : s.arn]
},
{
Sid = "EncryptDecryptSecrets"
Effect = "Allow"
Action = [
"kms:Encrypt",
"kms:Decrypt",
"kms:GenerateDataKey",
"kms:DescribeKey"
]
Resource = [local.effective_kms_key]
}
]
})
tags = { Name = "${local.prefix}-secrets-write" }
}
################################################################################
# Outputs
################################################################################
output "secret_arns" {
value = { for k, v in aws_secretsmanager_secret.secrets : k => v.arn }
description = "Secret ARNs"
}
output "secret_names" {
value = { for k, v in aws_secretsmanager_secret.secrets : k => v.name }
description = "Secret names"
}
output "kms_key_arn" {
value = local.effective_kms_key
description = "KMS key ARN used for encryption"
}
output "read_policy_arn" {
value = aws_iam_policy.read_secrets.arn
description = "IAM policy ARN for reading secrets"
}
output "write_policy_arn" {
value = aws_iam_policy.write_secrets.arn
description = "IAM policy ARN for writing secrets"
}
output "secret_retrieval_commands" {
value = {
for k, v in aws_secretsmanager_secret.secrets : k =>
"aws secretsmanager get-secret-value --secret-id ${v.name} --query SecretString --output text | jq ."
}
description = "CLI commands to retrieve each secret"
}