Security groups control network traffic at the resource level. The ADMA infrastructure implements a least-privilege security model with explicit allow rules and no direct internet access to backend services.
Security Architecture
Traffic Flow Overview
Internet
│
├─ HTTP/HTTPS (80/443)
▼
[sg-alb] Application Load Balancer
│
├─ Port 80 → Frontend
▼
[sg-frontend] Frontend Tasks (Nginx)
│
├─ Port 8080 → Backend
├─ Port 443 → VPC Endpoints
▼
[sg-backend] Backend Tasks (Spring Boot)
│
├─ Port 5432 → Database
├─ Port 443 → VPC Endpoints
▼
[sg-db] RDS PostgreSQL
Security Groups Module
All security groups are created in the modules/security module:
# modules/security/main.tf
locals {
name_prefix = " ${ var . project_name } - ${ var . environment } "
}
# Five security groups with distinct responsibilities
resource "aws_security_group" "alb" { ... }
resource "aws_security_group" "frontend" { ... }
resource "aws_security_group" "backend" { ... }
resource "aws_security_group" "db" { ... }
resource "aws_security_group" "vpc_endpoint" { ... }
ALB Security Group
The ALB security group allows public access to the application:
resource "aws_security_group" "alb" {
name = " ${ local . name_prefix } -alb-sg"
description = "Allow public ingress to frontend ALB"
vpc_id = var . vpc_id
tags = merge (var . tags , {
Name = " ${ local . name_prefix } -alb-sg"
})
}
Ingress Rules
HTTP (Port 80)
HTTPS (Port 443)
resource "aws_vpc_security_group_ingress_rule" "alb_http" {
for_each = toset (var . alb_ingress_cidrs )
security_group_id = aws_security_group . alb . id
ip_protocol = "tcp"
from_port = 80
to_port = 80
cidr_ipv4 = each . value
description = "Public HTTP access"
}
Default : Allows 0.0.0.0/0 (all internet traffic)Customization : Restrict to specific CIDR blocks:alb_ingress_cidrs = [
"203.0.113.0/24" , # Office network
"198.51.100.0/24" # VPN network
]
resource "aws_vpc_security_group_ingress_rule" "alb_https" {
for_each = var . allow_https_from_cidr ? toset (var . alb_ingress_cidrs ) : toset ([])
security_group_id = aws_security_group . alb . id
ip_protocol = "tcp"
from_port = 443
to_port = 443
cidr_ipv4 = each . value
description = "Public HTTPS access"
}
Conditional : Only created when enable_https = true
Egress Rules
ALB can only communicate with frontend tasks:
resource "aws_vpc_security_group_egress_rule" "alb_to_frontend" {
security_group_id = aws_security_group . alb . id
ip_protocol = "tcp"
from_port = var . frontend_port
to_port = var . frontend_port
referenced_security_group_id = aws_security_group . frontend . id
description = "ALB forwards traffic only to frontend service"
}
Security : ALB has NO direct route to backend. This enforces frontend as the entry point.
Summary Table
Direction Protocol Port Source/Destination Description Inbound TCP 80 0.0.0.0/0Public HTTP Inbound TCP 443 0.0.0.0/0Public HTTPS (if enabled) Outbound TCP 80 sg-frontendForward to frontend
Frontend Security Group
The frontend security group isolates the Nginx containers:
resource "aws_security_group" "frontend" {
name = " ${ local . name_prefix } -frontend-sg"
description = "Allow ALB ingress to frontend tasks"
vpc_id = var . vpc_id
tags = merge (var . tags , {
Name = " ${ local . name_prefix } -frontend-sg"
})
}
Ingress Rules
Only the ALB can reach frontend tasks:
resource "aws_vpc_security_group_ingress_rule" "frontend_from_alb" {
security_group_id = aws_security_group . frontend . id
ip_protocol = "tcp"
from_port = var . frontend_port
to_port = var . frontend_port
referenced_security_group_id = aws_security_group . alb . id
description = "Allow ALB traffic to frontend"
}
Egress Rules
Frontend can communicate with backend, VPC endpoints, and DNS:
To Backend
To VPC Endpoints
DNS Resolution
resource "aws_vpc_security_group_egress_rule" "frontend_to_backend" {
security_group_id = aws_security_group . frontend . id
ip_protocol = "tcp"
from_port = var . backend_port
to_port = var . backend_port
referenced_security_group_id = aws_security_group . backend . id
description = "Frontend may call backend service"
}
Purpose : Nginx reverse proxy forwards /api/* and /auth/* to backend via service discovery.resource "aws_vpc_security_group_egress_rule" "frontend_to_vpce" {
security_group_id = aws_security_group . frontend . id
ip_protocol = "tcp"
from_port = 443
to_port = 443
referenced_security_group_id = aws_security_group . vpc_endpoint . id
description = "Frontend task runtime access to AWS API endpoints"
}
Purpose : ECS agent needs ECR/CloudWatch/Secrets Manager access during task lifecycle.resource "aws_vpc_security_group_egress_rule" "frontend_dns_udp" {
security_group_id = aws_security_group . frontend . id
ip_protocol = "udp"
from_port = 53
to_port = 53
cidr_ipv4 = var . vpc_cidr
description = "DNS resolution in VPC"
}
resource "aws_vpc_security_group_egress_rule" "frontend_dns_tcp" {
security_group_id = aws_security_group . frontend . id
ip_protocol = "tcp"
from_port = 53
to_port = 53
cidr_ipv4 = var . vpc_cidr
description = "DNS resolution in VPC"
}
Purpose : Service discovery, VPC endpoint resolution, RDS endpoint lookup.
Summary Table
Direction Protocol Port Source/Destination Description Inbound TCP 80 sg-albALB traffic Outbound TCP 8080 sg-backendCall backend API Outbound TCP 443 sg-vpc-endpointAWS services Outbound TCP/UDP 53 VPC CIDR DNS resolution
Backend Security Group
The backend security group protects Spring Boot application containers:
resource "aws_security_group" "backend" {
name = " ${ local . name_prefix } -backend-sg"
description = "Allow ingress to backend only from frontend tasks"
vpc_id = var . vpc_id
tags = merge (var . tags , {
Name = " ${ local . name_prefix } -backend-sg"
})
}
Ingress Rules
Only frontend tasks can reach the backend:
resource "aws_vpc_security_group_ingress_rule" "backend_from_frontend" {
security_group_id = aws_security_group . backend . id
ip_protocol = "tcp"
from_port = var . backend_port
to_port = var . backend_port
referenced_security_group_id = aws_security_group . frontend . id
description = "Only frontend can reach backend"
}
Critical Security Control : Backend has NO public ingress. It cannot be accessed directly from the internet, even if someone discovers the private IP.
Egress Rules
To Database
To VPC Endpoints
DNS Resolution
resource "aws_vpc_security_group_egress_rule" "backend_to_db" {
security_group_id = aws_security_group . backend . id
ip_protocol = "tcp"
from_port = var . db_port
to_port = var . db_port
referenced_security_group_id = aws_security_group . db . id
description = "Backend may reach database"
}
Purpose : PostgreSQL connections from Spring Boot DataSource.resource "aws_vpc_security_group_egress_rule" "backend_to_vpce" {
security_group_id = aws_security_group . backend . id
ip_protocol = "tcp"
from_port = 443
to_port = 443
referenced_security_group_id = aws_security_group . vpc_endpoint . id
description = "Backend task runtime access to AWS API endpoints"
}
Purpose : Read secrets from Secrets Manager, write logs to CloudWatch.resource "aws_vpc_security_group_egress_rule" "backend_dns_udp" {
security_group_id = aws_security_group . backend . id
ip_protocol = "udp"
from_port = 53
to_port = 53
cidr_ipv4 = var . vpc_cidr
description = "DNS resolution in VPC"
}
resource "aws_vpc_security_group_egress_rule" "backend_dns_tcp" {
security_group_id = aws_security_group . backend . id
ip_protocol = "tcp"
from_port = 53
to_port = 53
cidr_ipv4 = var . vpc_cidr
description = "DNS resolution in VPC"
}
Summary Table
Direction Protocol Port Source/Destination Description Inbound TCP 8080 sg-frontendFrontend requests Outbound TCP 5432 sg-dbDatabase queries Outbound TCP 443 sg-vpc-endpointAWS services Outbound TCP/UDP 53 VPC CIDR DNS resolution
Database Security Group
The database security group restricts PostgreSQL access:
resource "aws_security_group" "db" {
name = " ${ local . name_prefix } -db-sg"
description = "Allow database access only from backend tasks"
vpc_id = var . vpc_id
tags = merge (var . tags , {
Name = " ${ local . name_prefix } -db-sg"
})
}
Ingress Rules
Only backend tasks can connect to the database:
resource "aws_vpc_security_group_ingress_rule" "db_from_backend" {
security_group_id = aws_security_group . db . id
ip_protocol = "tcp"
from_port = var . db_port
to_port = var . db_port
referenced_security_group_id = aws_security_group . backend . id
description = "Backend-only DB access"
}
Egress Rules
resource "aws_vpc_security_group_egress_rule" "db_internal" {
security_group_id = aws_security_group . db . id
ip_protocol = "-1"
cidr_ipv4 = var . vpc_cidr
description = "Allow DB response traffic inside VPC"
}
Design Note : RDS needs to send response packets back to backend. Egress is restricted to VPC CIDR only.
Summary Table
Direction Protocol Port Source/Destination Description Inbound TCP 5432 sg-backendPostgreSQL connections Outbound All All VPC CIDR Response traffic
VPC Endpoint Security Group
The VPC endpoint security group controls access to AWS services:
resource "aws_security_group" "vpc_endpoint" {
name = " ${ local . name_prefix } -vpce-sg"
description = "Restrict interface endpoint access to ECS tasks"
vpc_id = var . vpc_id
tags = merge (var . tags , {
Name = " ${ local . name_prefix } -vpce-sg"
})
}
Ingress Rules
Both frontend and backend tasks can access VPC endpoints:
resource "aws_vpc_security_group_ingress_rule" "vpce_from_frontend" {
security_group_id = aws_security_group . vpc_endpoint . id
ip_protocol = "tcp"
from_port = 443
to_port = 443
referenced_security_group_id = aws_security_group . frontend . id
description = "Frontend to VPC endpoints"
}
resource "aws_vpc_security_group_ingress_rule" "vpce_from_backend" {
security_group_id = aws_security_group . vpc_endpoint . id
ip_protocol = "tcp"
from_port = 443
to_port = 443
referenced_security_group_id = aws_security_group . backend . id
description = "Backend to VPC endpoints"
}
Egress Rules
resource "aws_vpc_security_group_egress_rule" "vpce_all" {
security_group_id = aws_security_group . vpc_endpoint . id
ip_protocol = "-1"
cidr_ipv4 = "0.0.0.0/0"
description = "Endpoint-managed return traffic"
}
Summary Table
Direction Protocol Port Source/Destination Description Inbound TCP 443 sg-frontendFrontend task API calls Inbound TCP 443 sg-backendBackend task API calls Outbound All All 0.0.0.0/0AWS-managed responses
Security Group Relationships
Visual representation of security group references:
┌──────────┐
│ sg-alb │
└────┬─────┘
│ (port 80)
▼
┌──────────────┐
│ sg-frontend │
└──────┬───────┘
│
├── (port 8080) ──▶ ┌──────────────┐
│ │ sg-backend │
│ └──────┬───────┘
│ │ (port 5432)
│ ▼
│ ┌──────────────┐
│ │ sg-db │
│ └──────────────┘
│
└── (port 443) ────▶ ┌──────────────┐
│ sg-vpc-endpt │◀── (port 443) ── sg-backend
└──────────────┘
Security Best Practices
Implemented Controls
Each security group allows only the minimum required traffic:
✅ No 0.0.0.0/0 egress rules (except VPC endpoints)
✅ No 0.0.0.0/0 ingress rules to private resources
✅ Security group references instead of CIDR blocks where possible
✅ Explicit ports (no port ranges like 1-65535)
Security is layered:
Network layer : Private subnets with no internet route
Security groups : Explicit allow rules
Application layer : JWT authentication, CORS restrictions
Data layer : TLS-enforced connections, encrypted at rest
Frontend cannot access database directly
Backend cannot receive public internet traffic
VPC endpoints restricted to specific security groups
ALB cannot reach backend directly
Production Hardening
For production deployments, consider:
IP Allowlisting
VPC Flow Logs
WAF Integration
Restrict ALB access to known networks: alb_ingress_cidrs = [
"203.0.113.0/24" , # Corporate office
"198.51.100.0/24" , # VPN gateway
"192.0.2.50/32" # DevOps bastion
]
Enable VPC Flow Logs for security monitoring: # Add to modules/network/main.tf
resource "aws_flow_log" "vpc" {
vpc_id = aws_vpc . this . id
traffic_type = "ALL"
iam_role_arn = aws_iam_role . flow_logs . arn
log_destination = aws_cloudwatch_log_group . flow_logs . arn
}
Add AWS WAF to ALB for application-layer protection: resource "aws_wafv2_web_acl_association" "alb" {
resource_arn = aws_lb . frontend . arn
web_acl_arn = aws_wafv2_web_acl . main . arn
}
Troubleshooting
Common Issues
Backend task cannot connect to RDS
Symptoms : Backend logs show connection timeout to databaseCheck :
Backend security group has egress rule to sg-db on port 5432
DB security group has ingress rule from sg-backend on port 5432
RDS is in same VPC and private subnets
Backend task is using correct DB_HOST environment variable
Verify :# Check security group rules
aws ec2 describe-security-group-rules \
--filters Name=group-id,Values=sg-xxxx
ECS task fails to pull image
Symptoms : Task stopped with “CannotPullContainerError”Check :
Frontend/backend SG has egress to sg-vpc-endpoint on port 443
VPC endpoint SG has ingress from frontend/backend SG on port 443
VPC endpoints for ecr.api, ecr.dkr, and s3 exist
Private DNS is enabled on ECR endpoints
Verify :# Check VPC endpoints
aws ec2 describe-vpc-endpoints --filters Name=vpc-id,Values=vpc-xxxx
ALB health checks failing
Symptoms : ECS service shows unhealthy targetsCheck :
ALB security group has egress to sg-frontend on port 80
Frontend security group has ingress from sg-alb on port 80
Target group health check path is correct (/)
Frontend container is listening on port 80
Verify :# Check target health
aws elbv2 describe-target-health \
--target-group-arn arn:aws:elasticloadbalancing:...
Module Outputs
The security module exposes these security group IDs:
# modules/security/outputs.tf
output "alb_sg_id" {
description = "ID of the ALB security group"
value = aws_security_group . alb . id
}
output "frontend_sg_id" {
description = "ID of the frontend security group"
value = aws_security_group . frontend . id
}
output "backend_sg_id" {
description = "ID of the backend security group"
value = aws_security_group . backend . id
}
output "db_sg_id" {
description = "ID of the database security group"
value = aws_security_group . db . id
}
output "vpc_endpoint_sg_id" {
description = "ID of the VPC endpoint security group"
value = aws_security_group . vpc_endpoint . id
}