⚡
Milan.dev
>Home>Projects>Experience>Blog
GitHubLinkedIn
status: building
>Home>Projects>Experience>Blog
status: building

Connect

Let's collaborate on infrastructure challenges

Open to discussing DevOps strategies, cloud architecture optimization, security implementations, and interesting infrastructure problems.

send a message→

Find me elsewhere

GitHub
@milandangol
LinkedIn
/in/milan-dangol
Email
milandangol57@gmail.com
Forged with& code

© 2026 Milan Dangol — All systems reserved

back to blog
security

Designing a PCI-DSS Compliant Platform on AWS

Architecting a PCI-DSS Level 1 compliant platform on AWS - featuring network segmentation, encryption everywhere, comprehensive audit logging, automated compliance checks, and Security Hub dashboards for continuous compliance monitoring.

M

Milan Dangol

Sr DevOps & DevSecOps Engineer

Jul 13, 2025
14 min read

Introduction

PCI-DSS compliance isn't optional when handling payment card data. I built a platform that processes millions of transactions while maintaining Level 1 compliance - the highest certification level. This required rethinking everything from network architecture to logging.

This post covers the technical implementation of:

  • Network segmentation - Isolating the Cardholder Data Environment (CDE)
  • Encryption everywhere - Data at rest, in transit, and in use
  • Comprehensive logging - Every access, every action, forever
  • Automated compliance - Continuous monitoring with Security Hub
  • Access controls - Least privilege, MFA, just-in-time access

Architecture Overview

flowchart TB subgraph Internet["Internet"] CUSTOMER[Customer] PARTNER[Payment Partners] end subgraph AWSCloud["AWS Cloud - PCI Scope"] subgraph EdgeLayer["Edge Layer"] WAF[AWS WAF] SHIELD[AWS Shield] CF[CloudFront] end subgraph DMZ["DMZ VPC - 10.100.0.0/16"] ALB[Application Load Balancer] APIGW[API Gateway] end subgraph CDE["Cardholder Data Environment - 10.200.0.0/16"] subgraph CDEPrivate["Private Subnets"] EKS_CDE[EKS - Payment Services] TOKENIZER[Tokenization Service] HSM[CloudHSM] end subgraph CDEData["Data Subnets"] RDS_CDE[(RDS - Encrypted)] REDIS_CDE[ElastiCache - Encrypted] end end subgraph NonCDE["Non-CDE VPC - 10.300.0.0/16"] EKS_APP[EKS - Non-sensitive Apps] RDS_APP[(RDS - Non-PCI)] end subgraph Security["Security VPC - 10.240.0.0/16"] SIEM[SIEM / Splunk] VAULT[HashiCorp Vault] SCANNER[Vulnerability Scanner] end subgraph Logging["Centralized Logging"] CLOUDTRAIL[(CloudTrail)] FLOWLOGS[(VPC Flow Logs)] APPLOGS[(Application Logs)] S3_LOGS[(S3 - Immutable)] end end CUSTOMER --> CF --> WAF --> ALB PARTNER --> APIGW ALB --> EKS_CDE APIGW --> EKS_CDE EKS_CDE --> TOKENIZER TOKENIZER --> HSM EKS_CDE --> RDS_CDE EKS_CDE --> SIEM CLOUDTRAIL --> S3_LOGS FLOWLOGS --> S3_LOGS style CDE fill:#e63946,stroke:#fff,stroke-width:3px,color:#fff style DMZ fill:#f77f00,stroke:#fff,stroke-width:2px,color:#fff style NonCDE fill:#2a9d8f,stroke:#fff,stroke-width:2px,color:#fff style Security fill:#264653,stroke:#fff,stroke-width:2px,color:#fff style Logging fill:#1a1a2e,stroke:#00d9ff,stroke-width:2px,color:#fff

PCI-DSS Requirements Mapping

flowchart LR subgraph Requirements["PCI-DSS Requirements"] direction TB R1["Req 1: Firewall"] R2["Req 2: No Defaults"] R3["Req 3: Protect CHD"] R4["Req 4: Encrypt Transit"] R5["Req 5: Anti-malware"] R6["Req 6: Secure Systems"] R7["Req 7: Access Control"] R8["Req 8: Authentication"] R9["Req 9: Physical"] R10["Req 10: Logging"] R11["Req 11: Testing"] R12["Req 12: Policies"] end subgraph Implementation["AWS Implementation"] direction TB I1["Security Groups<br/>NACLs<br/>Network Firewall"] I2["Config Rules<br/>SSM Automation<br/>AMI Hardening"] I3["KMS Encryption<br/>CloudHSM<br/>Tokenization"] I4["TLS 1.3<br/>ACM Certificates<br/>PrivateLink"] I5["GuardDuty<br/>Inspector<br/>ECR Scanning"] I6["Patch Manager<br/>CodeGuru<br/>SAST/DAST"] I7["IAM Policies<br/>RBAC<br/>Attribute-based"] I8["MFA<br/>SSO<br/>Short-lived creds"] I9["AWS Responsibility<br/>SOC 2 Reports"] I10["CloudTrail<br/>VPC Flow Logs<br/>EKS Audit"] I11["Security Hub<br/>Penetration Tests<br/>Vulnerability Scans"] I12["Control Tower<br/>SCPs<br/>Guardrails"] end R1 --> I1 R2 --> I2 R3 --> I3 R4 --> I4 R5 --> I5 R6 --> I6 R7 --> I7 R8 --> I8 R9 --> I9 R10 --> I10 R11 --> I11 R12 --> I12 style Requirements fill:#e63946,stroke:#fff,stroke-width:2px,color:#fff style Implementation fill:#2a9d8f,stroke:#fff,stroke-width:2px,color:#fff

Network Segmentation

CDE VPC Configuration

# cde-vpc/main.tf

module "cde_vpc" {
  source = "../../modules/vpc"

  name = "cde-vpc"
  cidr = "10.200.0.0/16"

  azs             = ["us-east-1a", "us-east-1b", "us-east-1c"]
  private_subnets = ["10.200.1.0/24", "10.200.2.0/24", "10.200.3.0/24"]
  data_subnets    = ["10.200.10.0/24", "10.200.11.0/24", "10.200.12.0/24"]

  # No public subnets in CDE
  public_subnets = []

  # No NAT Gateway - all egress through Transit Gateway
  enable_nat_gateway = false

  # VPC Flow Logs - required for PCI
  enable_flow_log                      = true
  flow_log_destination_type            = "s3"
  flow_log_destination_arn             = var.flow_logs_bucket_arn
  flow_log_traffic_type                = "ALL"
  flow_log_max_aggregation_interval    = 60

  tags = {
    Environment    = "production"
    Compliance     = "pci-dss"
    DataClass      = "cardholder-data"
    CostCenter     = "security"
  }
}

# Network ACLs - defense in depth
resource "aws_network_acl" "cde_private" {
  vpc_id     = module.cde_vpc.vpc_id
  subnet_ids = module.cde_vpc.private_subnet_ids

  # Allow inbound from DMZ only
  ingress {
    protocol   = "tcp"
    rule_no    = 100
    action     = "allow"
    cidr_block = "10.100.0.0/16"  # DMZ VPC
    from_port  = 443
    to_port    = 443
  }

  # Allow inbound from Security VPC (monitoring)
  ingress {
    protocol   = "tcp"
    rule_no    = 110
    action     = "allow"
    cidr_block = "10.240.0.0/16"
    from_port  = 443
    to_port    = 443
  }

  # Deny all other inbound
  ingress {
    protocol   = "-1"
    rule_no    = 200
    action     = "deny"
    cidr_block = "0.0.0.0/0"
    from_port  = 0
    to_port    = 0
  }

  # Allow outbound to data subnets
  egress {
    protocol   = "tcp"
    rule_no    = 100
    action     = "allow"
    cidr_block = "10.200.10.0/24"
    from_port  = 5432
    to_port    = 5432
  }

  egress {
    protocol   = "tcp"
    rule_no    = 110
    action     = "allow"
    cidr_block = "10.200.10.0/24"
    from_port  = 6379
    to_port    = 6379
  }

  # Allow outbound to Security VPC (logging)
  egress {
    protocol   = "tcp"
    rule_no    = 120
    action     = "allow"
    cidr_block = "10.240.0.0/16"
    from_port  = 443
    to_port    = 443
  }

  tags = {
    Name       = "cde-private-nacl"
    Compliance = "pci-dss"
  }
}

Security Groups

# cde-vpc/security-groups.tf

# EKS nodes in CDE
resource "aws_security_group" "cde_eks_nodes" {
  name_prefix = "cde-eks-nodes-"
  vpc_id      = module.cde_vpc.vpc_id
  description = "Security group for EKS nodes in CDE"

  # Inbound from ALB only
  ingress {
    description     = "HTTPS from ALB"
    from_port       = 443
    to_port         = 443
    protocol        = "tcp"
    security_groups = [var.alb_security_group_id]
  }

  # Node to node communication
  ingress {
    description = "Node to node"
    from_port   = 0
    to_port     = 65535
    protocol    = "tcp"
    self        = true
  }

  # Outbound to RDS
  egress {
    description     = "PostgreSQL to RDS"
    from_port       = 5432
    to_port         = 5432
    protocol        = "tcp"
    security_groups = [aws_security_group.cde_rds.id]
  }

  # Outbound to ElastiCache
  egress {
    description     = "Redis to ElastiCache"
    from_port       = 6379
    to_port         = 6379
    protocol        = "tcp"
    security_groups = [aws_security_group.cde_redis.id]
  }

  # Outbound to CloudHSM
  egress {
    description     = "CloudHSM"
    from_port       = 2223
    to_port         = 2225
    protocol        = "tcp"
    security_groups = [aws_security_group.cloudhsm.id]
  }

  # Outbound to VPC endpoints only
  egress {
    description     = "VPC Endpoints"
    from_port       = 443
    to_port         = 443
    protocol        = "tcp"
    prefix_list_ids = [var.vpc_endpoint_prefix_list_id]
  }

  # NO general internet egress

  tags = {
    Name       = "cde-eks-nodes-sg"
    Compliance = "pci-dss"
  }

  lifecycle {
    create_before_destroy = true
  }
}

# RDS in CDE
resource "aws_security_group" "cde_rds" {
  name_prefix = "cde-rds-"
  vpc_id      = module.cde_vpc.vpc_id
  description = "Security group for RDS in CDE"

  # Inbound from EKS nodes only
  ingress {
    description     = "PostgreSQL from EKS"
    from_port       = 5432
    to_port         = 5432
    protocol        = "tcp"
    security_groups = [aws_security_group.cde_eks_nodes.id]
  }

  # No egress needed for RDS

  tags = {
    Name       = "cde-rds-sg"
    Compliance = "pci-dss"
  }
}

Encryption Configuration

KMS Keys

# encryption/kms.tf

# Master key for CDE data
resource "aws_kms_key" "cde_master" {
  description              = "Master key for CDE data encryption"
  deletion_window_in_days  = 30
  enable_key_rotation      = true
  multi_region             = false

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Sid    = "AllowRootAccess"
        Effect = "Allow"
        Principal = {
          AWS = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root"
        }
        Action   = "kms:*"
        Resource = "*"
      },
      {
        Sid    = "AllowCDEServices"
        Effect = "Allow"
        Principal = {
          AWS = [
            aws_iam_role.cde_eks_node.arn,
            aws_iam_role.cde_rds.arn,
          ]
        }
        Action = [
          "kms:Encrypt",
          "kms:Decrypt",
          "kms:ReEncrypt*",
          "kms:GenerateDataKey*",
          "kms:DescribeKey"
        ]
        Resource = "*"
      },
      {
        Sid    = "AllowCloudWatchLogs"
        Effect = "Allow"
        Principal = {
          Service = "logs.${var.region}.amazonaws.com"
        }
        Action = [
          "kms:Encrypt",
          "kms:Decrypt",
          "kms:ReEncrypt*",
          "kms:GenerateDataKey*",
          "kms:DescribeKey"
        ]
        Resource = "*"
        Condition = {
          ArnLike = {
            "kms:EncryptionContext:aws:logs:arn" = "arn:aws:logs:${var.region}:${data.aws_caller_identity.current.account_id}:log-group:*"
          }
        }
      }
    ]
  })

  tags = {
    Name       = "cde-master-key"
    Compliance = "pci-dss"
    Purpose    = "cardholder-data-encryption"
  }
}

resource "aws_kms_alias" "cde_master" {
  name          = "alias/cde-master"
  target_key_id = aws_kms_key.cde_master.key_id
}

CloudHSM for Key Management

# encryption/cloudhsm.tf

resource "aws_cloudhsm_v2_cluster" "cde" {
  hsm_type   = "hsm1.medium"
  subnet_ids = module.cde_vpc.data_subnet_ids

  tags = {
    Name       = "cde-hsm-cluster"
    Compliance = "pci-dss"
  }
}

resource "aws_cloudhsm_v2_hsm" "cde" {
  count      = 2  # HA configuration
  cluster_id = aws_cloudhsm_v2_cluster.cde.cluster_id
  subnet_id  = module.cde_vpc.data_subnet_ids[count.index]
}

resource "aws_security_group" "cloudhsm" {
  name_prefix = "cloudhsm-"
  vpc_id      = module.cde_vpc.vpc_id
  description = "Security group for CloudHSM"

  ingress {
    description     = "HSM client connections"
    from_port       = 2223
    to_port         = 2225
    protocol        = "tcp"
    security_groups = [aws_security_group.cde_eks_nodes.id]
  }

  tags = {
    Name       = "cloudhsm-sg"
    Compliance = "pci-dss"
  }
}

RDS Encryption

# database/rds.tf

resource "aws_db_instance" "cde_primary" {
  identifier = "cde-primary"

  engine         = "postgres"
  engine_version = "15.4"
  instance_class = "db.r6i.xlarge"

  allocated_storage     = 100
  max_allocated_storage = 500
  storage_type          = "gp3"
  storage_encrypted     = true
  kms_key_id            = aws_kms_key.cde_master.arn

  db_name  = "payments"
  username = "admin"
  password = random_password.rds_master.result

  multi_az               = true
  db_subnet_group_name   = aws_db_subnet_group.cde.name
  vpc_security_group_ids = [aws_security_group.cde_rds.id]

  # No public access
  publicly_accessible = false

  # Backup configuration
  backup_retention_period = 35  # PCI requires minimum 1 year, but use S3 for long-term
  backup_window           = "03:00-04:00"
  maintenance_window      = "Mon:04:00-Mon:05:00"

  # Enhanced monitoring
  monitoring_interval = 60
  monitoring_role_arn = aws_iam_role.rds_monitoring.arn

  # Performance insights (encrypted)
  performance_insights_enabled          = true
  performance_insights_kms_key_id       = aws_kms_key.cde_master.arn
  performance_insights_retention_period = 731  # 2 years

  # Audit logging
  enabled_cloudwatch_logs_exports = [
    "postgresql",
    "upgrade"
  ]

  # Deletion protection
  deletion_protection       = true
  skip_final_snapshot       = false
  final_snapshot_identifier = "cde-primary-final-${formatdate("YYYY-MM-DD", timestamp())}"

  # Force SSL
  parameter_group_name = aws_db_parameter_group.cde.name

  tags = {
    Name           = "cde-primary"
    Compliance     = "pci-dss"
    DataClass      = "cardholder-data"
    BackupRequired = "true"
  }
}

resource "aws_db_parameter_group" "cde" {
  name   = "cde-postgres15"
  family = "postgres15"

  # Force SSL connections
  parameter {
    name  = "rds.force_ssl"
    value = "1"
  }

  # Audit logging
  parameter {
    name  = "log_connections"
    value = "1"
  }

  parameter {
    name  = "log_disconnections"
    value = "1"
  }

  parameter {
    name  = "log_statement"
    value = "all"
  }

  parameter {
    name  = "log_min_duration_statement"
    value = "0"  # Log all statements
  }

  tags = {
    Compliance = "pci-dss"
  }
}

Audit Logging

flowchart TB subgraph Sources["Log Sources"] CT[CloudTrail<br/>API Calls] VPC[VPC Flow Logs<br/>Network Traffic] EKS[EKS Audit Logs<br/>K8s API] APP[Application Logs<br/>Business Events] RDS[RDS Logs<br/>Database Access] WAF[WAF Logs<br/>Web Attacks] end subgraph Collection["Log Collection"] CW[CloudWatch Logs] KINESIS[Kinesis Firehose] end subgraph Storage["Immutable Storage"] S3[S3 Bucket<br/>Object Lock Enabled] GLACIER[S3 Glacier<br/>7 Year Retention] end subgraph Analysis["Analysis"] ATHENA[Athena<br/>Ad-hoc Queries] SIEM[SIEM<br/>Real-time Alerts] SECURITYHUB[Security Hub<br/>Compliance Dashboard] end CT & VPC & EKS & APP & RDS & WAF --> CW CW --> KINESIS KINESIS --> S3 S3 --> GLACIER S3 --> ATHENA CW --> SIEM CT --> SECURITYHUB style Sources fill:#264653,stroke:#fff,stroke-width:2px,color:#fff style Collection fill:#f77f00,stroke:#fff,stroke-width:2px,color:#fff style Storage fill:#e63946,stroke:#fff,stroke-width:2px,color:#fff style Analysis fill:#2a9d8f,stroke:#fff,stroke-width:2px,color:#fff

Immutable Log Storage

# logging/s3-logs.tf

resource "aws_s3_bucket" "audit_logs" {
  bucket = "company-pci-audit-logs-${data.aws_caller_identity.current.account_id}"

  tags = {
    Name       = "pci-audit-logs"
    Compliance = "pci-dss"
    Retention  = "7-years"
  }
}

# Enable versioning
resource "aws_s3_bucket_versioning" "audit_logs" {
  bucket = aws_s3_bucket.audit_logs.id
  versioning_configuration {
    status = "Enabled"
  }
}

# Object lock - makes logs immutable
resource "aws_s3_bucket_object_lock_configuration" "audit_logs" {
  bucket = aws_s3_bucket.audit_logs.id

  rule {
    default_retention {
      mode  = "GOVERNANCE"
      years = 7
    }
  }
}

# Encryption
resource "aws_s3_bucket_server_side_encryption_configuration" "audit_logs" {
  bucket = aws_s3_bucket.audit_logs.id

  rule {
    apply_server_side_encryption_by_default {
      kms_master_key_id = aws_kms_key.logs.arn
      sse_algorithm     = "aws:kms"
    }
    bucket_key_enabled = true
  }
}

# Block public access
resource "aws_s3_bucket_public_access_block" "audit_logs" {
  bucket = aws_s3_bucket.audit_logs.id

  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

# Lifecycle - transition to Glacier after 90 days
resource "aws_s3_bucket_lifecycle_configuration" "audit_logs" {
  bucket = aws_s3_bucket.audit_logs.id

  rule {
    id     = "archive-logs"
    status = "Enabled"

    transition {
      days          = 90
      storage_class = "GLACIER"
    }

    # Never expire - regulatory requirement
    # expiration block intentionally omitted
  }
}

# Bucket policy - deny deletion
resource "aws_s3_bucket_policy" "audit_logs" {
  bucket = aws_s3_bucket.audit_logs.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Sid    = "DenyDeletion"
        Effect = "Deny"
        Principal = "*"
        Action = [
          "s3:DeleteObject",
          "s3:DeleteObjectVersion"
        ]
        Resource = "${aws_s3_bucket.audit_logs.arn}/*"
      },
      {
        Sid    = "RequireSSL"
        Effect = "Deny"
        Principal = "*"
        Action = "s3:*"
        Resource = [
          aws_s3_bucket.audit_logs.arn,
          "${aws_s3_bucket.audit_logs.arn}/*"
        ]
        Condition = {
          Bool = {
            "aws:SecureTransport" = "false"
          }
        }
      }
    ]
  })
}

CloudTrail Configuration

# logging/cloudtrail.tf

resource "aws_cloudtrail" "pci" {
  name                          = "pci-audit-trail"
  s3_bucket_name                = aws_s3_bucket.audit_logs.id
  s3_key_prefix                 = "cloudtrail"
  include_global_service_events = true
  is_multi_region_trail         = true
  is_organization_trail         = false
  enable_log_file_validation    = true
  kms_key_id                    = aws_kms_key.logs.arn

  # CloudWatch Logs integration for real-time alerting
  cloud_watch_logs_group_arn = "${aws_cloudwatch_log_group.cloudtrail.arn}:*"
  cloud_watch_logs_role_arn  = aws_iam_role.cloudtrail_cloudwatch.arn

  # Log all events
  event_selector {
    read_write_type           = "All"
    include_management_events = true

    data_resource {
      type   = "AWS::S3::Object"
      values = ["arn:aws:s3"]
    }

    data_resource {
      type   = "AWS::Lambda::Function"
      values = ["arn:aws:lambda"]
    }
  }

  # Insights for anomaly detection
  insight_selector {
    insight_type = "ApiCallRateInsight"
  }

  insight_selector {
    insight_type = "ApiErrorRateInsight"
  }

  tags = {
    Name       = "pci-audit-trail"
    Compliance = "pci-dss"
  }
}

Security Hub and Compliance

# compliance/security-hub.tf

resource "aws_securityhub_account" "main" {}

# Enable PCI-DSS standard
resource "aws_securityhub_standards_subscription" "pci_dss" {
  standards_arn = "arn:aws:securityhub:${var.region}::standards/pci-dss/v/3.2.1"

  depends_on = [aws_securityhub_account.main]
}

# Enable CIS AWS Foundations
resource "aws_securityhub_standards_subscription" "cis" {
  standards_arn = "arn:aws:securityhub:${var.region}::standards/cis-aws-foundations-benchmark/v/1.4.0"

  depends_on = [aws_securityhub_account.main]
}

# Enable AWS Foundational Security Best Practices
resource "aws_securityhub_standards_subscription" "aws_foundational" {
  standards_arn = "arn:aws:securityhub:${var.region}::standards/aws-foundational-security-best-practices/v/1.0.0"

  depends_on = [aws_securityhub_account.main]
}

# Custom action for remediation
resource "aws_securityhub_action_target" "remediate" {
  name        = "Remediate"
  identifier  = "Remediate"
  description = "Trigger automated remediation"

  depends_on = [aws_securityhub_account.main]
}

AWS Config Rules for PCI

# compliance/config-rules.tf

# Encryption at rest
resource "aws_config_config_rule" "s3_bucket_encryption" {
  name = "s3-bucket-server-side-encryption-enabled"

  source {
    owner             = "AWS"
    source_identifier = "S3_BUCKET_SERVER_SIDE_ENCRYPTION_ENABLED"
  }

  tags = {
    Compliance = "pci-dss"
    Requirement = "3.4"
  }
}

resource "aws_config_config_rule" "rds_encryption" {
  name = "rds-storage-encrypted"

  source {
    owner             = "AWS"
    source_identifier = "RDS_STORAGE_ENCRYPTED"
  }

  tags = {
    Compliance = "pci-dss"
    Requirement = "3.4"
  }
}

# Encryption in transit
resource "aws_config_config_rule" "alb_https" {
  name = "alb-http-to-https-redirection-check"

  source {
    owner             = "AWS"
    source_identifier = "ALB_HTTP_TO_HTTPS_REDIRECTION_CHECK"
  }

  tags = {
    Compliance = "pci-dss"
    Requirement = "4.1"
  }
}

# Access control
resource "aws_config_config_rule" "iam_root_mfa" {
  name = "root-account-mfa-enabled"

  source {
    owner             = "AWS"
    source_identifier = "ROOT_ACCOUNT_MFA_ENABLED"
  }

  tags = {
    Compliance = "pci-dss"
    Requirement = "8.3"
  }
}

resource "aws_config_config_rule" "iam_user_mfa" {
  name = "iam-user-mfa-enabled"

  source {
    owner             = "AWS"
    source_identifier = "IAM_USER_MFA_ENABLED"
  }

  tags = {
    Compliance = "pci-dss"
    Requirement = "8.3"
  }
}

# Network security
resource "aws_config_config_rule" "restricted_ssh" {
  name = "restricted-ssh"

  source {
    owner             = "AWS"
    source_identifier = "INCOMING_SSH_DISABLED"
  }

  tags = {
    Compliance = "pci-dss"
    Requirement = "1.2"
  }
}

resource "aws_config_config_rule" "vpc_flow_logs" {
  name = "vpc-flow-logs-enabled"

  source {
    owner             = "AWS"
    source_identifier = "VPC_FLOW_LOGS_ENABLED"
  }

  tags = {
    Compliance = "pci-dss"
    Requirement = "10.1"
  }
}

Access Control

flowchart TD subgraph AccessFlow["Access Control Flow"] USER[User] --> SSO[AWS SSO] SSO --> MFA{MFA Required} MFA -->|Pass| ROLE[Assume Role] MFA -->|Fail| DENY[Access Denied] ROLE --> POLICY{Policy Check} POLICY -->|Allow| ACCESS[Access Granted] POLICY -->|Deny| DENY ACCESS --> AUDIT[Audit Log] end subgraph Policies["Policy Layers"] SCP[Service Control Policy] IAM[IAM Policy] RBAC[K8s RBAC] SCP --> IAM --> RBAC end style AccessFlow fill:#264653,stroke:#fff,stroke-width:2px,color:#fff style Policies fill:#e63946,stroke:#fff,stroke-width:2px,color:#fff

IAM Policies for CDE Access

# iam/cde-access.tf

# Policy for CDE administrators
resource "aws_iam_policy" "cde_admin" {
  name        = "CDEAdministrator"
  description = "Full access to CDE resources with MFA required"

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Sid    = "RequireMFA"
        Effect = "Deny"
        Action = "*"
        Resource = "*"
        Condition = {
          BoolIfExists = {
            "aws:MultiFactorAuthPresent" = "false"
          }
        }
      },
      {
        Sid    = "CDEResourceAccess"
        Effect = "Allow"
        Action = [
          "ec2:*",
          "rds:*",
          "elasticache:*",
          "eks:*",
          "kms:*"
        ]
        Resource = "*"
        Condition = {
          StringEquals = {
            "aws:ResourceTag/Compliance" = "pci-dss"
          }
        }
      },
      {
        Sid    = "DenyUnencryptedResources"
        Effect = "Deny"
        Action = [
          "rds:CreateDBInstance",
          "rds:CreateDBCluster"
        ]
        Resource = "*"
        Condition = {
          Bool = {
            "rds:StorageEncrypted" = "false"
          }
        }
      }
    ]
  })
}

# Read-only policy for auditors
resource "aws_iam_policy" "cde_auditor" {
  name        = "CDEAuditor"
  description = "Read-only access for PCI auditors"

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Sid    = "ReadOnlyAccess"
        Effect = "Allow"
        Action = [
          "ec2:Describe*",
          "rds:Describe*",
          "elasticache:Describe*",
          "eks:Describe*",
          "eks:List*",
          "kms:Describe*",
          "kms:List*",
          "cloudtrail:LookupEvents",
          "cloudtrail:GetTrailStatus",
          "config:Get*",
          "config:Describe*",
          "config:List*",
          "securityhub:Get*",
          "securityhub:List*",
          "logs:Get*",
          "logs:Describe*",
          "logs:FilterLogEvents"
        ]
        Resource = "*"
      },
      {
        Sid    = "DenyDataAccess"
        Effect = "Deny"
        Action = [
          "s3:GetObject",
          "rds:Download*",
          "logs:GetLogEvents"
        ]
        Resource = [
          "arn:aws:s3:::*cardholder*/*",
          "arn:aws:rds:*:*:*cde*"
        ]
      }
    ]
  })
}

Best Practices

Practice PCI Requirement Implementation
Network segmentation 1.3 Separate VPCs, NACLs, Security Groups
Encryption at rest 3.4 KMS, CloudHSM, RDS encryption
Encryption in transit 4.1 TLS 1.3, ACM, no HTTP
Access logging 10.1 CloudTrail, VPC Flow Logs
Log retention 10.7 7 year retention, immutable storage
MFA everywhere 8.3 IAM MFA, SSO MFA
Vulnerability scanning 11.2 Inspector, ECR scanning
Change detection 11.5 Config Rules, Security Hub

Troubleshooting

"Security Hub finding: S3 bucket not encrypted"

# Check bucket encryption
aws s3api get-bucket-encryption --bucket my-bucket

# Enable encryption
aws s3api put-bucket-encryption --bucket my-bucket \
  --server-side-encryption-configuration '{
    "Rules": [{
      "ApplyServerSideEncryptionByDefault": {
        "SSEAlgorithm": "aws:kms",
        "KMSMasterKeyID": "alias/cde-master"
      }
    }]
  }'

"Config rule non-compliant: VPC flow logs"

# Check flow logs
aws ec2 describe-flow-logs --filter "Name=resource-id,Values=vpc-xxxxx"

# Enable flow logs
aws ec2 create-flow-logs \
  --resource-type VPC \
  --resource-ids vpc-xxxxx \
  --traffic-type ALL \
  --log-destination-type s3 \
  --log-destination arn:aws:s3:::audit-logs/vpc-flow-logs

Conclusion

PCI-DSS compliance on AWS is achievable with the right architecture. The key principles:

  • Defense in depth - Multiple layers of security controls
  • Encryption everywhere - At rest, in transit, in use
  • Log everything - Immutable, long-term retention
  • Automate compliance - Continuous monitoring, not point-in-time audits
  • Least privilege - MFA required, short-lived credentials

The combination of AWS native services (KMS, CloudHSM, Security Hub) with proper network segmentation creates a platform that satisfies even the most demanding QSAs while remaining operationally manageable.

Share this article

Tags

#pci-dss#compliance#encryption#audit#fintech

Related Articles

cloud12 min read

Engineering AWS NLB Infrastructure for Financial Services Proxy Networks

Designing a multi-environment AWS NLB infrastructure for financial services using Terraform - featuring dual internal/external load balancers, JSON-driven per-port IP whitelists, intelligent port-range routing, and Transit Gateway hybrid connectivity.

security11 min read

Mastering Secrets Management at Scale: Vault, AWS Secrets Manager, and Parameter Store

Unifying secrets management strategy combining HashiCorp Vault, AWS Secrets Manager, and Parameter Store - with cross-account sharing, automatic rotation, and Kubernetes integration via External Secrets Operator.

system-design13 min read

Payment Processing System at Scale: Stripe/Adyen Integration with AWS EventBridge, Lambda, and DynamoDB

Building a payment processing system handling millions of daily transactions - featuring EventBridge for event-driven orchestration, Lambda for serverless processing, DynamoDB for transaction state, idempotency guarantees, and real-time fraud detection with Kinesis.