Scenario Advanced Aws AWS Networking

Design a PCI-DSS Compliant VPC for a Multi-Tier Application

Design a VPC architecture with isolated PCI cardholder data environment (CDE), network segmentation using NACLs and Security Groups, WAF, and Shield Advanced.

January 20, 2025 6 min read ~35 min to complete DB
The Situation

Your company processes credit card payments and must pass a PCI-DSS Level 1 assessment. The QSA (Qualified Security Assessor) requires network segmentation that isolates the Card Data Environment (CDE) from all other systems. You need to design the VPC architecture, prove network isolation with technical controls, and document your compensating controls.

6 Steps
7 Services Used
~35 min Duration
Advanced Difficulty

The Problem

PCI-DSS requires that systems in the Cardholder Data Environment (CDE) be isolated from all other systems. “Isolated” means network controls that provably prevent unauthorized traffic — not just a firewall rule that could be misconfigured. AWS gives you multiple overlapping controls: NACLs, security groups, route tables, and VPC endpoints.

PCI-DSS Requirement
Requirement 1: Install and maintain network security controls.
Requirement 1.3: Network access controls restrict inbound and outbound traffic to only that which is necessary.
Requirement 1.3.2: Restrict inbound traffic to system components in the CDE to that which is necessary.

VPC Architecture

VPC: 10.0.0.0/16
│
├── Public Subnets (10.0.0.0/24, 10.0.1.0/24, 10.0.2.0/24) — 3 AZs
│   ├── ALB (internet-facing, WAF + Shield attached)
│   ├── NAT Gateway (HA across AZs)
│   └── Bastion Host (replaced by Session Manager — no SSH port open)
│
├── Private App Subnets (10.0.10.0/24, 10.0.11.0/24, 10.0.12.0/24)
│   ├── ECS/EKS Worker Nodes (non-PCI services)
│   └── Lambda Functions (VPC-attached)
│
├── Private Data Subnets (10.0.20.0/24, 10.0.21.0/24, 10.0.22.0/24)
│   ├── RDS Cluster (Multi-AZ) — non-cardholder data only
│   ├── ElastiCache
│   └── OpenSearch
│
└── Isolated PCI Subnet (10.0.30.0/24, 10.0.31.0/24) — CDE
    ├── Payment Processing Service ONLY
    ├── Tokenization Service
    ├── No internet route table — ever
    ├── Dedicated NACLs (deny-all default, explicit allow only)
    └── Only reachable from Private App Subnet on port 8443

Step 1: Route Table Isolation for CDE Subnet

The CDE subnet route table has no route to the internet gateway or NAT gateway — a misconfigured security group cannot override a missing route:

# Create route table for CDE subnets (no internet route)
aws ec2 create-route-table \
  --vpc-id vpc-0abc123 \
  --tag-specifications 'ResourceType=route-table,Tags=[{Key=Name,Value=rtb-pci-cde}]'

# Add ONLY local VPC route (10.0.0.0/16 → local)
# Do NOT add: 0.0.0.0/0 → igw or 0.0.0.0/0 → nat

# Associate with CDE subnets
aws ec2 associate-route-table \
  --subnet-id subnet-pci-az1 \
  --route-table-id rtb-pci-cde

aws ec2 associate-route-table \
  --subnet-id subnet-pci-az2 \
  --route-table-id rtb-pci-cde

This guarantees that even if every security group is accidentally set to 0.0.0.0/0, the CDE cannot reach the internet.

Step 2: Network ACLs — Deny-All Default for CDE

NACLs are stateless and process rules in order. Set a deny-all default and add only necessary allows:

resource "aws_network_acl" "pci_cde" {
  vpc_id     = aws_vpc.main.id
  subnet_ids = [aws_subnet.pci_az1.id, aws_subnet.pci_az2.id]

  tags = { Name = "nacl-pci-cde" }
}

# Allow inbound from App tier on port 8443 only
resource "aws_network_acl_rule" "pci_allow_inbound_app" {
  network_acl_id = aws_network_acl.pci_cde.id
  rule_number    = 100
  egress         = false
  protocol       = "tcp"
  rule_action    = "allow"
  cidr_block     = "10.0.10.0/22"   # App subnet CIDR only
  from_port      = 8443
  to_port        = 8443
}

# Allow return traffic (ephemeral ports)
resource "aws_network_acl_rule" "pci_allow_return_traffic" {
  network_acl_id = aws_network_acl.pci_cde.id
  rule_number    = 110
  egress         = true
  protocol       = "tcp"
  rule_action    = "allow"
  cidr_block     = "10.0.10.0/22"
  from_port      = 1024
  to_port        = 65535
}

# Deny everything else (explicit — belt AND suspenders)
resource "aws_network_acl_rule" "pci_deny_all_inbound" {
  network_acl_id = aws_network_acl.pci_cde.id
  rule_number    = 32766
  egress         = false
  protocol       = "-1"
  rule_action    = "deny"
  cidr_block     = "0.0.0.0/0"
}

resource "aws_network_acl_rule" "pci_deny_all_outbound" {
  network_acl_id = aws_network_acl.pci_cde.id
  rule_number    = 32766
  egress         = true
  protocol       = "-1"
  rule_action    = "deny"
  cidr_block     = "0.0.0.0/0"
}

Step 3: Security Groups — Least Privilege per Service

# CDE payment service security group
resource "aws_security_group" "pci_payment_service" {
  name   = "sg-pci-payment-service"
  vpc_id = aws_vpc.main.id

  ingress {
    from_port       = 8443
    to_port         = 8443
    protocol        = "tcp"
    security_groups = [aws_security_group.app_tier.id]  # Only from app tier SG
    description     = "Payment API from app tier only"
  }

  # Egress: only to tokenization service and secrets manager VPC endpoint
  egress {
    from_port       = 443
    to_port         = 443
    protocol        = "tcp"
    security_groups = [aws_security_group.pci_tokenization.id]
    description     = "Tokenization service"
  }

  egress {
    from_port       = 443
    to_port         = 443
    protocol        = "tcp"
    prefix_list_ids = [data.aws_ec2_managed_prefix_list.secretsmanager.id]
    description     = "Secrets Manager VPC endpoint"
  }

  tags = { Name = "sg-pci-payment-service", PCIScope = "in-scope" }
}

Step 4: VPC Endpoints — No Internet Egress for PCI Data

The CDE must reach AWS services (Secrets Manager, KMS, CloudWatch Logs) without an internet path:

# Interface VPC endpoint for Secrets Manager
resource "aws_vpc_endpoint" "secretsmanager" {
  vpc_id              = aws_vpc.main.id
  service_name        = "com.amazonaws.us-east-1.secretsmanager"
  vpc_endpoint_type   = "Interface"
  subnet_ids          = [aws_subnet.pci_az1.id, aws_subnet.pci_az2.id]
  security_group_ids  = [aws_security_group.vpc_endpoint_sg.id]
  private_dns_enabled = true

  tags = { Name = "vpce-secretsmanager-pci" }
}

# KMS endpoint — for decrypting card data
resource "aws_vpc_endpoint" "kms" {
  vpc_id              = aws_vpc.main.id
  service_name        = "com.amazonaws.us-east-1.kms"
  vpc_endpoint_type   = "Interface"
  subnet_ids          = [aws_subnet.pci_az1.id, aws_subnet.pci_az2.id]
  security_group_ids  = [aws_security_group.vpc_endpoint_sg.id]
  private_dns_enabled = true
}

Step 5: WAF on Public ALB

Attach WAF to the internet-facing ALB to protect against OWASP Top 10 attacks:

resource "aws_wafv2_web_acl" "public_alb" {
  name  = "public-alb-waf"
  scope = "REGIONAL"

  default_action { allow {} }

  # AWS Managed Rules — OWASP Top 10
  rule {
    name     = "AWSManagedRulesCommonRuleSet"
    priority = 1
    override_action { none {} }
    statement {
      managed_rule_group_statement {
        name        = "AWSManagedRulesCommonRuleSet"
        vendor_name = "AWS"
      }
    }
    visibility_config {
      cloudwatch_metrics_enabled = true
      metric_name                = "CommonRuleSet"
      sampled_requests_enabled   = true
    }
  }

  # AWS Managed Rules — Known Bad Inputs (SQL injection, XSS, etc.)
  rule {
    name     = "AWSManagedRulesKnownBadInputsRuleSet"
    priority = 2
    override_action { none {} }
    statement {
      managed_rule_group_statement {
        name        = "AWSManagedRulesKnownBadInputsRuleSet"
        vendor_name = "AWS"
      }
    }
    visibility_config {
      cloudwatch_metrics_enabled = true
      metric_name                = "KnownBadInputs"
      sampled_requests_enabled   = true
    }
  }

  visibility_config {
    cloudwatch_metrics_enabled = true
    metric_name                = "PublicALBWebACL"
    sampled_requests_enabled   = true
  }
}

resource "aws_wafv2_web_acl_association" "alb" {
  resource_arn = aws_lb.public.arn
  web_acl_arn  = aws_wafv2_web_acl.public_alb.arn
}

Step 6: VPC Flow Logs — Evidence for Auditors

PCI-DSS requires logging of all network traffic to and from the CDE:

resource "aws_flow_log" "pci_cde" {
  iam_role_arn    = aws_iam_role.flow_log.arn
  log_destination = aws_cloudwatch_log_group.pci_flow_logs.arn
  traffic_type    = "ALL"
  subnet_id       = aws_subnet.pci_az1.id   # Log per-subnet for CDE
}

# Retention: PCI-DSS requires 12 months minimum
resource "aws_cloudwatch_log_group" "pci_flow_logs" {
  name              = "/aws/vpc/flowlogs/pci-cde"
  retention_in_days = 365
  kms_key_id        = aws_kms_key.log_encryption.arn
}

PCI-DSS Evidence Checklist

RequirementTechnical ControlEvidence
1.3: Restrict inbound CDE trafficNACL deny-all + allow port 8443 from App tier onlyNACL configuration export
1.3.2: No direct public access to CDERoute table has no IGW/NAT routeRoute table screenshot + Terraform state
4.2.1: Encrypt cardholder data in transitAll communication on port 443/8443 with TLS 1.2+ACM certificate + ALB listener config
10.2: Audit logs for CDEVPC Flow Logs + CloudTrail enabledLog group with 365-day retention
6.4: WAF protecting public web applicationsWAF v2 with OWASP managed rules on ALBWAF web ACL configuration
Interview Angle
Mention defense in depth: route tables → NACLs → security groups → WAF. Each layer compensates for the others. A single misconfigured security group can’t bypass a NACL. A NACL bypass can’t reach the internet without a route table entry. Auditors want to see multiple independent controls, not one control you’re betting everything on.
Services Used
VPCNetwork ACLsSecurity GroupsAWS WAFAWS Shield AdvancedVPC Flow LogsPrivateLink
Prerequisites
  • Understanding of VPC fundamentals (subnets, route tables, IGW)
  • Basic knowledge of security groups and NACLs
  • Familiarity with PCI-DSS network segmentation requirements
What You Learned
  • How to segment a VPC into public, private app, private data, and isolated PCI layers
  • The difference between NACLs (stateless) and Security Groups (stateful) for defense in depth
  • Why VPC endpoints eliminate internet egress for PCI data flows
  • How to configure WAF for OWASP Top 10 protection on public-facing ALBs
  • What evidence to collect for a PCI-DSS network segmentation audit

Have a similar scenario to share?

Production incidents are the best teachers. Submit your real-world scenario and help others learn.

Open Google Form

Related Scenarios

Learning Paths beginner

AWS Cloud Engineer Learning Path

Who Is This Path For? This path is designed for complete beginners who want to break into cloud computing as an AWS engineer. If you know …

Jan 20, 2025 Read more