← All work

Platform & infrastructure · Security & compliance

A multi-account AWS organisation, built security-first

Personal engineering work · public repository

View Terraform repository
Focus
Platform & infrastructure · Security & compliance
Evidence
3 documented proof points
Decision record
Firsthand trade-off included

The brief

What was built

Hierarchical OUs, a centralised security account, production-grade SCPs with root-account protection, and Security Lake telemetry in OCSF — provisioned end to end in Terraform.

Evidence

Implemented controls

  • Cross-account least-privilege IAM with S3 + DynamoDB + KMS remote state
  • Hub-and-spoke Transit Gateway networking across accounts
  • Config drift detection with SNS/SQS routing to a SOC alerting path

Decision record

Terraform modules should not own their own authentication

I initially defined the Kubernetes and Helm providers inside the ArgoCD Terraform module. It looked self-contained, but it coupled provider authentication to the module and made short-lived EKS credentials harder to manage. I moved the providers to the root environment and switched authentication to `aws eks get-token`, so credentials refresh automatically and the same provider configuration can be reused across modules. The lesson was simple: reusable Terraform modules should describe infrastructure, not own environment-specific authentication.

Built architecture

Account boundaries and why they exist

Every node maps to the public Terraform repository. The explanation records the rejected failure mode, not only the service name.

Management account

AWS Organizations

The account hierarchy — management, security and workload accounts under organisational units.

Why: Separate accounts are the only hard blast-radius boundary AWS offers. IAM boundaries within one account are a policy away from being wrong; an account boundary is not.

Service Control Policies

Organisation-wide guardrails, including root-account protection with explicit exceptions.

Why: SCPs constrain what an account can do even if its own IAM is misconfigured. They are the control that assumes the other controls will eventually fail.

Cross-account IAM

Assume-role paths between accounts, scoped to least privilege.

Why: Terraform modules describe infrastructure; they do not own environment credentials. Authentication resolves at the root, so short-lived credentials refresh rather than being baked into a module.

Remote state

S3 for state, DynamoDB for locking, KMS for encryption at rest.

Why: State is the most sensitive artefact in the estate — it holds resource identifiers and sometimes secrets. Locking prevents two applies racing; encryption means a bucket misconfiguration is not immediately a breach.

Networking

Workload VPC — spoke

Private application and database subnets across availability zones, with no Internet Gateway or local NAT. Flow logs are delivered cross-account in Parquet.

Why: Removing every direct internet path makes the inspection route mandatory rather than optional. A compromised workload cannot bypass the central egress controls through its own NAT gateway.

Private service endpoints

Interface endpoints for ECR, EC2 and SSM, STS, Logs, load balancing, Auto Scaling, SNS and SQS; dedicated endpoints for EKS, Secrets Manager and KMS; and a gateway endpoint for S3.

Why: Workloads can consume AWS services without crossing the public internet. Endpoint policies add a network-side check to identity controls for secrets, encryption keys and object storage.

Transit Gateway

Explicit attachments and route-table associations connect the workload spoke to the egress hub; default association and propagation are disabled.

Why: A hub-and-spoke model replaces an ungovernable peering mesh with one routing policy. Appliance mode keeps forward and return traffic symmetric through the stateful firewall.

Egress VPC — hub

Dedicated Transit Gateway, firewall and public subnet tiers, with an Internet Gateway and one NAT Gateway per availability zone.

Why: Internet access exists in one controlled VPC instead of every workload VPC. Per-AZ NAT preserves fault isolation and avoids making healthy zones depend on a cross-zone egress path.

Network Firewall · fail-close

A strict-order HTTP host and TLS SNI allowlist inspects outbound traffic. EventBridge and Lambda monitor every firewall endpoint and can replace the TGW default route with a blackhole.

Why: Unapproved destinations are dropped, limiting command-and-control and data-exfiltration paths. If inspection becomes unhealthy, the design chooses an outage over silently bypassing the control.

Workload account

EKS

A managed Kubernetes cluster with workers in private subnets, KMS-encrypted secrets, control-plane logging, and CoreDNS, VPC CNI, EBS CSI and EFS CSI add-ons.

Why: Application compute stays inside the workload boundary, while the security account retains the evidence. Managed control-plane and storage integrations reduce undifferentiated operational work.

Karpenter · managed nodes

A baseline managed node group provides stable capacity while Karpenter discovers tagged subnets and security groups to provision additional workers on demand.

Why: The baseline keeps critical services schedulable; demand-driven nodes reduce idle capacity without making cluster availability depend entirely on just-in-time provisioning.

IRSA · OIDC

OIDC-bound IAM roles are scoped to Kubernetes service accounts for Karpenter, VPC CNI, EBS CSI and EFS CSI.

Why: Pods receive only the AWS permissions their controller needs instead of inheriting a broad worker-node role. Identity remains independently auditable at the service-account boundary.

RDS · backup storage

Multi-AZ SQL Server in isolated database subnets, with KMS encryption, IAM authentication, 35-day backups and deletion protection; backup objects live in a versioned, KMS-encrypted S3 bucket.

Why: The database accepts SQL traffic only from the EKS cluster security group. Compute remains replaceable while the durable layer stays private, recoverable and protected from casual deletion.

ArgoCD

A version-pinned Helm release in its own namespace, configured to wait for workloads and jobs and clean up an unsuccessful installation.

Why: Declarative Git state makes cluster changes reviewable and reproducible. Provider authentication stays at the root and uses short-lived EKS tokens instead of credentials owned by the module.

Security account

CloudTrail · GuardDuty · Config · Flow Logs

API audit, threat detection, configuration drift and network telemetry — all delivered cross-account.

Why: Collection lives in the security account, not with the workload. Anyone who compromises a workload should not also be able to delete the evidence.

Security Lake (OCSF 1.1.0)

All sources normalised into one open schema with 365-day retention.

Why: Four sources in four formats means four query languages and no correlation. Normalising to OCSF is what makes a question like “what else did this principal touch” answerable in one query.

OpenSearch · Athena

Interactive search over the lake, plus SQL for anything ad hoc.

Why: Detections run as OpenSearch monitors rather than EventBridge rules, so alerts can be query-based, thresholded and correlated the way a SOC actually works.

SOC alerting — SNS · SQS · DLQ

Severity-routed notification, with a dead-letter queue on the delivery path.

Why: An alerting pipeline that can silently drop messages is worse than none, because it is trusted. The DLQ exists so a failed alert is itself an alert.

Runbooks · MITRE ATT&CK mapping

Detections committed alongside the response procedure and their ATT&CK technique.

Why: A detection with no runbook produces a page nobody knows how to action. Mapping to ATT&CK turns a list of alerts into coverage you can find the gaps in.

Return to all work