mirror of
https://github.com/ghndrx/terraform-foundation.git
synced 2026-02-10 06:45:06 +00:00
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
428 lines
12 KiB
HCL
428 lines
12 KiB
HCL
################################################################################
|
|
# 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"
|
|
}
|