Skip to main content
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

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
]

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

DirectionProtocolPortSource/DestinationDescription
InboundTCP800.0.0.0/0Public HTTP
InboundTCP4430.0.0.0/0Public HTTPS (if enabled)
OutboundTCP80sg-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:
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.

Summary Table

DirectionProtocolPortSource/DestinationDescription
InboundTCP80sg-albALB traffic
OutboundTCP8080sg-backendCall backend API
OutboundTCP443sg-vpc-endpointAWS services
OutboundTCP/UDP53VPC CIDRDNS 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

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.

Summary Table

DirectionProtocolPortSource/DestinationDescription
InboundTCP8080sg-frontendFrontend requests
OutboundTCP5432sg-dbDatabase queries
OutboundTCP443sg-vpc-endpointAWS services
OutboundTCP/UDP53VPC CIDRDNS 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

DirectionProtocolPortSource/DestinationDescription
InboundTCP5432sg-backendPostgreSQL connections
OutboundAllAllVPC CIDRResponse 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

DirectionProtocolPortSource/DestinationDescription
InboundTCP443sg-frontendFrontend task API calls
InboundTCP443sg-backendBackend task API calls
OutboundAllAll0.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:
  1. Network layer: Private subnets with no internet route
  2. Security groups: Explicit allow rules
  3. Application layer: JWT authentication, CORS restrictions
  4. 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:
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
]

Troubleshooting

Common Issues

Symptoms: Backend logs show connection timeout to databaseCheck:
  1. Backend security group has egress rule to sg-db on port 5432
  2. DB security group has ingress rule from sg-backend on port 5432
  3. RDS is in same VPC and private subnets
  4. 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
Symptoms: Task stopped with “CannotPullContainerError”Check:
  1. Frontend/backend SG has egress to sg-vpc-endpoint on port 443
  2. VPC endpoint SG has ingress from frontend/backend SG on port 443
  3. VPC endpoints for ecr.api, ecr.dkr, and s3 exist
  4. Private DNS is enabled on ECR endpoints
Verify:
# Check VPC endpoints
aws ec2 describe-vpc-endpoints --filters Name=vpc-id,Values=vpc-xxxx
Symptoms: ECS service shows unhealthy targetsCheck:
  1. ALB security group has egress to sg-frontend on port 80
  2. Frontend security group has ingress from sg-alb on port 80
  3. Target group health check path is correct (/)
  4. 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
}

Build docs developers (and LLMs) love