fintech_terraform

Compliant Azure Platform for Financial Services

Read the full technical breakdown on my blog: Infrastructure as Code: Terraform for Fintech Compliance on Microsoft Azure View the architecture and project details: Fintech Compliance Platform on Microsoft Azure

Terraform-managed cloud infrastructure for a regulated financial services platform on Microsoft Azure. Built to satisfy PCI DSS, SOC 2, and New Zealand financial compliance requirements including the RBNZ BS11 Outsourcing Policy and Privacy Act 2020 from the first infrastructure decision. Every resource operates within a private network boundary with no public endpoints, zero hardcoded credentials, and a tamper-proof audit trail enforced at the infrastructure level.

Compliance is a design constraint in this architecture, not a retrofit. When a regulator asks how encryption is guaranteed or how access is controlled, the answer is Terraform code committed to version control, not a policy document assembled after the fact.


Architectural Decision Process: A Step-by-Step Evolution

Building this platform required solving six consecutive security and compliance problems. Here is the logical progression of how the architecture was designed:

  1. The Isolation Problem: We need a private boundary for financial workloads.
    • Decision: Create a dedicated Virtual Network (Spoke VNet) divided into strict subnets (Public, Application, Data). No resources in the App or Data subnets are allowed public IP addresses.
  2. The Ingress Problem: We must protect the application from OWASP Top 10 web vulnerabilities (e.g., SQL injection, XSS) before traffic reaches our servers.
    • Decision: Deploy an Application Gateway at the edge with a Web Application Firewall (WAF) policy in Prevention Mode. Only clean, verified traffic is forwarded internally to the compute layer.
  3. The Compute Problem: We need to scale elastically without managing individual virtual machines or OS patching.
    • Decision: Deploy an Azure Kubernetes Service (AKS) cluster. To ensure the control plane cannot be compromised from the internet, it is deployed as a Private Cluster.
  4. The Zero-Trust Data Problem: The application needs database passwords, and data must be encrypted, but we cannot hardcode secrets or expose databases to the internet.
    • Decision: Use Azure Key Vault and Azure SQL Database. Both are secured using Private Endpoints, meaning they only possess private IPs inside the VNet. The AKS cluster uses a Managed Identity to authenticate to Key Vault via Azure RBAC—meaning there are zero passwords stored in code.
  5. The Data Exfiltration Problem: If a container is compromised, how do we stop it from sending sensitive financial data to an external attacker’s server?
    • Decision: Implement a Hub & Spoke topology. We deploy an Azure Firewall in a central Hub VNet and use a User Defined Route (UDR) to force every single outbound packet from the Spoke through the firewall. The firewall defaults to deny, only allowing explicit, pre-approved Azure API traffic.
  6. The Audit Problem: PCI DSS and SOC 2 require proof that critical security events are recorded and that no one tampered with the logs.
    • Decision: Stream all firewall, Key Vault, and SQL diagnostic logs to an Azure Storage Account equipped with a 365-day WORM (Write Once, Read Many) immutability policy. Once a log is written, it is mathematically impossible for anyone—even a subscription Owner—to delete or alter it.

Regulatory Context

Financial infrastructure in New Zealand operates under a layered compliance regime. This architecture encodes the following regulatory requirements directly into infrastructure code:

Framework Requirement Terraform Control
PCI DSS Network segmentation, encryption, audit logging Hub and Spoke topology, TDE, WORM storage
Privacy Act 2020 Encryption, access control, data residency Private Endpoints, RBAC, region-pinned resources
RBNZ BS11 Auditability, outsourcing risk control Immutable logs, Git-tracked deployment history
AML/CFT Act Transaction recording and monitoring Service Bus session ordering, SQL audit logs
SOC 2 CC7 Tamper-proof audit trail 365-day WORM immutability policy
ISO 27001 Least-privilege access, secrets management Managed Identity, Key Vault RBAC

Architecture Overview

Internet
    │
    ▼
┌─────────────────────────────┐
│   Application Gateway (WAF) │  Terminates public HTTPS, blocks OWASP threats
└─────────────┬───────────────┘
              │  Clean traffic only, rt-appgw routes to Internet
              ▼
┌───────────────────────────────────────────────────────────────────┐
│                     SPOKE VNet  10.0.0.0/16                       │
│                                                                   │
│  ┌─────────────────┐    ┌──────────────┐    ┌──────────────────┐  │
│  │   AKS Cluster   │    │  SQL Subnet  │    │  AppGW Subnet    │  │
│  │   snet-aks      │───▶│  snet-sql    │    │  snet-appgw      │  │
│  │   10.0.1.0/24   │    │  10.0.2.0/24 │    │  10.0.3.0/24     │  │
│  └────────┬────────┘    └──────────────┘    └──────────────────┘  │
│           │  rt-spoke: 0.0.0.0/0 → Firewall IP                    │
└───────────┼───────────────────────────────────────────────────────┘
            │  VNet Peering (bidirectional, allow forwarded traffic)
┌───────────┼───────────────────────────────────────────────────────┐
│                      HUB VNet  10.10.0.0/16                       │
│                                                                   │
│  ┌─────────────────────┐        ┌──────────────────────────┐      │
│  │   Azure Firewall    │        │      Azure Bastion        │      │
│  │   AzureFirewall     │        │   AzureBastionSubnet      │      │
│  │   Subnet            │        │   Secure RDP/SSH only     │      │
│  └─────────────────────┘        └──────────────────────────┘      │
└───────────────────────────────────────────────────────────────────┘

A Customer Makes a Fund Transfer

This single transaction traces every layer of the architecture from the moment a customer clicks Transfer to the moment the money moves.

CUSTOMER BROWSER
      │
      │  POST /api/transfer  {"from": "ACC001", "to": "ACC002", "amount": 5000}
      ▼
┌──────────────────────────────────────────────────────────┐
│  STEP 1  Application Gateway (WAF)                       │
│                                                          │
│  Public IP receives the HTTPS request.                   │
│  WAF Policy (OWASP 3.2, Prevention Mode) scans:          │
│   ✓ No SQL injection patterns                            │
│   ✓ No XSS payloads                                      │
│   ✓ No malformed headers                                 │
│                                                          │
│  Request is clean. Forwarded internally.                 │
└──────────────────────┬───────────────────────────────────┘
                       │  Private IP routing (snet-appgw to snet-aks)
                       ▼
┌──────────────────────────────────────────────────────────┐
│  STEP 2  AKS Pod (Transfer Service)                      │
│                                                          │
│  The transfer-service pod receives the request.          │
│  It needs the database password to write the record.     │
│  It calls Azure Key Vault using its Managed Identity     │
│  token. No username, no password hardcoded anywhere.     │
│                                                          │
│  Key Vault returns: sql-admin-password = "xK9#mP2..."   │
└──────────────────────┬───────────────────────────────────┘
                       │  Private Endpoint (pe-kv-dev, inside snet-sql)
                       ▼
┌──────────────────────────────────────────────────────────┐
│  STEP 3  Azure SQL (Private Endpoint)                    │
│                                                          │
│  Pod connects to sql-fintech-dev.database.windows.net    │
│  This resolves to a private IP inside snet-sql.          │
│  TDE encrypts the row on disk automatically.             │
│                                                          │
│  SQL writes: INSERT INTO transfers VALUES (...)          │
│  Status: PENDING                                         │
└──────────────────────┬───────────────────────────────────┘
                       │
                       ▼
┌──────────────────────────────────────────────────────────┐
│  STEP 4  Service Bus (Ordered Queue)                     │
│                                                          │
│  Pod publishes event to Service Bus queue.               │
│  Queue: transactions-ordered                             │
│  SessionId: "ACC001" (guarantees per-account FIFO order) │
│  Message: { transferId: "TXN-9981", amount: 5000 }       │
│                                                          │
│  Decouples API response from actual processing.          │
│  Customer receives instant Transfer Submitted receipt.   │
└──────────────────────┬───────────────────────────────────┘
                       │
                       ▼
┌──────────────────────────────────────────────────────────┐
│  STEP 5  AKS Pod (Settlement Worker)                     │
│                                                          │
│  A separate worker pod picks up the message from         │
│  Service Bus in guaranteed FIFO order per account.       │
│  It deducts from ACC001, credits ACC002.                 │
│  Updates SQL: Status = COMPLETED                         │
│                                                          │
│  Container image pulled from ACR (private registry)      │
│  using AKS Managed Identity. No Docker credentials.      │
└──────────────────────┬───────────────────────────────────┘
                       │  Egress: any outbound call (e.g. external rate API)
                       ▼
┌──────────────────────────────────────────────────────────┐
│  STEP 6  Azure Firewall (Egress Gate)                    │
│                                                          │
│  Route table on snet-aks forces all egress through       │
│  Azure Firewall in the Hub VNet.                         │
│                                                          │
│  Firewall checks Application Rule Collection:            │
│   ✓ *.azure.com ALLOW                                    │
│   ✗ *.unknown-domain.com DENY and LOG                    │
│                                                          │
│  Only pre-approved Azure APIs are reachable.             │
└──────────────────────┬───────────────────────────────────┘
                       │  All events written to:
                       ▼
┌──────────────────────────────────────────────────────────┐
│  STEP 7  Immutable Audit Log (WORM Storage)              │
│                                                          │
│  Every firewall decision, every Key Vault secret read,   │
│  every SQL transaction is logged to Storage Account.     │
│                                                          │
│  Immutability Policy: 365 days, locked.                  │
│  No principal, including subscription Owners, can        │
│  delete these logs. Satisfies PCI DSS Requirement 10.    │
└──────────────────────────────────────────────────────────┘

This single transaction touched every service in the architecture: Application Gateway WAF → AKS → Key Vault → SQL → Service Bus → AKS Worker → Azure Firewall → Audit Storage.


How Each Service Works Together

Traffic Ingress: Internet to AKS via Application Gateway

When a request arrives at the public IP, the Application Gateway WAF Policy (OWASP 3.2, Prevention Mode) inspects it for SQL injection, XSS, and all OWASP Top 10 attack patterns. Only clean requests are forwarded over the private network to the AKS backend pool. Malicious requests are dropped at the edge before they reach the application.

Compute: AKS Cluster

The AKS cluster runs with private_cluster_enabled = true, meaning the Kubernetes API server has no public IP. It uses a SystemAssigned Managed Identity, so no passwords exist for the cluster itself. Azure grants it access to ACR and Key Vault via RBAC role assignments scoped to exactly those resources.

Egress Filtering: AKS to Azure Firewall to Internet

Every outbound packet from a pod hits the Route Table on snet-aks which has a single rule directing all traffic to the Azure Firewall Private IP. Traffic crosses the VNet Peering into the Hub VNet and is inspected by Azure Firewall against the Application Rule Collection. Only pre-approved Azure API hostnames are allowed. Everything else is denied and logged.

Secrets Management: Key Vault Zero-Trust Flow

Terraform generates a random 20-character SQL admin password. That password is stored as a Key Vault Secret. Key Vault is accessible only via a Private Endpoint inside snet-sql. The AKS Managed Identity is granted the Key Vault Secrets User RBAC role — it can read the secret at runtime but cannot modify or delete it. No passwords appear in code, config files, or environment variables.

Database: Private SQL with Encryption

Azure SQL has public_network_access_enabled = false. Its DNS resolves only to a private IP inside the VNet via a Private Endpoint. All data on disk is encrypted via Transparent Data Encryption. TLS 1.2 is enforced at the protocol level. Daily backups run at 23:00 and are retained for 30 days via Recovery Services Vault.

Async Messaging: Service Bus for Financial FIFO Ordering

Financial transactions are published to a Service Bus Premium queue with requires_session = true. This guarantees per-account FIFO ordering so debits are always processed before credits for the same account. The Premium SKU supports Private Endpoints so message payloads never cross the public internet.

Container Images: ACR with Managed Identity Pull

The CI/CD pipeline pushes Docker images to ACR with admin_enabled = false. The AKS cluster is assigned the AcrPull role and authenticates using its Managed Identity token. public_network_access_enabled = false means image pulls happen entirely within the private network.

Compliance: Immutable WORM Audit Logs

All diagnostic logs land in a Storage Account with a 365-day immutability policy (Write Once Read Many). Once written, no principal including subscription Owners can delete or modify those logs. This satisfies PCI DSS Requirement 10 and SOC 2 CC7 for tamper-proof audit trails.


Repository Structure

.
├── environments/
│   └── dev/
│       └── terraform.tfvars        # Dev environment variable values
├── modules/
│   ├── acr/                        # Azure Container Registry + AKS role assignment
│   ├── aks/                        # Private AKS cluster + network profile
│   ├── appgw/                      # Application Gateway v2 + standalone WAF Policy
│   ├── hub/                        # Azure Firewall + Bastion + Hub VNet
│   ├── keyvault/                   # Key Vault + random password + RBAC + Private Endpoint
│   ├── network/                    # Spoke VNet, subnets, NSG, UDR route tables
│   ├── observability/              # Immutable Storage + Recovery Services Vault
│   ├── servicebus/                 # Service Bus Premium + ordered session queue
│   └── sql/                        # SQL Server + Database + Private Endpoint
├── main.tf                         # Root module wiring all components and VNet peerings
├── provider.tf                     # AzureRM and Random provider configuration
├── variables.tf                    # Root variable declarations
└── README.md

Security Controls

Control Implementation
No public endpoints public_network_access_enabled = false on SQL, ACR, Key Vault, Service Bus
No hardcoded secrets random_password stored in Key Vault, retrieved at runtime via Managed Identity
Egress filtering UDR on snet-aks and snet-sql forces all outbound traffic through Azure Firewall
WAF protection Standalone WAF Policy (OWASP 3.2, Prevention mode) linked to Application Gateway
Encryption at rest TDE on SQL, automatic Storage Account encryption
Encryption in transit TLS 1.2 minimum enforced across all services
Immutable audit trail WORM storage with 365-day lock satisfies PCI DSS Requirement 10
Least-privilege identity SystemAssigned Managed Identity with scoped RBAC role assignments
Private DNS resolution Private Endpoints on SQL and Key Vault provide private IPs inside the VNet
AppGW subnet routing Dedicated route table with 0.0.0.0/0 next hop Internet, required for AGW v2

Deployment

Prerequisites

Terraform CLI 1.5 or higher and Azure CLI installed, with an active Azure subscription where the deploying principal holds Owner or Contributor plus User Access Administrator roles.

Steps

Authenticate to Azure:

az login
az account set --subscription "YOUR_SUBSCRIPTION_ID"

Initialize Terraform:

terraform init

Review the execution plan:

terraform plan -var-file="environments/dev/terraform.tfvars"

Apply the infrastructure (full deployment takes approximately 15 to 20 minutes):

terraform apply -var-file="environments/dev/terraform.tfvars"

Destroy when done:

terraform destroy -var-file="environments/dev/terraform.tfvars"

Configuration Reference

Variable Description Example
location Azure region for all resources australiaeast
environment Environment tag appended to all resource names dev
resource_group_name Resource group containing all deployed resources rg-fintech-dev
vnet_address_space Spoke VNet CIDR block ["10.0.0.0/16"]
aks_node_count Number of AKS worker nodes 2
aks_vm_size VM size for each worker node Standard_D2s_v3