Section 12

Reference Architectures

Production-ready reference architectures with security controls annotated at every layer. Use these as starting points for your own designs — adapt them to your specific threat model.

How to Use These Architectures

Each architecture includes a visual diagram, annotated security controls, common misconfigurations to avoid, and infrastructure-as-code snippets for critical components. Start with the one closest to your deployment model, then customize.

1. Secure Three-Tier Web Application

The classic architecture for web applications — presentation, application logic, and data layers with security controls at each tier and trust boundary.

Secure Three-Tier Architecture

graph LR User["👤 Client"] subgraph DMZ["DMZ / Public"] CDN["CDN / WAF"] --> ALB["Load Balancer"] end subgraph AppTier["Application Tier"] App1["App Svc 1"] & App2["App Svc 2"] end subgraph DataTier["Data Tier"] DB[("RDS / Aurora")] & Cache[("Redis Cache")] end User -->|"HTTPS"| CDN ALB --> App1 & App2 App1 & App2 --> DB & Cache

Key Security Controls

Network Controls

  • • Security Groups: least-privilege port access
  • • NACLs: subnet-level deny rules
  • • No direct internet access to app/data tier
  • • VPC Flow Logs enabled

Application Controls

  • • JWT validation at API gateway
  • • RBAC/ABAC at service level
  • • Input validation + output encoding
  • • Rate limiting per authenticated user

Terraform — VPC with Security Groups

secure-vpc.tf
hcl
# Secure VPC with isolated subnets per tier
resource "aws_vpc" "main" {
  cidr_block           = "10.0.0.0/16"
  enable_dns_hostnames = true
  enable_dns_support   = true
  tags = { Name = "secure-app-vpc" }
}

# Public subnet (ALB only - no application code here)
resource "aws_subnet" "public" {
  vpc_id                  = aws_vpc.main.id
  cidr_block              = "10.0.1.0/24"
  map_public_ip_on_launch = false  # No automatic public IPs
}

# Private subnet for application tier
resource "aws_subnet" "app" {
  vpc_id     = aws_vpc.main.id
  cidr_block = "10.0.10.0/24"
}

# Isolated subnet for database (no internet route)
resource "aws_subnet" "data" {
  vpc_id     = aws_vpc.main.id
  cidr_block = "10.0.20.0/24"
}

# Security Group: ALB - only HTTPS from internet
resource "aws_security_group" "alb" {
  vpc_id = aws_vpc.main.id
  ingress {
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
  egress {
    from_port       = 8080
    to_port         = 8080
    protocol        = "tcp"
    security_groups = [aws_security_group.app.id]
  }
}

# Security Group: App - only from ALB
resource "aws_security_group" "app" {
  vpc_id = aws_vpc.main.id
  ingress {
    from_port       = 8080
    to_port         = 8080
    protocol        = "tcp"
    security_groups = [aws_security_group.alb.id]
  }
}

# Security Group: DB - only from App on port 5432
resource "aws_security_group" "db" {
  vpc_id = aws_vpc.main.id
  ingress {
    from_port       = 5432
    to_port         = 5432
    protocol        = "tcp"
    security_groups = [aws_security_group.app.id]
  }
  # No egress to internet
  egress { from_port = 0; to_port = 0; protocol = "-1"; cidr_blocks = [] }
}
# Secure VPC with isolated subnets per tier
resource "aws_vpc" "main" {
  cidr_block           = "10.0.0.0/16"
  enable_dns_hostnames = true
  enable_dns_support   = true
  tags = { Name = "secure-app-vpc" }
}

# Public subnet (ALB only - no application code here)
resource "aws_subnet" "public" {
  vpc_id                  = aws_vpc.main.id
  cidr_block              = "10.0.1.0/24"
  map_public_ip_on_launch = false  # No automatic public IPs
}

# Private subnet for application tier
resource "aws_subnet" "app" {
  vpc_id     = aws_vpc.main.id
  cidr_block = "10.0.10.0/24"
}

# Isolated subnet for database (no internet route)
resource "aws_subnet" "data" {
  vpc_id     = aws_vpc.main.id
  cidr_block = "10.0.20.0/24"
}

# Security Group: ALB - only HTTPS from internet
resource "aws_security_group" "alb" {
  vpc_id = aws_vpc.main.id
  ingress {
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
  egress {
    from_port       = 8080
    to_port         = 8080
    protocol        = "tcp"
    security_groups = [aws_security_group.app.id]
  }
}

# Security Group: App - only from ALB
resource "aws_security_group" "app" {
  vpc_id = aws_vpc.main.id
  ingress {
    from_port       = 8080
    to_port         = 8080
    protocol        = "tcp"
    security_groups = [aws_security_group.alb.id]
  }
}

# Security Group: DB - only from App on port 5432
resource "aws_security_group" "db" {
  vpc_id = aws_vpc.main.id
  ingress {
    from_port       = 5432
    to_port         = 5432
    protocol        = "tcp"
    security_groups = [aws_security_group.app.id]
  }
  # No egress to internet
  egress { from_port = 0; to_port = 0; protocol = "-1"; cidr_blocks = [] }
}

Common Misconfigurations

Database subnet has route to internet gateway

Data tier should have NO internet access. Use VPC endpoints for AWS services.

Security groups use 0.0.0.0/0 for app tier ingress

App tier should only accept traffic from the load balancer security group.

RDS instance publicly accessible

Set publicly_accessible = false. Access DB only through app tier or bastion/SSM.

2. Secure Cloud Landing Zone (Hub-Spoke)

A multi-account / multi-subscription architecture for enterprise cloud deployments. Central shared services hub with isolated spoke environments for each workload.

Hub-Spoke Cloud Landing Zone

graph LR subgraph Mgmt["🔐 Management"] IAM["IAM Center"] & Audit["Audit Logs"] end subgraph Hub["Shared Hub"] Transit["Transit GW"] --- FW["Firewall"] & DNS["DNS / Logs"] end subgraph Prod["Prod Spoke"] ProdVPC["Workload VPC"] end subgraph Dev["Dev Spoke"] DevVPC["Workload VPC"] end Mgmt -->|"SCPs"| Hub & Prod & Dev Transit <-->|"Peering"| ProdVPC & DevVPC

Landing Zone Security Controls

Account-Level Controls

  • • SCPs preventing disabling CloudTrail/GuardDuty
  • • Mandatory encryption on S3/EBS/RDS
  • • Deny public S3 buckets at account level
  • • Require MFA for all IAM users

Network Controls

  • • All egress through centralized inspection
  • • DNS filtering via Route 53 Resolver
  • • VPC Flow Logs to central SIEM
  • • No direct internet access in spokes

Service Control Policy — Deny Dangerous Actions

deny-dangerous-actions-scp.json
json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyDisablingCloudTrail",
      "Effect": "Deny",
      "Action": [
        "cloudtrail:StopLogging",
        "cloudtrail:DeleteTrail"
      ],
      "Resource": "*"
    },
    {
      "Sid": "DenyPublicS3",
      "Effect": "Deny",
      "Action": "s3:PutBucketPublicAccessBlock",
      "Resource": "*",
      "Condition": {
        "StringNotEquals": {
          "s3:PublicAccessBlockConfiguration/BlockPublicAcls": "true"
        }
      }
    },
    {
      "Sid": "DenyUnencryptedUploads",
      "Effect": "Deny",
      "Action": "s3:PutObject",
      "Resource": "*",
      "Condition": {
        "StringNotEquals": {
          "s3:x-amz-server-side-encryption": "aws:kms"
        }
      }
    },
    {
      "Sid": "DenyLeavingOrganization",
      "Effect": "Deny",
      "Action": "organizations:LeaveOrganization",
      "Resource": "*"
    }
  ]
}
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyDisablingCloudTrail",
      "Effect": "Deny",
      "Action": [
        "cloudtrail:StopLogging",
        "cloudtrail:DeleteTrail"
      ],
      "Resource": "*"
    },
    {
      "Sid": "DenyPublicS3",
      "Effect": "Deny",
      "Action": "s3:PutBucketPublicAccessBlock",
      "Resource": "*",
      "Condition": {
        "StringNotEquals": {
          "s3:PublicAccessBlockConfiguration/BlockPublicAcls": "true"
        }
      }
    },
    {
      "Sid": "DenyUnencryptedUploads",
      "Effect": "Deny",
      "Action": "s3:PutObject",
      "Resource": "*",
      "Condition": {
        "StringNotEquals": {
          "s3:x-amz-server-side-encryption": "aws:kms"
        }
      }
    },
    {
      "Sid": "DenyLeavingOrganization",
      "Effect": "Deny",
      "Action": "organizations:LeaveOrganization",
      "Resource": "*"
    }
  ]
}

3. Zero Trust Reference Architecture

Based on NIST SP 800-207. Every access request is authenticated, authorized, and encrypted — regardless of network location.

NIST 800-207 Zero Trust Architecture

graph LR Subject["👤 Subject"] --> PDP subgraph PDP["Policy Engine — PDP"] IdP["Identity Provider"] --- PAP["Policy Decision"] Threat["Threat Intel"] --- DataPolicy["Data Policy"] end PDP -->|"Allow / Deny"| PEP Subject --> PEP subgraph PEP["Enforcement Point — PEP"] CF["Cloudflare / Zscaler"] Proxy["API Gateway (mTLS)"] end PEP -->|"Encrypted"| Resources subgraph Resources["Enterprise Resources"] SaaS["SaaS"] & Legacy["On-prem"] & APIs["Cloud APIs"] & Lakes["Data Lakes"] end

Cloudflare Access — Zero Trust App Protection

cloudflare-access-policy.tf
hcl
# Cloudflare Access application (ZTNA - replaces VPN)
resource "cloudflare_access_application" "internal_app" {
  zone_id          = var.zone_id
  name             = "Internal Dashboard"
  domain           = "dashboard.internal.example.com"
  session_duration = "1h"

  # Require device posture check
  auto_redirect_to_identity = true
}

# Access policy: require corporate identity + device posture
resource "cloudflare_access_policy" "corp_policy" {
  application_id = cloudflare_access_application.internal_app.id
  zone_id        = var.zone_id
  name           = "Corporate Access"
  precedence     = 1
  decision       = "allow"

  include {
    login_method = ["oidc"]  # SSO via Okta/Entra
  }

  require {
    device_posture = [
      cloudflare_device_posture_rule.disk_encryption.id,
      cloudflare_device_posture_rule.os_version.id,
      cloudflare_device_posture_rule.firewall_enabled.id
    ]
  }
}

# Device posture: require disk encryption
resource "cloudflare_device_posture_rule" "disk_encryption" {
  account_id = var.account_id
  type       = "disk_encryption"
  name       = "Require disk encryption"
  input { require_all = true }
}
# Cloudflare Access application (ZTNA - replaces VPN)
resource "cloudflare_access_application" "internal_app" {
  zone_id          = var.zone_id
  name             = "Internal Dashboard"
  domain           = "dashboard.internal.example.com"
  session_duration = "1h"

  # Require device posture check
  auto_redirect_to_identity = true
}

# Access policy: require corporate identity + device posture
resource "cloudflare_access_policy" "corp_policy" {
  application_id = cloudflare_access_application.internal_app.id
  zone_id        = var.zone_id
  name           = "Corporate Access"
  precedence     = 1
  decision       = "allow"

  include {
    login_method = ["oidc"]  # SSO via Okta/Entra
  }

  require {
    device_posture = [
      cloudflare_device_posture_rule.disk_encryption.id,
      cloudflare_device_posture_rule.os_version.id,
      cloudflare_device_posture_rule.firewall_enabled.id
    ]
  }
}

# Device posture: require disk encryption
resource "cloudflare_device_posture_rule" "disk_encryption" {
  account_id = var.account_id
  type       = "disk_encryption"
  name       = "Require disk encryption"
  input { require_all = true }
}

4. Microservices with Service Mesh

Kubernetes-native architecture with Istio service mesh providing automatic mTLS, authorization policies, and observability for all service-to-service communication.

Microservices with Service Mesh (Istio)

graph LR Ingress["Ingress Gateway"] subgraph Mesh["☸ Service Mesh (mTLS everywhere)"] AppSvc["App Svc + Envoy"] OrderSvc["Order Svc + Envoy"] AuthSvc["Auth Svc + Envoy"] PaymentSvc["Payment Svc + Envoy"] end CP["istiod Control Plane"] Ingress -->|"mTLS"| AppSvc AppSvc <-->|"mTLS"| OrderSvc OrderSvc <-->|"mTLS"| PaymentSvc AppSvc <-->|"mTLS"| AuthSvc CP -.->|"Certs + Config"| Mesh

Istio Authorization Policy

istio-auth-policy.yaml
yaml
# Enforce mTLS for all traffic in the mesh
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
  name: default
  namespace: istio-system
spec:
  mtls:
    mode: STRICT   # All traffic must be mTLS

---
# Only allow order-svc to call payment-svc
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
  name: payment-svc-policy
  namespace: production
spec:
  selector:
    matchLabels:
      app: payment-svc
  action: ALLOW
  rules:
  - from:
    - source:
        principals: ["cluster.local/ns/production/sa/order-svc"]
    to:
    - operation:
        methods: ["POST"]
        paths: ["/api/v1/charges", "/api/v1/refunds"]

---
# Deny all other traffic to payment-svc
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
  name: payment-svc-deny-all
  namespace: production
spec:
  selector:
    matchLabels:
      app: payment-svc
  action: DENY
  rules: []  # Empty = deny everything not explicitly allowed
# Enforce mTLS for all traffic in the mesh
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
  name: default
  namespace: istio-system
spec:
  mtls:
    mode: STRICT   # All traffic must be mTLS

---
# Only allow order-svc to call payment-svc
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
  name: payment-svc-policy
  namespace: production
spec:
  selector:
    matchLabels:
      app: payment-svc
  action: ALLOW
  rules:
  - from:
    - source:
        principals: ["cluster.local/ns/production/sa/order-svc"]
    to:
    - operation:
        methods: ["POST"]
        paths: ["/api/v1/charges", "/api/v1/refunds"]

---
# Deny all other traffic to payment-svc
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
  name: payment-svc-deny-all
  namespace: production
spec:
  selector:
    matchLabels:
      app: payment-svc
  action: DENY
  rules: []  # Empty = deny everything not explicitly allowed

Kubernetes Network Policy

network-policy.yaml
yaml
# Default deny all ingress and egress
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
  namespace: production
spec:
  podSelector: {}
  policyTypes:
  - Ingress
  - Egress

---
# Allow order-svc to reach payment-svc on port 8080 only
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-order-to-payment
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: payment-svc
  policyTypes:
  - Ingress
  ingress:
  - from:
    - podSelector:
        matchLabels:
          app: order-svc
    ports:
    - protocol: TCP
      port: 8080
# Default deny all ingress and egress
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
  namespace: production
spec:
  podSelector: {}
  policyTypes:
  - Ingress
  - Egress

---
# Allow order-svc to reach payment-svc on port 8080 only
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-order-to-payment
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: payment-svc
  policyTypes:
  - Ingress
  ingress:
  - from:
    - podSelector:
        matchLabels:
          app: order-svc
    ports:
    - protocol: TCP
      port: 8080

5. Secure CI/CD Pipeline

Security gates at every stage of the pipeline — from commit to production. Every artifact is scanned, signed, and verified before deployment.

Secure CI/CD Pipeline — Security Gates

graph LR Dev["✍️ Write Code"] G1["🔒 GATE 1 Secrets Scan Security Lint"] G2["🔍 GATE 2 SAST · SCA License Check"] G3["🧪 GATE 3 DAST Scan Pen Test Review"] G4["📦 GATE 4 Image Sign · OPA SBOM Generate"] Prod["🚀 Production"] Dev --> G1 --> G2 --> G3 --> G4 --> Prod

GitHub Actions — Secure Pipeline

.github/workflows/secure-pipeline.yml
yaml
name: Secure CI/CD Pipeline
on: [push, pull_request]

permissions:
  contents: read
  security-events: write

jobs:
  gate-1-secrets:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
      with: { fetch-depth: 0 }
    - uses: gitleaks/gitleaks-action@v2
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

  gate-2-sast:
    needs: gate-1-secrets
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
    - name: Semgrep SAST
      uses: semgrep/semgrep-action@v1
      with:
        config: >-
          p/security-audit
          p/owasp-top-ten
          p/secrets
    - name: Dependency scan
      uses: aquasecurity/trivy-action@master
      with:
        scan-type: fs
        severity: CRITICAL,HIGH
        exit-code: 1  # Fail on critical/high

  gate-3-container:
    needs: gate-2-sast
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
    - name: Build image
      run: docker build -t app:${{ github.sha }} .
    - name: Scan container image
      uses: aquasecurity/trivy-action@master
      with:
        image-ref: app:${{ github.sha }}
        severity: CRITICAL,HIGH
        exit-code: 1
    - name: Generate SBOM
      uses: anchore/sbom-action@v0
      with:
        image: app:${{ github.sha }}
        format: spdx-json
    - name: Sign image with Cosign
      uses: sigstore/cosign-installer@v3
    - run: cosign sign --yes app:${{ github.sha }}

  gate-4-deploy:
    needs: gate-3-container
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    environment: production  # Requires approval
    steps:
    - name: Verify image signature
      run: cosign verify app:${{ github.sha }}
    - name: Deploy to production
      run: kubectl set image deployment/app app=app:${{ github.sha }}
name: Secure CI/CD Pipeline
on: [push, pull_request]

permissions:
  contents: read
  security-events: write

jobs:
  gate-1-secrets:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
      with: { fetch-depth: 0 }
    - uses: gitleaks/gitleaks-action@v2
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

  gate-2-sast:
    needs: gate-1-secrets
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
    - name: Semgrep SAST
      uses: semgrep/semgrep-action@v1
      with:
        config: >-
          p/security-audit
          p/owasp-top-ten
          p/secrets
    - name: Dependency scan
      uses: aquasecurity/trivy-action@master
      with:
        scan-type: fs
        severity: CRITICAL,HIGH
        exit-code: 1  # Fail on critical/high

  gate-3-container:
    needs: gate-2-sast
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
    - name: Build image
      run: docker build -t app:${{ github.sha }} .
    - name: Scan container image
      uses: aquasecurity/trivy-action@master
      with:
        image-ref: app:${{ github.sha }}
        severity: CRITICAL,HIGH
        exit-code: 1
    - name: Generate SBOM
      uses: anchore/sbom-action@v0
      with:
        image: app:${{ github.sha }}
        format: spdx-json
    - name: Sign image with Cosign
      uses: sigstore/cosign-installer@v3
    - run: cosign sign --yes app:${{ github.sha }}

  gate-4-deploy:
    needs: gate-3-container
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    environment: production  # Requires approval
    steps:
    - name: Verify image signature
      run: cosign verify app:${{ github.sha }}
    - name: Deploy to production
      run: kubectl set image deployment/app app=app:${{ github.sha }}

6. Hybrid / Multi-Cloud Security

Interconnecting on-premises, AWS, Azure, and GCP environments while maintaining consistent security policies and identity federation.

Hybrid / Multi-Cloud Architecture

graph LR subgraph OnPrem["🏢 On-Premises"] AD["AD / LDAP"] Legacy["Legacy Apps"] SIEM["SIEM (Splunk)"] end subgraph Cloud["☁️ Cloud (AWS)"] Entra["Entra ID / IAM"] CloudApps["Cloud-Native Apps"] Trail["CloudTrail GuardDuty"] end AD <-->|"SAML / OIDC Federation"| Entra OnPrem <-->|"VPN / Direct Connect (IPSec)"| Cloud Trail -->|"Logs"| SIEM Legacy ---|"SAML / OIDC"| CloudApps

Identity Federation

  • • Single IdP as source of truth (Entra ID / Okta)
  • • SAML/OIDC federation to all cloud providers
  • • No local cloud accounts — federated only
  • • Consistent MFA policy across all environments

Network Security

  • • Encrypted site-to-site VPN or Direct Connect
  • • Consistent DNS security across environments
  • • Centralized egress inspection
  • • Uniform firewall rules via IaC

Data Sovereignty

  • • Data residency requirements per region
  • • Encryption keys in jurisdiction-appropriate KMS
  • • GDPR / data localization compliance
  • • Cross-border transfer agreements

Unified Observability

  • • Central SIEM aggregating all logs
  • • Consistent alert rules across providers
  • • CSPM tools covering all clouds
  • • Single pane of glass for incidents

Security Architecture Decision Record (ADR) Template

Document every security architecture decision so future teams understand the context. Use this template for each significant decision.

adr-template.md
markdown
# ADR-{number}: {Title}

## Status
Proposed | Accepted | Deprecated | Superseded by ADR-{n}

## Context
What is the problem or situation that requires a decision?
What are the security requirements and constraints?

## Decision Drivers
- Compliance requirement (e.g., SOC 2, PCI DSS)
- Threat model finding (reference TM-{n})
- Performance / scalability requirement
- Operational complexity budget

## Considered Options
1. **Option A** — Description, pros, cons
2. **Option B** — Description, pros, cons
3. **Option C** — Description, pros, cons

## Decision
We chose **Option B** because...

## Security Implications
- **Risk accepted**: What residual risk remains?
- **Controls required**: What must be implemented alongside?
- **Monitoring**: What should we watch for?

## Consequences
- Positive: What improves?
- Negative: What trade-offs are we making?
- Related decisions: Links to dependent ADRs
# ADR-{number}: {Title}

## Status
Proposed | Accepted | Deprecated | Superseded by ADR-{n}

## Context
What is the problem or situation that requires a decision?
What are the security requirements and constraints?

## Decision Drivers
- Compliance requirement (e.g., SOC 2, PCI DSS)
- Threat model finding (reference TM-{n})
- Performance / scalability requirement
- Operational complexity budget

## Considered Options
1. **Option A** — Description, pros, cons
2. **Option B** — Description, pros, cons
3. **Option C** — Description, pros, cons

## Decision
We chose **Option B** because...

## Security Implications
- **Risk accepted**: What residual risk remains?
- **Controls required**: What must be implemented alongside?
- **Monitoring**: What should we watch for?

## Consequences
- Positive: What improves?
- Negative: What trade-offs are we making?
- Related decisions: Links to dependent ADRs

Use These as Starting Points

No reference architecture is one-size-fits-all. Start with the pattern closest to your deployment model, run a threat model against it, and adapt the controls to your specific risk profile and compliance requirements.