# ============================================================================= # AI Software Factory — Security (security groups, KMS, S3 bucket policy) # # Security group model (3 tiers, matching the k8s namespace segmentation): # # edge — internet-facing: web ALB (443/80 in), 22 to ops CIDRs (out to # private app nodes). The ALB is the ONLY public entry point. # app — application tier: EC2 nodes (web/api/worker/realtime), ALB→node # (3000/4000/4001 + health 8080), node→data tier (5432/6379/9200/ # 9000-S3), node→node (4000/4001 for internal service calls). # data — data tier: RDS, ElastiCache, OpenSearch. Ingress from `app` SG # ONLY; no internet, no ingress from `edge`. # # The S3 uploads bucket is reachable from app nodes via the S3 VPC endpoint # (network.tf) and from the ALB/worker for presigned flows. # ============================================================================= # --- Edge security group (ALB) ------------------------------------------------- resource "aws_security_group" "edge" { name_prefix = "${local.names.project}-edge-" vpc_id = aws_vpc.main.id # HTTPS in from the internet ingress { description = "HTTPS from internet to ALB" from_port = 443 to_port = 443 protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] } # HTTP in (only for the 443 redirect) ingress { description = "HTTP from internet to ALB (redirect to HTTPS)" from_port = 80 to_port = 80 protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] } # ALB health checks to private app nodes (SG must allow ALB's CIDR) egress { description = "ALB to app tier (target ports)" from_port = 0 to_port = 0 protocol = "-1" security_groups = [ aws_security_group.app.id, ] } # SSH from ops CIDRs to app nodes (ALB forwards? No — SSH goes to nodes # directly; this rule is on the APP SG, not edge. Kept here as a no-op # comment for clarity: see aws_security_group.app below.) tags = { Name = "${local.names.project}-edge" Tier = "edge" } } # --- App security group (EC2 nodes) -------------------------------------------- resource "aws_security_group" "app" { name_prefix = "${local.names.project}-app-" vpc_id = aws_vpc.main.id # From ALB (web / api / realtime target groups + health checks) ingress { description = "From edge ALB (web/api/realtime + health)" from_port = 0 to_port = 0 protocol = "-1" security_groups = [ aws_security_group.edge.id, ] } # SSH from ops (node management via systemd; tighten var.allowed_ssh_cidrs) ingress { description = "SSH from ops CIDRs" from_port = 22 to_port = 22 protocol = "tcp" cidr_blocks = var.allowed_ssh_cidrs } # App ↔ app (internal service calls: web→api, api→realtime, worker→api) ingress { description = "App tier to app tier" from_port = 0 to_port = 0 protocol = "-1" security_groups = [ aws_security_group.app.id, ] } # To data tier (postgres, redis, opensearch, s3) egress { description = "App to data tier" from_port = 0 to_port = 0 protocol = "-1" security_groups = [ aws_security_group.data.id, ] } # Outbound to internet for package managers / image pulls (via NAT) egress { description = "App outbound (NAT)" from_port = 0 to_port = 0 protocol = "-1" cidr_blocks = ["0.0.0.0/0"] } tags = { Name = "${local.names.project}-app" Tier = "app" } } # --- Data security group (RDS / ElastiCache / OpenSearch) ------------------------ resource "aws_security_group" "data" { name_prefix = "${local.names.project}-data-" vpc_id = aws_vpc.main.id # From app tier only ingress { description = "From app tier" from_port = 0 to_port = 0 protocol = "-1" security_groups = [ aws_security_group.app.id, ] } # RDS needs outbound for patching etc. egress { description = "Data tier outbound" from_port = 0 to_port = 0 protocol = "-1" cidr_blocks = ["0.0.0.0/0"] } tags = { Name = "${local.names.project}-data" Tier = "data" } } # --- KMS key (application secrets + S3 SSE) -------------------------------------- # This key encrypts: the S3 uploads bucket (SSE-KMS), the RDS storage, the # ElastiCache at-rest, and (via the app's MASTER_ENCRYPTION_KEY flow) any # application-level secret encryption at rest. resource "aws_kms_key" "app" { description = "${local.names.project} application secrets and data-at-rest encryption" deletion_window_in_days = 30 enable_key_rotation = true # Allow the EC2 role (app tier) and S3 to use the key. # The IAM role is defined in compute.tf; reference it by name via data. # (KMS key policies use ARNs; we build them from the known role name.) tags = { Name = local.names.kms } } resource "aws_kms_alias" "app" { name = "alias/${local.names.kms}" target_key_id = aws_kms_key.app.key_id } # The KMS key policy grants the app IAM role (defined in compute.tf) and S3 # (for SSE). We keep the policy minimal and reference the role ARN that # compute.tf creates. resource "aws_kms_grant" "app_role" { depends_on = [aws_iam_role.app] key_id = aws_kms_key.app.key_id principal = aws_iam_role.app.arn operations = [ "kms:Encrypt", "kms:Decrypt", "kms:ReEncrypt*", "kms:GenerateDataKey*", "kms:DescribeKey", ] } # --- S3 bucket policy ---------------------------------------------------------- # The uploads bucket itself is created in data.tf (aws_s3_bucket.uploads). # The policy below enforces: # * TLS-only access (aws:SecureTransport = true) # * Bucket owner full control (implicit via resource ownership) # * (Optionally) deny non-TLS — covered by the condition above. # # The bucket is PRIVATE by default (no public access block relaxation). # Presigned URLs from the API grant temporary per-object access. resource "aws_s3_bucket_policy" "uploads" { bucket = aws_s3_bucket.uploads.id policy = jsonencode({ Version = "2012-10-17" Statement = [ { Sid = "DenyInsecureTransport" Effect = "Deny" Principal = "*" Action = "s3:*" Resource = [ aws_s3_bucket.uploads.arn, "${aws_s3_bucket.uploads.arn}/*" ] Condition = { Bool = { "aws:SecureTransport" = "false" } } } ] }) }