1# ============================================================================= 2# AI Software Factory — Security (security groups, KMS, S3 bucket policy) 3# 4# Security group model (3 tiers, matching the k8s namespace segmentation): 5# 6# edge — internet-facing: web ALB (443/80 in), 22 to ops CIDRs (out to 7# private app nodes). The ALB is the ONLY public entry point. 8# app — application tier: EC2 nodes (web/api/worker/realtime), ALB→node 9# (3000/4000/4001 + health 8080), node→data tier (5432/6379/9200/ 10# 9000-S3), node→node (4000/4001 for internal service calls). 11# data — data tier: RDS, ElastiCache, OpenSearch. Ingress from `app` SG 12# ONLY; no internet, no ingress from `edge`. 13# 14# The S3 uploads bucket is reachable from app nodes via the S3 VPC endpoint 15# (network.tf) and from the ALB/worker for presigned flows. 16# ============================================================================= 17 18# --- Edge security group (ALB) ------------------------------------------------- 19 20resource "aws_security_group" "edge" { 21 name_prefix = "${local.names.project}-edge-" 22 vpc_id = aws_vpc.main.id 23 24 # HTTPS in from the internet 25 ingress { 26 description = "HTTPS from internet to ALB" 27 from_port = 443 28 to_port = 443 29 protocol = "tcp" 30 cidr_blocks = ["0.0.0.0/0"] 31 } 32 33 # HTTP in (only for the 443 redirect) 34 ingress { 35 description = "HTTP from internet to ALB (redirect to HTTPS)" 36 from_port = 80 37 to_port = 80 38 protocol = "tcp" 39 cidr_blocks = ["0.0.0.0/0"] 40 } 41 42 # ALB health checks to private app nodes (SG must allow ALB's CIDR) 43 egress { 44 description = "ALB to app tier (target ports)" 45 from_port = 0 46 to_port = 0 47 protocol = "-1" 48 security_groups = [ 49 aws_security_group.app.id, 50 ] 51 } 52 53 # SSH from ops CIDRs to app nodes (ALB forwards? No — SSH goes to nodes 54 # directly; this rule is on the APP SG, not edge. Kept here as a no-op 55 # comment for clarity: see aws_security_group.app below.) 56 57 tags = { 58 Name = "${local.names.project}-edge" 59 Tier = "edge" 60 } 61} 62 63# --- App security group (EC2 nodes) -------------------------------------------- 64 65resource "aws_security_group" "app" { 66 name_prefix = "${local.names.project}-app-" 67 vpc_id = aws_vpc.main.id 68 69 # From ALB (web / api / realtime target groups + health checks) 70 ingress { 71 description = "From edge ALB (web/api/realtime + health)" 72 from_port = 0 73 to_port = 0 74 protocol = "-1" 75 security_groups = [ 76 aws_security_group.edge.id, 77 ] 78 } 79 80 # SSH from ops (node management via systemd; tighten var.allowed_ssh_cidrs) 81 ingress { 82 description = "SSH from ops CIDRs" 83 from_port = 22 84 to_port = 22 85 protocol = "tcp" 86 cidr_blocks = var.allowed_ssh_cidrs 87 } 88 89 # App ↔ app (internal service calls: web→api, api→realtime, worker→api) 90 ingress { 91 description = "App tier to app tier" 92 from_port = 0 93 to_port = 0 94 protocol = "-1" 95 security_groups = [ 96 aws_security_group.app.id, 97 ] 98 } 99 100 # To data tier (postgres, redis, opensearch, s3) 101 egress { 102 description = "App to data tier" 103 from_port = 0 104 to_port = 0 105 protocol = "-1" 106 security_groups = [ 107 aws_security_group.data.id, 108 ] 109 } 110 111 # Outbound to internet for package managers / image pulls (via NAT) 112 egress { 113 description = "App outbound (NAT)" 114 from_port = 0 115 to_port = 0 116 protocol = "-1" 117 cidr_blocks = ["0.0.0.0/0"] 118 } 119 120 tags = { 121 Name = "${local.names.project}-app" 122 Tier = "app" 123 } 124} 125 126# --- Data security group (RDS / ElastiCache / OpenSearch) ------------------------ 127 128resource "aws_security_group" "data" { 129 name_prefix = "${local.names.project}-data-" 130 vpc_id = aws_vpc.main.id 131 132 # From app tier only 133 ingress { 134 description = "From app tier" 135 from_port = 0 136 to_port = 0 137 protocol = "-1" 138 security_groups = [ 139 aws_security_group.app.id, 140 ] 141 } 142 143 # RDS needs outbound for patching etc. 144 egress { 145 description = "Data tier outbound" 146 from_port = 0 147 to_port = 0 148 protocol = "-1" 149 cidr_blocks = ["0.0.0.0/0"] 150 } 151 152 tags = { 153 Name = "${local.names.project}-data" 154 Tier = "data" 155 } 156} 157 158# --- KMS key (application secrets + S3 SSE) -------------------------------------- 159# This key encrypts: the S3 uploads bucket (SSE-KMS), the RDS storage, the 160# ElastiCache at-rest, and (via the app's MASTER_ENCRYPTION_KEY flow) any 161# application-level secret encryption at rest. 162 163resource "aws_kms_key" "app" { 164 description = "${local.names.project} application secrets and data-at-rest encryption" 165 deletion_window_in_days = 30 166 enable_key_rotation = true 167 168 # Allow the EC2 role (app tier) and S3 to use the key. 169 # The IAM role is defined in compute.tf; reference it by name via data. 170 # (KMS key policies use ARNs; we build them from the known role name.) 171 tags = { 172 Name = local.names.kms 173 } 174} 175 176resource "aws_kms_alias" "app" { 177 name = "alias/${local.names.kms}" 178 target_key_id = aws_kms_key.app.key_id 179} 180 181# The KMS key policy grants the app IAM role (defined in compute.tf) and S3 182# (for SSE). We keep the policy minimal and reference the role ARN that 183# compute.tf creates. 184resource "aws_kms_grant" "app_role" { 185 depends_on = [aws_iam_role.app] 186 187 key_id = aws_kms_key.app.key_id 188 principal = aws_iam_role.app.arn 189 operations = [ 190 "kms:Encrypt", 191 "kms:Decrypt", 192 "kms:ReEncrypt*", 193 "kms:GenerateDataKey*", 194 "kms:DescribeKey", 195 ] 196} 197 198# --- S3 bucket policy ---------------------------------------------------------- 199# The uploads bucket itself is created in data.tf (aws_s3_bucket.uploads). 200# The policy below enforces: 201# * TLS-only access (aws:SecureTransport = true) 202# * Bucket owner full control (implicit via resource ownership) 203# * (Optionally) deny non-TLS — covered by the condition above. 204# 205# The bucket is PRIVATE by default (no public access block relaxation). 206# Presigned URLs from the API grant temporary per-object access. 207 208resource "aws_s3_bucket_policy" "uploads" { 209 bucket = aws_s3_bucket.uploads.id 210 211 policy = jsonencode({ 212 Version = "2012-10-17" 213 Statement = [ 214 { 215 Sid = "DenyInsecureTransport" 216 Effect = "Deny" 217 Principal = "*" 218 Action = "s3:*" 219 Resource = [ 220 aws_s3_bucket.uploads.arn, 221 "${aws_s3_bucket.uploads.arn}/*" 222 ] 223 Condition = { 224 Bool = { 225 "aws:SecureTransport" = "false" 226 } 227 } 228 } 229 ] 230 }) 231} 232