Last reviewed 15 min read

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 pairs a visual diagram with the controls to review, plus a worked example where it teaches the most — an IaC snippet, a policy, or a misconfiguration to avoid. Not every architecture ships code; start with the one closest to your deployment model, then customize.

Consolidated Worked Examples

The patterns themselves are introduced earlier in this section — defense in depth in Section 4, Zero Trust in Section 6, cloud-native and service-mesh hardening in Section 7, and secure SDLC gates in Section 9. This page is the consolidated, buildable reference: the full Terraform, policy, YAML, and workflow manifests live here.

Architecture Selection Matrix

Choose your starting architecture based on team maturity, regulatory requirements, and deployment model. Most organizations evolve through these patterns as they grow.

Factor Three-Tier Hub-Spoke Zero Trust Service Mesh Secure CI/CD Hybrid
Team Size 2–10 10+ Any 10+ Any 20+
Cloud Maturity Beginner Intermediate Intermediate Advanced Any Advanced
Regulatory Need Low Medium Medium–High Low–Med Any High
Complexity Low Medium Medium High Low–Med High
Start Here If… First cloud app Multi-team org Remote-first / BYO device Microservices already built Any new pipeline On-prem + cloud migration

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.

Introduced in: Section 4 — Defense in Depth

Secure Three-Tier Architecture

Client traffic passes through a CDN/WAF edge and an Application Gateway before reaching stateless app replicas. The DMZ, application tier and data tier each sit in their own security group, and the app tier reaches Azure SQL and Redis only over private endpoints.

Key Security Controls

Network Controls

  • • Security Groups: least-privilege port access
  • • NSG rules: subnet-level deny rules
  • • No direct internet access to app/data tier
  • • NSG 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 — VNet with Network Security Groups

secure-vnet.tf
# Three-tier VNet: one subnet per tier, each bound to its own NSG
resource "azurerm_subnet" "public" {        # Application Gateway only
  name                 = "public-subnet"
  address_prefixes     = ["10.0.1.0/24"]
  virtual_network_name = azurerm_virtual_network.main.name
  resource_group_name  = azurerm_resource_group.main.name
}
resource "azurerm_subnet" "app" {
  name                 = "app-subnet"
  address_prefixes     = ["10.0.10.0/24"]
  virtual_network_name = azurerm_virtual_network.main.name
  resource_group_name  = azurerm_resource_group.main.name
}
resource "azurerm_subnet" "data" {          # no outbound internet
  name                 = "data-subnet"
  address_prefixes     = ["10.0.20.0/24"]
  virtual_network_name = azurerm_virtual_network.main.name
  resource_group_name  = azurerm_resource_group.main.name
}

# NSG rule bodies omitted; the decisions are:
#   appgw-nsg : allow 443/tcp  inbound from *             (internet → App Gateway)
#   app-nsg   : allow 8080/tcp inbound from 10.0.1.0/24   (App Gateway subnet only)
#   db-nsg    : allow 1433/tcp inbound from 10.0.10.0/24  (app subnet only)
#   db-nsg    : deny  */*      outbound                   (no internet egress)

# Rules only take effect once each subnet is associated with its NSG
resource "azurerm_subnet_network_security_group_association" "public" {
  subnet_id                 = azurerm_subnet.public.id
  network_security_group_id = azurerm_network_security_group.appgw.id
}
resource "azurerm_subnet_network_security_group_association" "app" {
  subnet_id                 = azurerm_subnet.app.id
  network_security_group_id = azurerm_network_security_group.app.id
}
resource "azurerm_subnet_network_security_group_association" "data" {
  subnet_id                 = azurerm_subnet.data.id
  network_security_group_id = azurerm_network_security_group.db.id
}

Common Misconfigurations

Database subnet has route to internet gateway

Data tier should have NO internet access. Use Private Endpoints for Azure 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.

Database VM directly reachable from the internet

Give the data tier no public IP. Allow 1433 only from the app subnet and reach the VM for administration through Azure Bastion.

Monitoring & Observability

monitoring.tf
# (NSG Flow Logs and the Defender for Cloud plans are elsewhere in monitoring.tf.)

# Alert on real 5xx responses (Application Gateway ResponseStatus, filtered to
# HttpStatusGroup=5xx) — UnhealthyHostCount measures backend health, not errors.
resource "azurerm_monitor_metric_alert" "high_5xx" {
  name                = "high-5xx-rate"
  resource_group_name = azurerm_resource_group.main.name
  scopes              = [azurerm_application_gateway.main.id]
  severity            = 1
  window_size         = "PT15M"
  criteria {
    metric_namespace = "Microsoft.Network/applicationGateways"
    metric_name      = "ResponseStatus"
    aggregation      = "Total"
    operator         = "GreaterThan"
    threshold        = 50
    dimension {
      name     = "HttpStatusGroup"
      operator = "Include"
      values   = ["5xx"]
    }
  }
  action {
    action_group_id = azurerm_monitor_action_group.security_alerts.id
  }
}
# Failed administrative (control-plane) operations. Activity Log does NOT
# contain sign-ins — monitor sign-in risk with Entra ID diagnostic logs.
resource "azurerm_monitor_activity_log_alert" "admin_operation_failure" {
  name                = "admin-operation-failures"
  resource_group_name = azurerm_resource_group.main.name
  scopes              = ["/subscriptions/${data.azurerm_subscription.current.subscription_id}"]
  criteria {
    category = "Administrative"
    level    = "Error"
  }
  action {
    action_group_id = azurerm_monitor_action_group.security_alerts.id
  }
}

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.

Baselines: platform and workload configuration follows the CIS Benchmarks.

Hub-Spoke Cloud Landing Zone

A shared hub (Virtual WAN, Azure Firewall, private DNS resolver) centralises inspection, and Entra ID applies Azure Policy to the hub and to isolated Prod and Dev spokes. Spoke traffic peers through the hub rather than going straight to the internet.

Landing Zone Security Controls

Subscription-Level Controls

  • • Azure Policy preventing disabling Defender/Activity Log
  • • Mandatory encryption on Blob Storage/Managed Disks/Azure SQL
  • • Deny public Blob containers at subscription level
  • • Require MFA for all Entra ID users

Network Controls

  • • All egress through centralized inspection
  • • DNS filtering via Azure DNS Private Resolver
  • • NSG Flow Logs to central SIEM
  • • No direct internet access in spokes

Azure Policy — Deny Dangerous Actions

deny-dangerous-actions-policy.json
{
  "properties": {
    "displayName": "Deny Dangerous Actions",
    "metadata": {
      "comment": "Confirm the Microsoft.Security/securityContacts/alertNotifications.state alias against your target tenant before enforcing (az policy alias list)."
    },
    "policyType": "Custom",
    "mode": "All",
    "policyRule": {
      "if": {
        "anyOf": [
          { "allOf": [
            { "field": "type", "equals": "Microsoft.Security/securityContacts" },
            { "field": "Microsoft.Security/securityContacts/alertNotifications.state", "equals": "Off" }
          ]},
          { "allOf": [
            { "field": "type", "equals": "Microsoft.Storage/storageAccounts" },
            { "field": "Microsoft.Storage/storageAccounts/allowBlobPublicAccess", "equals": true }
          ]},
          { "allOf": [
            { "field": "type", "equals": "Microsoft.Storage/storageAccounts" },
            { "not": { "field": "Microsoft.Storage/storageAccounts/encryption.services.blob.enabled", "equals": true } }
          ]}
        ]
      },
      "then": { "effect": "Deny" }
    }
  }
}

3. Zero Trust Reference Architecture

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

Introduced in: Section 6 — Zero Trust Architecture

NIST 800-207 Zero Trust Architecture

The subject authenticates to the identity provider, and the policy engine (PDP) weighs identity plus threat-intel and data-classification signals to return an Allow/Deny decision. The enforcement point (PEP — Cloudflare/Zscaler plus an mTLS API gateway) brokers every request, so SaaS, on-prem, cloud APIs and data lakes are never reached directly.

Cloudflare Access — Zero Trust App Protection

cloudflare-access-policy.tf
# Cloudflare Zero Trust Access application (ZTNA, replaces VPN)
resource "cloudflare_zero_trust_access_application" "internal_app" {
  account_id                = var.account_id
  name                      = "Internal Dashboard"
  domain                    = "dashboard.internal.example.com"
  type                      = "self_hosted"
  session_duration          = "1h"
  auto_redirect_to_identity = true
  policies = [
    { id = cloudflare_zero_trust_access_policy.corp_policy.id, precedence = 1 }
  ]
}

# Require corporate SSO identity plus device posture
resource "cloudflare_zero_trust_access_policy" "corp_policy" {
  account_id = var.account_id
  name       = "Corporate Access"
  decision   = "allow"
  include = [
    { login_method = ["oidc"] }  # SSO via Okta/Entra
  ]
  require = [
    { device_posture = { id = cloudflare_zero_trust_device_posture_rule.disk_encryption.id } }
  ]
}

# Device posture rules moved under the zero_trust namespace in provider v5.
# Add os_version / firewall rules the same way.
resource "cloudflare_zero_trust_device_posture_rule" "disk_encryption" {
  account_id = var.account_id
  name       = "Require disk encryption"
  type       = "disk_encryption"
  match { platform = "windows" }
  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.

Introduced in: Section 7 — Cloud-Native Security. Hardening references: NIST SP 800-190 and the CIS Kubernetes Benchmark.

Microservices with Service Mesh (Istio)

North-south traffic enters through an Ingress Gateway, then every service-to-service call (App → Order/Auth, Order → Payment) runs over STRICT mTLS inside the mesh. istiod is the control plane that issues each sidecar its certificates and configuration.

Istio Authorization Policy

istio-auth-policy.yaml
# Enforce mTLS for all traffic in the mesh
apiVersion: security.istio.io/v1
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.
# A single ALLOW policy is already default-deny for the selected workload:
# once an ALLOW policy matches a pod, any request that no ALLOW rule matches
# is rejected. No separate DENY policy is needed — a DENY policy only carves
# explicit exceptions out of an existing allowance.
apiVersion: security.istio.io/v1
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"]

Kubernetes Network Policy

network-policy.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

---
# A connection must be allowed at BOTH ends. This is the caller's egress —
# without it order-svc's default-deny egress silently drops the request.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-order-egress-payment
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: order-svc
  policyTypes:
  - Egress
  egress:
  - to:
    - podSelector:
        matchLabels:
          app: payment-svc
    ports:
    - protocol: TCP
      port: 8080

---
# The receiver's ingress: 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

---
# DNS egress to CoreDNS in kube-system — default-deny also blocks :53, so
# without this every pod loses name resolution
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-dns-egress
  namespace: production
spec:
  podSelector: {}
  policyTypes:
  - Egress
  egress:
  - to:
    - namespaceSelector:
        matchLabels:
          kubernetes.io/metadata.name: kube-system
    ports:
    - protocol: UDP
      port: 53
    - protocol: TCP
      port: 53

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.

Introduced in: Section 9 — Secure SDLC Integration

Secure CI/CD Pipeline — Security Gates

A developer commit flows through four gates — secrets scan, SAST/SCA/licence check, DAST/pen-test review, then image sign/OPA/SBOM — before a verified release reaches production. The pipeline fails closed at each gate.

GitHub Actions — Secure Pipeline

.github/workflows/secure-pipeline.yml (gates 1–2)
# Gates 1-2: source. Third-party actions are pinned by commit SHA, not a tag.
name: Secure CI/CD Pipeline
on: [push, pull_request]

permissions:
  contents: read
  # Fork PRs receive neither repository secrets nor security-events: write,
  # so secret scanning / SARIF upload are best-effort on fork contributions.
  security-events: write

jobs:
  source-gates:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1  # v7.0.1
      with: { fetch-depth: 0 }        # full history for Gitleaks
    - name: Gate 1 — secrets scan
      uses: gitleaks/gitleaks-action@e0c47f4f8be36e29cdc102c57e68cb5cbf0e8d1e  # v3.0.0
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
    - name: Gate 2 — SAST (Semgrep)
      run: |
        docker run --rm -v "$PWD:/src" -w /src semgrep/semgrep \
          semgrep scan --error --config p/security-audit --config p/owasp-top-ten
    - name: Gate 2 — SCA (Trivy)
      uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25  # v0.36.0
      with:
        scan-type: fs
        scan-ref: .
        severity: CRITICAL,HIGH
        exit-code: 1
.github/workflows/secure-pipeline.yml (gates 3–4)
# Gates 3-4: build the artifact, then sign and verify its DIGEST (not a tag).
# Continuation of the gates 1-2 panel: same workflow, same top-level jobs: map.
jobs:
  release:
    needs: source-gates
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    environment: production          # deploy requires approval
    permissions:
      contents: read
      packages: write
      id-token: write                # keyless (OIDC) cosign signing
    steps:
    - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1  # v7.0.1
    - name: Gate 3 — build and push
      id: image
      run: |
        IMAGE=ghcr.io/${{ github.repository }}:${{ github.sha }}
        docker build -t "$IMAGE" . && docker push "$IMAGE"
        echo "digest=$(docker inspect -f '{{index .RepoDigests 0}}' "$IMAGE")" >> "$GITHUB_OUTPUT"
    - name: Gate 3 — scan image
      uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25  # v0.36.0
      with:
        image-ref: ${{ steps.image.outputs.digest }}   # scan the digest, not the tag
        severity: CRITICAL,HIGH
        exit-code: 1
    - name: Gate 3 — SBOM (SPDX)
      uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25  # v0.36.0
      with:
        image-ref: ${{ steps.image.outputs.digest }}
        format: spdx-json
        output: sbom.spdx.json
    - uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6  # v4.1.2
    - name: Gate 3 — sign the digest (keyless)
      run: cosign sign --yes ${{ steps.image.outputs.digest }}
    - name: Gate 4 — verify digest, then deploy
      run: |
        cosign verify \
          --certificate-oidc-issuer https://token.actions.githubusercontent.com \
          --certificate-identity-regexp "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY" \
          ${{ steps.image.outputs.digest }}
        kubectl set image deployment/app app=${{ steps.image.outputs.digest }}

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

On-prem AD/LDAP federates to Entra ID over SAML/OIDC, while on-prem workloads reach Azure over VPN/ExpressRoute (IPSec). Defender for Cloud activity logs stream to the on-prem SIEM, giving one monitoring view across both estates.

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 ExpressRoute
  • • 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
# 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.

Framework Alignment

NIST CSF 2.0: PR (Protect), DE (Detect) — controls mapped per architecture
NIST SP 800-207: Zero Trust reference architecture (pattern 3)
CIS Controls v8.1: 3 (Data Protection), 6 (Access Control), 12 (Network), 16 (App Security)
Azure Well-Architected: Security Pillar best practices
Related: Security Frameworks → | Case Studies →

Reference Architecture Labs

Deploy and validate these reference architectures hands-on. Each lab builds production-grade infrastructure with real security controls.

Deploy Secure Three-Tier on Azure Custom Lab medium
Apply the secure-vnet.tf Terraform module to create the VNet with isolated subnetsDeploy an Application Gateway with HTTPS only and WAF rulesConfigure NSG rules with least-privilege port access per tierEnable NSG Flow Logs, Defender for Cloud, and Azure Monitor alerts from monitoring.tfVerify: attempt direct DB access from internet (should fail)Run Checkov/tfsec against your Terraform and fix all CRITICAL findings
Build a Secure CI/CD Pipeline Custom Lab medium
Implement the GitHub Actions secure pipeline from this guideAdd Gitleaks for secrets scanning (Gate 1)Add Semgrep SAST and Trivy SCA scanning (Gate 2)Add container image scanning and SBOM generation (Gate 3)Implement Cosign image signing and verification (Gate 4)Intentionally introduce a vulnerability and verify the pipeline catches it
Service Mesh Security with Istio Custom Lab hard
Deploy a multi-service app on Kubernetes with IstioEnable STRICT mTLS mode across the meshCreate AuthorizationPolicies that allow only documented service-to-service pathsApply default-deny NetworkPolicies for defense-in-depthVerify: attempt unauthorized service calls (should get 403 RBAC denied)Monitor traffic with Kiali dashboard and Envoy access logs