akanjilal.dev
Back to reference architectures
Reference architectureAmazon EKS0 RPO core, 0 RTO99.999 percentPCI DSS

Enterprise Kubernetes for a critical payments platform on AWS

A multi-region, active-active design on Amazon Elastic Kubernetes Service for payment authorization, settlement, account-to-account transfer rails, and real-time messaging, built for zero data loss on the core ledger and continuous availability across two Canadian regions.

Part I, Reference architecture

1. Executive summary

This document describes a production-grade architecture for a critical payments platform running on Amazon Elastic Kubernetes Service across two Canadian AWS regions, Canada Central in Montreal and Canada West in Calgary. The platform carries a full product portfolio of payment authorization, settlement, account-to-account transfer rails, and real-time messaging. The defining constraint is that the core payment and settlement ledger must lose no committed data under any single failure, which is a recovery point objective (RPO) of zero, while every product remains continuously available, which is a recovery time objective (RTO) of zero at the product level. The target availability is 99.999 percent.

The hardest problem in the design is achieving a literal zero recovery point objective for the ledger across two regions while keeping all data inside Canada. The analysis below shows that the managed service which would normally solve this, Amazon Aurora DSQL in a multi-region configuration, cannot be used here because its Canadian regions are offered only as single-region clusters and are not part of any multi-region set, and because its multi-region quorum requires a third region that would sit outside Canada. Amazon Aurora Global Database, the usual cross-region option, replicates asynchronously and therefore cannot offer a literal zero recovery point objective. With only two Canadian regions there is also no third failure domain for an automatic quorum to survive a full region loss.

The resolution is a cell-based, regionally owned ledger with synchronous cross-region commit. The ledger keyspace is divided into cells, each cell is owned by one region, and every commit is made durable in the partner region before it is acknowledged. This delivers a true zero recovery point objective across a complete region loss. The unavoidable cost, which the design contains and makes explicit, is that the inter-region round trip is added to commit latency on mutating transactions only, and that during a cross-region network partition one side must stop writing to preserve consistency and durability. A witness placed in an on-premises Canadian data center adjudicates that partition so the surviving region continues without data loss and without split-brain.

Around this core, the platform uses a private-only network posture, full account-level isolation per environment and per region, a defense-in-depth security model that minimizes the cardholder data environment, image signing with admission-time verification, GitOps delivery with region-by-region promotion, and a 99.999 percent service level objective governed by an error budget. The remainder of the document records the requirements, the principles, the reference architecture, and the decisions that produced it, each with the alternatives that were considered and rejected.

2. Context and scope

The platform is a regulated financial system. It processes high transaction volumes, integrates with partner banks and a card switch, and exchanges settlement and reconciliation files with existing systems of record in an on-premises data center. Access is private only. There is no public internet ingress to any workload. Inbound traffic arrives over AWS Direct Connect private virtual interfaces and the AWS backbone.

In scope

  • The compute platform on Amazon Elastic Kubernetes Service, including cluster topology, node strategy, and tenancy.
  • The data tier for the ledger and for the supporting products, including the cross-region resilience model.
  • Network connectivity, segmentation, and the private ingress and controlled egress model.
  • Security and Payment Card Industry Data Security Standard (PCI DSS) alignment, secrets, and key management.
  • Continuous integration and continuous delivery, GitOps, and progressive multi-region rollout.
  • Observability, service level objectives, and the failure and recovery runbooks.

Out of scope

  • The internal business logic of each product beyond what is needed to size resilience and latency.
  • The on-premises core banking systems themselves, which are treated as integration endpoints.
  • Card scheme certification activities, which depend on the specific scheme and acquirer.

A note on the regions

Canada has exactly two AWS regions, Canada Central and Canada West. Several resilience patterns that are routine in the United States or Europe, where three or more regions form a set, are not available here. This single fact shapes the data tier more than any other and is treated explicitly throughout.

3. Functional requirements

The platform delivers four product families. Each is summarized below with the behavior that the architecture must support.

3.1 Payment authorization

  • Accept authorization requests from partner channels and the card switch over private connectivity, and return an approve or decline decision within a tight latency budget.
  • Apply risk and fraud scoring using cached features and rules before the ledger is touched.
  • Tokenize the primary account number (PAN) at the boundary so that clear card data does not flow into the application or the ledger.
  • Record an authorization hold against the account ledger with strong consistency, so that no two requests can double-spend the same available balance.

3.2 Settlement

  • Process clearing and settlement against the same ledger as authorization, so that balances reflect a single source of truth.
  • Produce settlement and reconciliation outputs for exchange with the on-premises systems of record.
  • Guarantee that a settled transaction is never lost, which ties settlement to the zero recovery point objective tier.

3.3 Account-to-account transfer rails

  • Support real-time account-to-account transfers in the style of domestic transfer rails, including request to pay and immediate credit.
  • Maintain idempotency so that a retried transfer is never applied twice.
  • Tolerate a short recovery point objective of one to five minutes, which allows an asynchronous cross-region data strategy for this product family.

3.4 Real-time messaging

  • Provide a low-latency messaging backbone for events such as authorization outcomes, settlement notifications, and status updates.
  • Deliver events at least once with consumer-side idempotency, and preserve ordering where a product requires it.
  • Replicate the event stream across regions so that consumers continue after a region loss within the one to five minute objective.

4. Non-functional requirements

The non-functional requirements are the binding constraints. They are stated as targets and are traced to the design in the next section.

IDCategoryRequirementTarget
N1Durability, coreNo loss of committed payment and settlement data under any single failure, including a full region lossRecovery point objective of zero
N2Durability, otherBounded data loss for transfer rails and messaging under a region lossRecovery point objective of 1 to 5 minutes
N3AvailabilityContinuous service at the product level, including during a region lossRecovery time objective of zero, always on
N4Service levelPlatform availability over the rolling year99.999 percent
N5ConsistencyStrong consistency for all business transactions on the ledgerSnapshot isolation or stronger, no double spend
N6LatencyAuthorization decision within budget at high volumeTight per-decision budget, cross-region cost paid once at commit
N7Data residencyAll data, including quorum metadata, remains in CanadaStrict, enforced by policy
N8SecurityPayment Card Industry Data Security Standard alignment, best-practice posturePCI DSS, defense in depth, minimized scope
N9Network postureNo public ingress, controlled egressPrivate only, allow-list egress
N10SeparationEnvironment and regional isolationFull account separation, production, non-production, performance
N11ScalabilityAbsorb peak volumes and bursts without manual interventionElastic, cost aware, no graphics processing units
N12RecoverabilityRegion loss is served from the other region by definitionTested failover, automated where safe
N13ObservabilityFull signal coverage, audit, and service level governanceMetrics, traces, logs, error budget

5. Requirements traceability

Each non-functional requirement maps to one or more decision records, which are detailed in section 8. The mapping makes it possible to see why each requirement is met and where to look if a requirement changes.

ReqMet byHow it is satisfied
N1ADR-01, ADR-02, ADR-05Cell-based ownership with synchronous cross-region commit and a Canadian witness
N2ADR-03Aurora Global Database, Amazon MSK replication, Redis global datastore
N3ADR-01, ADR-04, ADR-05Active-active cells, health-based routing, fast cell adoption
N4ADR-04, ADR-14Active-active topology and a 99.999 percent objective with an error budget
N5ADR-02Single-writer-per-cell ledger with synchronous durability
N6ADR-02, ADR-04Local execution with the cross-region cost confined to commit
N7ADR-05, ADR-06Canadian witness and a service control policy that denies other regions
N8ADR-11, ADR-12Defense in depth, scope minimization, signed images, managed keys
N9ADR-09Private-only ingress and allow-list egress through a network firewall
N10ADR-06, ADR-07Account per environment and region, namespace isolation within clusters
N11ADR-08Managed and self-managed node scaling on Graviton with Spot for burst
N12ADR-05, ADR-13Tested failover runbooks and progressive multi-region delivery
N13ADR-14Metrics, traces, logs, central security information and event management

6. Architecture principles

Correctness before convenience

On the core ledger, never trade away consistency or durability for latency or operational ease. Make the cost of correctness explicit and contain it.

Keep data in Canada

Every byte of business data and every piece of quorum metadata stays in Canada. Residency is enforced by policy, not by convention.

Private by default

No public ingress. Egress is denied unless explicitly allowed. Identity is established before any routing decision is made.

Minimize the blast radius

Isolate by account, by region, by cell, and by namespace. A failure or a compromise is contained to the smallest possible scope.

Everything is reconciled from Git

Cluster and application state is declared in Git and reconciled continuously. Drift is detected and corrected. Promotion is auditable.

Test resilience rather than assume it

Failover and partition behavior are exercised regularly with fault injection. A failover path that is never exercised should be treated as broken until proven otherwise.

7. Reference architecture overview

Both regions are active. Amazon Route 53 directs each caller to the nearest healthy region using latency-based routing with health checks. Within each region a private Amazon Elastic Kubernetes Service cluster runs the product workloads behind an internal load balancer, with no public load balancers anywhere. The data tier sits outside the cluster as managed services in private subnets, reached over virtual private cloud (VPC) interface endpoints. The two regions are joined by AWS Cloud WAN, and AWS Direct Connect links both regions to the on-premises data center, which also hosts the ledger witness.

1 Access channels (private only) Partner banks and card switch Internal product applications No public internet ingress 2 Amazon Route 53 - global traffic mgmt Latency-based routing to nearest healthy region Health-check regional failover, ~30 s detection Private hosted zones over the AWS backbone Edge and identity controls AWS WAF and Shield Advanced mTLS service identity end to end Network Firewall egress control AWS Region - Canada (Central) ca-central-1 Three Availability Zones: AZ 1 · AZ 2 · AZ 3 3 Internal ingress - Gateway API on internal NLB AWS PrivateLink, no public LBs. mTLS at mesh edge. 4 Amazon EKS - production fleet (private cluster) Mesh: Istio ambient, mTLS STRICT, authz policies Payment authTier-0 SettlementTier-0 PaymentsTier-1 MessagingTier-1 5 Data tier - external managed services (in region) Tier-0 ledger - Aurora PostgreSQL, Multi-AZ synchronous Writer for cells C1 to C8 - 0 RPO in region Aurora Global DBTier-1 async Amazon MSKevent backbone ElastiCacheRedis global AWS Cloud WAN core Synchronous commit 0 RPO - cells Tier-1 async AWS Region - Canada West ca-west-1 Three Availability Zones: AZ 1 · AZ 2 · AZ 3 3 Internal ingress - Gateway API on internal NLB AWS PrivateLink, no public LBs. mTLS at mesh edge. 4 Amazon EKS - production fleet (private cluster) Mesh: Istio ambient, mTLS STRICT, authz policies Payment authTier-0 SettlementTier-0 PaymentsTier-1 MessagingTier-1 5 Data tier - external managed services (in region) Tier-0 ledger - Aurora PostgreSQL, Multi-AZ synchronous Writer for cells C9 to C16 - 0 RPO in region Aurora Global DBTier-1 async Amazon MSKevent backbone ElastiCacheRedis global On-premises data center - Canada third failure domain, in-country Direct Connect 6 AWS Direct Connect (redundant) Two dedicated links from separate sites Private VIFs into both regions, transit to Cloud WAN Carries partner and channel traffic 7 Tier-0 witness and arbiter Lightweight quorum witness for cells Breaks ties on a cross-region partition Journal metadata only, keeps write quorum in Canada Systems of record and integration Core banking and ledger integration Reconciliation and settlement exchange Private connectivity only, no workload state on prem
Figure 1. Global topology. Both Canadian regions are active. Amazon Route 53 performs global traffic management, AWS Cloud WAN forms the core network, and an on-premises data center in Canada provides a third failure domain that hosts the ledger witness.

The remainder of the document works inward from this picture. Section 8 records the decisions in order, starting with the resilience model and the data tier, which are the decisions that everything else depends on.

8. Architecture decision records

Each record states the context, the decision, the alternatives that were considered and rejected, and the consequences. The records are ordered so that foundational decisions appear before the decisions that depend on them.

ADR-01

Tiered resilience model

Accepted

Context

The products do not all share the same durability need. Payment authorization and settlement must lose nothing, while transfer rails and messaging can tolerate a small bounded loss. Treating every product as if it needed a literal zero recovery point objective would impose cross-region commit latency on workloads that do not require it, and would inflate cost and complexity.

Decision

Split the platform into two durability tiers. Tier zero covers payment authorization and the settlement ledger and carries a recovery point objective of zero. Tier one covers transfer rails and real-time messaging and carries a recovery point objective of one to five minutes. Both tiers carry a product-level recovery time objective of zero, meaning the service remains available through a region loss. The tier boundary is a first-class design seam that determines which data strategy each workload uses.

Alternatives considered

Single uniform tier at zero recovery point objective. Rejected because it would force the cross-region synchronous commit cost onto messaging and rails that do not need it, raising latency and cost for no resilience benefit.
Single uniform tier at near-zero recovery point objective. Rejected because it would fail the hard zero requirement on the ledger, which is the central constraint of the platform.

Consequences

The data tier decisions divide along the tier boundary. Tier zero uses synchronous cross-region durability, recorded in ADR-02. Tier one uses asynchronous managed replication, recorded in ADR-03. Workload placement and routing must respect the tier of the data they touch.

ADR-02

Tier-zero data tier, cell ownership with synchronous cross-region commit

Accepted

Context

A literal zero recovery point objective across a full region loss means that a committed transaction must be durable in a second region before it is acknowledged. With only two Canadian regions and strict residency, the usual managed answers do not apply, as the rejected alternatives below show. The design must therefore provide synchronous cross-region durability while keeping the ledger writable in both regions for availability and throughput.

Decision

Partition the ledger keyspace by a consistent hash of the account key into cells, and assign each cell a single home region. Canada Central owns one half of the cells and Canada West owns the other half, so both regions are active and no two regions ever write the same key at the same time. Each cell runs on Amazon Aurora PostgreSQL with synchronous replication, where the primary does not acknowledge a commit until a synchronous standby in the partner region has persisted it. In PostgreSQL terms this is synchronous_commit = remote_apply with a synchronous standby set that includes the partner region. A local Multi-AZ replica provides zero data loss within the region, and the partner-region standby provides zero data loss across a region loss. On loss of a region, the surviving region promotes the partner-region standby for the failed region cells and adopts them, serving the full keyspace. Because the standby already holds every committed transaction, the recovery point objective is zero and the recovery time objective is the adoption time, which is driven to seconds.

Alternatives considered

Amazon Aurora DSQL, multi-region. Aurora DSQL provides synchronous cross-region commit with strong consistency and would be the natural answer. It is rejected here because its Canadian regions are offered only as single-region clusters and are not part of any multi-region set, and because a multi-region cluster requires a witness region that for Canada would have to sit outside the country, breaking residency.
Amazon Aurora Global Database. Rejected for tier zero because its cross-region replication is asynchronous with replication latency typically under one second, which is near zero but not zero. It is the right tool for tier one and is used there.
A quorum distributed database across the two regions. Rejected because a majority quorum needs a third independent failure domain to survive a full region loss, and Canada has only two regions. Without a Canadian third domain there is no automatic majority survivor.

Consequences

The platform accepts that mutating transactions pay the inter-region round trip at commit. Reads and the entire execution phase stay local, so the cost is spent once per write rather than per statement. The single-writer-per-cell rule removes cross-region write conflicts by construction. The partition behavior and the witness are handled in ADR-05. Operating synchronous replication for a tier-zero ledger is a deliberate operational commitment that the runbooks in section 9 support.

1 Cell map - ledger keyspace partitioned by consistent hash of the account key home: Central home: West C1 C2 C3 C4 C5 C6 C7 C8 C9 C10 C11 C12 C13 C14 C15 C16 Commit path - mutating txn 1 Smart client routes the write to the home region of the cell. 2 Primary executes locally. Reads stay in region. 3 On COMMIT, synchronous quorum: one local AZ replica and the partner-region standby. 4 Partner standby persists the commit. Inter-region round trip is added here, on commit. 5 Commit acknowledged only after quorum confirms. 0 RPO across regions. 6 If partner standby is unreachable, the witness adjudicates per partition policy. Canada (Central) - ca-central-1 writer C1-C8 Primary ledger (writer) - cells C1-C8 Aurora PostgreSQL - synchronous_commit = remote_apply synchronous_standby_names = ANY 1 (local_az, partner_region) Local Multi-AZ synchronous replicas - 0 RPO in region AZ 1 replica AZ 2 replica AZ 3 replica Synchronous standby - partner cells C9-C16 Receives every committed transaction from the partner region Promotion target on partner-region loss (cell adoption) Canada West - ca-west-1 writer C9-C16 Primary ledger (writer) - cells C9-C16 Aurora PostgreSQL - synchronous_commit = remote_apply synchronous_standby_names = ANY 1 (local_az, partner_region) Local Multi-AZ synchronous replicas - 0 RPO in region AZ 1 replica AZ 2 replica AZ 3 replica Synchronous standby - partner cells C1-C8 Receives every committed transaction from the partner region Promotion target on partner-region loss (cell adoption) sync C1-C8 sync C9-C16 2 Tier-0 witness and arbiter - on-premises data center (Canada) Quorum witness that holds journal metadata only, never cardholder data. On a cross-region partition it adjudicates which side keeps writing, preventing split-brain. 3 Commit latency anatomy - where the cross-region cost lands BEGIN and local execution in region, reads and writes local local AZ fsync sub-millisecond partner-region durable persist inter-region round trip - the added cost ACK to client 0 RPO guaranteed Region loss - cell adoption a Central is lost. Router marks C1-C8 for adoption. b West promotes its standby for C1-C8 to writer. c West now serves all 16 cells. d Route 53 steers all clients to West. RPO0 (zero) RTOseconds
Figure 2. The tier-zero data plane. Each region is the writer for its own cells and the synchronous standby for the partner cells. The cross-region cost lands only at commit, and cell adoption preserves a zero recovery point objective on a region loss.
ADR-03

Tier-one data tier, asynchronous managed replication

Accepted

Context

Transfer rails and messaging tolerate a recovery point objective of one to five minutes. They need cross-region continuity without the latency cost of synchronous commit.

Decision

Use Amazon Aurora Global Database for relational state in the tier-one products, with its asynchronous replication and managed fast failover. Use Amazon Managed Streaming for Apache Kafka (Amazon MSK) with cross-region replication for the event backbone. Use Amazon ElastiCache for Redis with a global datastore for cross-region cache. Each of these comfortably meets the one to five minute objective and avoids any synchronous cross-region path.

Alternatives considered

Reuse the tier-zero synchronous design for tier one. Rejected as unnecessary cost and latency for data that does not require zero loss.

Consequences

Consumers must be idempotent and tolerate at-least-once delivery, which is already required by the messaging product. A region loss can drop at most the replication-lag window, which is held within the objective and is acceptable for these products.

ADR-04

Active-active cell topology and global traffic management

Accepted

Context

Both regions must serve traffic to meet the always-on objective and to use capacity efficiently, while honoring the single-writer-per-cell rule from ADR-02.

Decision

Run both regions active. Amazon Route 53 performs latency-based routing with health checks so that callers reach the nearest healthy region. A smart client and a cell router resolve the home region for each account key. A request that lands in a region which does not own the target cell is steered to the owning region over the core network. On a health-check failure, Route 53 withdraws the unhealthy region and all traffic flows to the survivor, which by then has adopted the failed cells.

Alternatives considered

Active-passive with a standby region. Rejected because it wastes half the footprint and lengthens recovery, and because the ledger already supports active writers per cell.
Active-active writes to the same keys in both regions. Rejected because strong consistency with zero loss across two regions cannot keep the same key writable on both sides during a partition. Cell ownership avoids the conflict entirely.

Consequences

Routing must be cell-aware, and the cell map is itself a small piece of strongly consistent state that the router consults. The end-to-end request path, including where the cross-region cost appears, is shown below.

Channel and partner network Edge - Route 53, WAF, NLB, mTLS Payment authorization service (CDE) Risk scoring and tokenization Tier-0 ledger - home region primary Partner-region synchronous standby Event backbone and downstream (MSK) 1 Auth request over Direct Connect 2 Route to nearest healthy region, WAF, mTLS 3 Auth service receives, mesh authz 4 Cell router maps key to home cell 5 Risk score from Redis and rules 6 Tokenize PAN to token 7 Ledger executes locally COMMIT (quorum) 8 Persist commit synchronous, durable cross-region round trip Ack - 0 RPO 9 10 Emit settlement and notify events 11 Authorization response Latency budget Steps 1 to 7 and the response are in region. Only step 8 crosses regions, so inter-region cost is paid once per auth, at commit. Durability guarantee Authorization is acknowledged only after the partner region has the commit. A full loss of the home region after step 9 loses nothing. RPO is zero.
Figure 3. The lifecycle of one payment authorization. Edge, application, risk, and tokenization steps run in region. Only the commit crosses regions, and the authorization is acknowledged only after the partner region holds the commit.
ADR-05

Partition and failover policy

Accepted

Context

Strong consistency, a zero recovery point objective, and a network partition cannot all hold at once for the same data. During a cross-region partition the platform must choose, per cell, between continuing to write and preserving zero loss. A naive choice risks either data loss or a split-brain in which both regions write the same cells and diverge.

Decision

Place a lightweight witness and arbiter in the on-premises data center, which is a third failure domain inside Canada reached over AWS Direct Connect. The witness holds journal metadata only and never holds cardholder data. The synchronous standby set is configured so that a local Availability Zone replica and the partner-region standby together form the durability quorum, which means a local zone fault does not block writes. On a true cross-region partition, the witness adjudicates which side retains ownership of each contested cell. The side that loses the adjudication stops writing those cells until the partition heals, which preserves zero loss and prevents split-brain. The side that wins continues to serve.

Alternatives considered

Degrade to asynchronous replication during a partition. Rejected because it would break the zero recovery point objective for the duration of the partition, which is exactly the moment the guarantee matters.
Block all writes on any partner unreachability. Rejected as too blunt, because a single Availability Zone issue should not halt a region. The quorum set and the witness give a more precise outcome.

Consequences

A cross-region partition reduces write availability for the cells that lose adjudication, which is the correct trade for a system that must never lose committed money. The witness is a critical dependency and is itself made redundant within the data center. Partition handling is exercised by fault injection on a schedule.

ADR-06

Cluster separation and multi-account landing zone

Accepted

Context

The platform requires full separation across environments and regions, strong blast-radius control, and residency enforcement. The separation model was selected after weighing namespace-only, cluster-per-environment, and account-per-environment isolation.

Decision

Adopt a multi-account landing zone under AWS Organizations. Each environment and each region runs in its own dedicated account, so production in Canada Central, production in Canada West, the performance environment, and the non-production environments are separate accounts. Foundational accounts carry security tooling and log archive, network and shared connectivity, and shared platform services such as the container registry and the signing keys. Organization-wide service control policies enforce guardrails, including a policy that denies any region outside Canada Central and Canada West so that residency is structural rather than procedural.

Alternatives considered

Namespace-only separation in shared clusters. Rejected because a control-plane or account-level fault, a noisy neighbor, or a compliance boundary cannot be contained by namespaces alone, and because it complicates Payment Card Industry scope.
Cluster-per-environment inside one shared account. Rejected because a shared account shares identity, service quotas, and a billing and policy boundary, which weakens isolation and residency enforcement.

Consequences

Account sprawl is managed with automation and a consistent baseline applied to every account. Cross-account access is brokered with least privilege and short-lived sessions. The account topology is shown below.

1 Management account AWS Organizations, consolidated billing, service control policies Security OU Log Archive audit Immutable, centralized log retention Organization CloudTrail and config Security Tooling deleg admin GuardDuty and Security Hub delegated admin SIEM, detective controls, response Infrastructure OU Network shared net Cloud WAN core, Transit Gateway Direct Connect, DNS, shared ingress Shared Services platform ECR, cosign and KMS signing keys Platform tooling, golden pipelines Workloads OU Non-prod sub-OU dev ca-central-1 EKS dev cluster relaxed quotas EKS cluster + external data tier test ca-central-1 EKS test cluster integration suites EKS cluster + external data tier Perf sub-OU perf - central ca-central-1 EKS perf cluster load and soak EKS cluster + external data tier perf - west ca-west-1 EKS perf cluster cross-region perf EKS cluster + external data tier Prod sub-OU prod - central ca-central-1 EKS prod, Tier-0 writer CDE, full controls EKS cluster + external data tier prod - west ca-west-1 EKS prod, Tier-0 writer CDE, full controls EKS cluster + external data tier Organization-wide guardrails Region restriction SCP denies any region outside ca-central-1 and ca-west-1. Residency at the boundary. Preventive SCPs Deny root usage, deny disabling logging or GuardDuty, deny public S3 and load balancers. Delegated administration GuardDuty, Security Hub, IAM Identity Center and config aggregated into Security accounts.
Figure 4. The landing zone. Foundational security and network accounts sit beside a workloads organizational unit that holds a separate account for each environment and region, all under organization-wide guardrails.
ADR-07

Tenancy and namespace isolation within clusters

Accepted

Context

Within a production cluster, several product domains run side by side. They must be isolated from one another, and the cardholder data environment must be isolated most strictly of all.

Decision

Isolate product domains by namespace, with a default-deny network policy, per-namespace resource quotas and limit ranges, and pod security standards enforced at admission. The cardholder data environment runs on dedicated nodes, separated from non-cardholder workloads, so that the most sensitive components do not share a host with anything out of scope. Service-to-service authorization is enforced in the mesh in addition to network policy, so that identity and not only network reachability governs access.

Alternatives considered

One namespace per product across a flat network. Rejected because without default-deny and dedicated nodes the cardholder scope would expand and lateral movement would be easier.

Consequences

Scheduling is constrained by node pools and taints, which is acceptable and is handled by the compute strategy in ADR-08.

ADR-08

Compute strategy

Accepted

Context

The platform needs fast, cost-aware scaling for bursty payment traffic, hardened nodes for a regulated environment, and the ability to run host-level security agents. No graphics processing units are required.

Decision

Use a mixed model. A managed baseline carries the steady-state tier-zero workloads on reserved capacity for predictable cost and performance, on AWS Graviton instances for price and energy efficiency. Self-managed Karpenter provisions elastic capacity for tier-one and bursty workloads, mixing on-demand and Spot capacity, again favoring Graviton. Self-managed Karpenter is chosen over fully managed Auto Mode for the regulated fleet because it allows custom machine images and host-level agents that the security baseline requires, while Auto Mode remains a candidate for lower-sensitivity clusters. Nodes run a hardened, minimal operating system from signed images. Short-lived batch jobs run on AWS Fargate for additional isolation. Karpenter brings new nodes online in roughly forty-five to sixty seconds, which keeps headroom small without starving bursts.

Alternatives considered

Fully managed Auto Mode everywhere. Rejected for the cardholder fleet because it does not support custom machine images, so host-level scanning agents would have to run only as in-cluster workloads, which is weaker for this scope. It remains attractive for non-sensitive clusters.
Static managed node groups only. Rejected because fixed groups over-provision and scale slowly compared with just-in-time provisioning.

Consequences

The platform runs two provisioning models and must keep their machine images and policies consistent. The single-region cluster detail is shown below.

1 Amazon EKS managed control plane HA API server across three AZs, private API endpoint only. Control-plane audit logs to CloudWatch and SIEM. Amazon VPC 10.20.0.0/16 - private subnets only, no IGW Availability Zone 1 - ca-central-1a Availability Zone 2 - ca-central-1b Availability Zone 3 - ca-central-1c 2 EKS data plane - node pools and workloads system managed node group, on-demand tier0-app on-demand Graviton, reserved tier1-app Karpenter Graviton, Spot batch Fargate, ephemeral isolation Worker nodes (Bottlerocket, signed AMI) pay-auth settle payments msg platform Worker nodes (Bottlerocket, signed AMI) pay-auth settle payments msg platform Worker nodes (Bottlerocket, signed AMI) pay-auth settle payments msg platform 3 In-cluster platform and policy (GitOps managed) ArgoCD agent Istio ambient OTel +Prometheus ExternalSecrets cert-manager Kyvernoadmission FalcoKarpenter 4 External managed data tier - private data subnets Aurora PostgreSQL writer (Tier-0), Multi-AZ sync Aurora reader + Global DB Tier-1 Aurora reader standby promotion target MSK brokerTLS, quorum ElastiCacheRedis node MSK brokerTLS, quorum ElastiCacheRedis node MSK brokerTLS, quorum ElastiCacheRedis node VPC interface endpoints (PrivateLink): ECR - STS - Secrets Manager - KMS - CloudWatch - S3 gateway - EKS - Aurora - MSK Egress via AWS Network Firewall (allow-list only) Cross-cutting identity & security 5 Workload identity EKS Pod Identity for pod to AWS No long-lived IAM keys on nodes 6 Secrets and keys AWS Secrets Manager via External KMS envelope encryption, per-tenant 7 Supply chain cosign with KMS ECC_NIST_P256 Kyverno verifies at admission 8 Network policy Default-deny, namespace micro-seg Istio authz policies per service 9 Detection GuardDuty for EKS, runtime Falco Inspector image and host CVE scan 10 Tenancy Namespace isolation per domain ResourceQuota, LimitRange, PodSec
Figure 5. A single region in detail. A private cluster across three Availability Zones, mixed node pools, in-cluster platform add-ons, an external managed data tier in private subnets, and cross-cutting identity and security controls.
ADR-09

Networking and connectivity

Accepted

Context

The platform is private only, spans two regions, and integrates with an on-premises data center. It must route between regions and to on-premises without any public exposure, and it must control egress tightly.

Decision

Join the regions with AWS Cloud WAN as the core network, segmented so that production and non-production never share a segment. Connect both regions to the on-premises data center with redundant AWS Direct Connect links from separate locations. Expose services through internal load balancers and the Kubernetes Gateway interface with AWS PrivateLink, with no public load balancers. Reach AWS services over virtual private cloud interface endpoints. Control all egress through AWS Network Firewall with a strict domain allow-list, so that no workload subnet has general internet access. Log all flows.

Alternatives considered

Public load balancers with a web application firewall. Rejected because the posture is private only, so there is no public ingress to protect in the first place.
Open egress with monitoring. Rejected because a regulated payments environment requires egress to be denied by default and allowed by exception.

Consequences

New external dependencies require an explicit allow-list change, which is a small operational cost that buys a strong containment guarantee.

ADR-10

Service mesh

Accepted

Context

The platform needs mutual authentication between services, fine-grained authorization, and traffic control for progressive delivery, without imposing a heavy sidecar tax on every pod.

Decision

Adopt Istio in ambient mode with mutual Transport Layer Security (mTLS) set to strict, so that every service-to-service call is authenticated and encrypted by identity. Authorization policies govern which service may call which, in addition to network policy. The mesh provides the traffic shifting used by progressive delivery in ADR-13.

Alternatives considered

Sidecar-per-pod mesh. Rejected as the default because of its per-pod resource overhead at high pod counts, although it remains available for workloads that need per-pod Layer 7 features.
No mesh, network policy only. Rejected because network reachability is not identity, and the platform requires identity-based authorization and encryption in transit between services.

Consequences

Mesh identity becomes a control that auditors can rely on. Operating the mesh is a platform responsibility owned by the central team.

ADR-11

Security posture and PCI DSS alignment

Accepted

Context

The platform handles card data and must align with the Payment Card Industry Data Security Standard while keeping the scope as small as practical.

Decision

Apply defense in depth across trust zones, from the untrusted partner edge through the ingress and application zones to the cardholder data environment, which is the innermost zone on dedicated nodes. Tokenize the primary account number at the boundary so that clear card data does not propagate inward. Encrypt data at rest with customer-managed keys and in transit with mutual Transport Layer Security inside the mesh and Transport Layer Security at the edge. Establish workload identity with EKS Pod Identity so that there are no standing credentials on nodes. Verify image signatures at admission. Detect at runtime with Amazon GuardDuty for the cluster and a runtime sensor, and scan images and hosts with Amazon Inspector. Provide node access only through AWS Systems Manager, with no secure shell and no bastion. Break-glass access is time-boxed and logged.

Alternatives considered

Treat the whole platform as in scope. Rejected because a larger cardholder scope increases audit burden and risk. Tokenization and zoning shrink the scope to the innermost components.

Consequences

The zoning and the tokenization boundary are load-bearing controls that must be verified continuously. The zone model and its mapping to the standard are shown below.

1 Untrusted zone - partner banks, card switch, internal channels No direct internet exposure. All inbound arrives over AWS Direct Connect private virtual interfaces. PERIMETER BOUNDARY Route 53 | AWS WAF | Shield Advanced | Network Firewall | DX private VIF 2 Edge and ingress zone - out of PCI data scope Internal Network Load Balancer with PrivateLink. Gateway API and WAF inspection. mTLS terminates at the mesh edge. SEGMENTATION BOUNDARY Security groups | network ACLs | mesh ingress authorization | rate limiting 3 Application zone - connected-to PCI scope Non-cardholder services and orchestration. Mesh enforces mTLS STRICT and per-service authz. East-west default-deny. Messaging Notifications Fraud scoring API orchestration Routing / cell map CDE BOUNDARY (strict) default-deny NetworkPolicy | Istio authz | namespace isolation | dedicated nodes CARDHOLDER DATA ENVIRONMENT - PCI DSS in scope Paymentauthorizationapp, in-scope Settlementengineapp, in-scope Tokenization /vaultPAN to token Tier-0 ledger(Aurora)encrypted at rest MSK PAN topicsencrypted, TLS Key management(KMS)envelope keys Data protection inside the CDE: PANs tokenized at the boundary. Stored data encrypted with KMS customer-managed keys. All transmission over TLS or mesh mTLS. Access brokered by EKS Pod Identity, no standing credentials. 5 Management zone - out of data scope GitOps reconciliation (ArgoCD) and platform control. Node access via AWS Systems Manager only, no SSH, no bastion. Observability and SIEM collection. Break-glass logged and time-boxed. 6 Egress zone - controlled and logged AWS Network Firewall with a strict domain allow-list. Outbound limited to named AWS endpoints and partner ranges. No general internet egress. All flows logged to VPC Flow Logs and SIEM. PCI DSS control mapping Req 1 Network segmentation by zone, default-deny, CDE isolated to nodes. Req 2 Hardened Bottlerocket nodes, no defaults, immutable signed images. Req 3 Stored PAN tokenized, KMS customer-managed keys, no clear PAN. Req 4 TLS in transit externally, mesh mTLS STRICT internally. Req 6 Signed images, Kyverno admission, SAST and SCA in the pipeline. Req 7/8 Least privilege, EKS Pod Identity, no standing human credentials. Req 10 Central logging, control-plane audit, immutable SIEM retention. Req 11 Continuous scanning, Inspector, fault injection and DR tests.
Figure 6. Defense in depth from the untrusted edge to the cardholder data environment, with the Payment Card Industry scope minimized to the innermost zone and a control mapping to the standard.
ADR-12

Secrets and key management

Accepted

Context

The platform needs managed secrets and managed encryption keys, with no secrets baked into images or held as long-lived credentials.

Decision

Store secrets in AWS Secrets Manager and synchronize them into the cluster with the External Secrets operator, so that secrets are never committed to Git. Encrypt data with AWS Key Management Service (KMS) using customer-managed keys, with per-tenant keys where isolation requires it. Sign container images with cosign using a key held in AWS Key Management Service on the elliptic-curve type, and verify those signatures at admission so that only signed and approved images run.

Alternatives considered

In-cluster secret stores as the system of record. Rejected in favor of a managed service with audit, rotation, and a clear ownership boundary.

Consequences

Key and secret access is governed by identity and is fully audited. Rotation is a managed operation rather than a manual one.

ADR-13

Continuous integration, delivery, and GitOps

Accepted

Context

Delivery must be auditable, must prevent unsigned or unscanned code from reaching production, and must promote changes region by region with the ability to roll back automatically.

Decision

Build and test in GitHub Actions, authenticating to AWS with OpenID Connect so that no static cloud keys exist in the pipeline. Build images with a rootless builder, run unit and contract tests, static application security testing, and software composition analysis, produce a software bill of materials, scan with Amazon Inspector, then sign with cosign and push to the registry. Deliver with ArgoCD in a pull-based GitOps model, where a change to a configuration repository updates the image digest and ArgoCD reconciles it. Verify the image signature, provenance, and policy at admission. Use progressive delivery with canary analysis and mesh traffic shifting. Promote in waves across separate accounts, from non-production to performance to production in Canada Central and then to production in Canada West, with bake time and health gates between waves. A failed gate halts the wave, reverts the change in Git, and reconciles the previous signed digest.

Alternatives considered

Push-based deployment from the pipeline into clusters. Rejected in favor of pull-based GitOps, which gives drift detection, a single source of truth in Git, and a cleaner audit trail.
Promote all regions at once. Rejected because a bad change would reach both regions together, defeating the purpose of regional isolation.

Consequences

Every production change is traceable to a signed commit, a signed image, and an approved Git change. The delivery flow is shown below.

Continuous integration - GitHub Actions, OIDC to AWS, no static keys 1 Commit and PR Trunk-based, signed commits Branch protection, reviews 2 Build - BuildKit Rootless OCI build Reproducible, pinned bases 3 Test and scan Unit and contract tests SAST and SCA gates 4 SBOM and CVE CycloneDX SBOM Amazon Inspector scan 5 Sign and push cosign with KMS ECC_NIST_P256, push ECR image digest + attestations Continuous delivery - GitOps with ArgoCD, per environment and region 6 Config repo update Digest bump via PR Approval, env overlays 7 ArgoCD reconcile Pull-based GitOps Drift detect, self-heal 8 Admission verify Kyverno checks cosign sig Provenance and SBOM policy 9 Progressive delivery Argo Rollouts canary Mesh shift, metric analysis Multi-region promotion waves - separate AWS account per environment and region A Non-prod dev and test accounts functional gates B Performance perf account load, soak, SLO check C Prod - ca-central-1 canary then full bake, metric analysis D Prod - ca-west-1 wave 2 after bake health-gated promotion perf wave 1 wave 2 Automated rollback: Any failed health or SLO gate halts the wave, reverts the Git change, and ArgoCD reconciles the previous signed digest. A region is never promoted while the prior wave is unhealthy. Rollout history and approvals stay in Git for audit.
Figure 7. Continuous integration produces signed, attested images. ArgoCD reconciles from Git, admission control verifies provenance, and promotion advances region by region with automated rollback.
ADR-14

Observability and service level objectives

Accepted

Context

A 99.999 percent target leaves a very small error budget, so the platform must detect and respond to problems quickly and must govern change against the budget.

Decision

Collect metrics with Prometheus and aggregate across regions with Thanos for a global view. Instrument services with OpenTelemetry for distributed tracing. Visualize with Grafana. Ship security-relevant logs to a central security information and event management system with immutable retention. Define service level objectives per product, track an error budget, and tie the budget to change policy so that releases slow when the budget is spent. Alert on symptoms that matter to customers rather than on raw resource metrics alone.

Alternatives considered

Per-region dashboards with no global aggregation. Rejected because an active-active platform needs a single cross-region view to reason about health and failover.

Consequences

The error budget becomes a shared currency between reliability and delivery, which keeps the 99.999 percent target honest rather than aspirational.

9. Failure modes and runbooks

The table records the failure scenarios the platform is designed to survive, how each is detected, the response, and the resulting recovery point and recovery time for the core tier.

ScenarioDetectionResponseRPORTO
Single node or pod lossLiveness and readiness probes, schedulerReschedule on healthy nodes, Karpenter provisions replacementsZeroSeconds
Availability Zone lossHealth checks, replica heartbeatsTraffic shifts to remaining zones, Multi-AZ replica serves, no cross-region actionZeroSeconds
Full region lossRoute 53 health checks, ledger heartbeatsSurviving region adopts the failed cells by promoting the synchronous standby, Route 53 withdraws the regionZeroSeconds to a minute
Cross-region partitionReplication and witness signalsWitness adjudicates ownership per cell, the losing side pauses writes on contested cellsZeroWrite pause on contested cells until heal
Partner standby unreachableSynchronous replication timeoutLocal Availability Zone replica keeps the quorum, alert raised, policy in ADR-05 appliesZeroNone for local faults
Witness lossWitness heartbeatRedundant witness instance takes over, no write impact while a region is healthyZeroNone
Bad releaseCanary analysis, service level objective gatesWave halts, Git change reverts, ArgoCD reconciles the previous signed digestZeroMinutes, contained to one wave

9.1 Runbook, region loss and cell adoption

  1. Route 53 health checks mark the failed region unhealthy and stop sending it traffic.
  2. The cell router marks the failed region cells for adoption and points them at the surviving region.
  3. The surviving region promotes its synchronous standby for those cells to writer. Because the standby holds every committed transaction, no data is lost.
  4. The surviving region now serves the full keyspace. Capacity scales up under Karpenter to absorb the additional load.
  5. When the failed region returns, it is rebuilt from the survivor, resynchronized, and cells are rebalanced back during a low-traffic window.

9.2 Runbook, cross-region partition

  1. Replication stalls and the witness observes that the two regions cannot see each other.
  2. The witness adjudicates ownership of each contested cell so that only one side keeps writing it.
  3. The side that loses adjudication pauses writes on those cells and continues to serve reads and the cells it still owns.
  4. When the partition heals, the paused cells resynchronize and writing resumes. No committed transaction is lost on either side.

10. Capacity and cost optimization

An always-on, active-active platform across two regions costs more than a single-region system, and the design makes that cost deliberate rather than accidental. The optimization tower below moves from the largest levers to the smallest.

LeverApproachEffect
Processor choiceAWS Graviton across baseline and elastic capacityLower price and energy per unit of work than comparable instances
Purchase modelReserved capacity or savings plans for steady-state tier zero, on-demand for the restDiscounts the predictable baseline without locking in burst
Spot for burstSpot capacity for tier-one and stateless burst under KarpenterLarge savings on interruptible, horizontally scalable work
Just-in-time scalingKarpenter provisions in roughly forty-five to sixty seconds and consolidates idle nodesSmall headroom, less idle capacity, automatic bin-packing
Right-sizingContinuous review of pod requests against real usagePrevents over-provisioning that no autoscaler can correct
Region-loss capacityEach region runs lean and relies on rapid scale-up on failover rather than carrying a full idle doubleAvoids paying for a permanently idle second copy while still meeting the always-on objective

The cost of resilience is a conscious choice

The two expensive properties are the active-active footprint and the synchronous cross-region commit. The footprint is held down by running each region lean and scaling on failover. The commit cost is held down by confining the cross-region round trip to the commit of mutating transactions, so reads and execution stay local and the premium is paid once per write rather than per statement.

11. Compliance mapping

The platform is designed to align with the Payment Card Industry Data Security Standard and with Canadian regulatory expectations for technology, cyber risk, and operational resilience. The table summarizes how the design supports the standard. The mapping is an architecture aid and not an attestation, which depends on a formal assessment.

Requirement areaHow the architecture supports it
Network segmentationTrust zones, default-deny network policy, a cardholder data environment isolated to dedicated nodes, and a minimized scope
Secure configurationHardened minimal nodes from signed images, no defaults, immutable infrastructure
Protect stored dataPrimary account numbers tokenized at the boundary, encryption with customer-managed keys, no clear card data at rest
Protect data in transitTransport Layer Security at the edge and mutual Transport Layer Security inside the mesh
Secure developmentSigned images, admission verification, static and composition analysis in the pipeline
Access controlLeast privilege, workload identity without standing credentials, central human identity with short sessions
Logging and monitoringCentral logging, control-plane audit, immutable retention in the security information and event management system
TestingContinuous scanning, vulnerability assessment, and scheduled fault injection and recovery tests

On the regulatory side, the design supports the expectations set out by the Office of the Superintendent of Financial Institutions for technology and cyber risk and for operational resilience. Strict in-Canada data residency, a defined and tested recovery posture, third-party and concentration awareness through the multi-account model, and continuous monitoring all map to those expectations. The relevant guidance is listed in the references.

12. Risks, assumptions, and open items

  • Commit latency must be validated against the real budget. The synchronous cross-region commit adds the inter-region round trip on writes. The exact figure between Montreal and Calgary must be measured under load and confirmed to fit the authorization budget before launch.
  • Operating synchronous replication is a real commitment. Running the tier-zero ledger with synchronous cross-region replication requires mature operations, tested promotion, and careful handling of the standby-unreachable case. The runbooks address this and must be exercised regularly.
  • The witness is a critical dependency. It is made redundant inside the data center, holds only journal metadata, and must be monitored as a tier-zero component.
  • Cell rebalancing needs care. Moving cells between regions after a recovery must be done without violating the single-writer rule, on a controlled schedule.
  • Revisit if managed options change. If Amazon Aurora DSQL later offers a Canadian multi-region set with an in-country quorum, the tier-zero decision in ADR-02 should be re-evaluated, since a managed synchronous option would reduce the operational burden.
  • Throughput ceilings. Per-cell write throughput has limits. The cell count and the hash function must be sized for peak volume with headroom, and the cell map must support growth.

13. References

The decisions that depend on current service behavior were checked against the following sources. They are recorded in monospace for verification.

[1] Amazon Aurora DSQL, overview and multi-Region clusters - docs.aws.amazon.com/aurora-dsql/latest/userguide/what-is-aurora-dsql.html
[2] Amazon Aurora DSQL, Region availability - docs.aws.amazon.com/aurora-dsql/latest/userguide/region-availability.html
[3] Aurora DSQL now in additional Regions, Canada single-Region - aws.amazon.com/about-aws/whats-new/2026/02/amazon-aurora-dsql-additional-aws-regions/
[4] Resilience in Amazon Aurora DSQL, synchronous multi-Region - docs.aws.amazon.com/aurora-dsql/latest/userguide/disaster-recovery-resiliency.html
[5] Aurora DSQL for global-scale financial transactions - aws.amazon.com/blogs/database/amazon-aurora-dsql-for-global-scale-financial-transactions/
[6] Amazon Aurora Global Database, asynchronous replication - docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/aurora-global-database.html
[7] Amazon EKS Auto Mode, best practices - docs.aws.amazon.com/eks/latest/best-practices/automode.html
[8] Karpenter, Amazon EKS best practices - docs.aws.amazon.com/eks/latest/best-practices/karpenter.html
[9] PCI DSS, Payment Card Industry Security Standards Council - www.pcisecuritystandards.org
[10] OSFI guidance on technology and cyber risk and operational resilience - www.osfi-bsif.gc.ca

Part II, Detailed design and configuration

1. Purpose and conventions

This document turns the reference architecture into buildable configuration. It assumes the decisions recorded there and does not repeat their rationale. Snippets are shown in Terraform for AWS resources and in Kubernetes manifests for in-cluster objects, with GitHub Actions and ArgoCD for delivery. Values such as account identifiers, virtual private cloud identifiers, and Amazon Resource Names are shown as placeholders in the form REPLACE_ME or as Terraform references, since they are environment specific.

Implementation refinement to decision record ADR-02

The reference architecture named Amazon Aurora PostgreSQL for the tier-zero ledger as shorthand. At the implementation level that is corrected, because Aurora cannot meet a literal zero recovery point objective across regions. Aurora replicates across regions only through Amazon Aurora Global Database, which is asynchronous. A true zero recovery point objective with PostgreSQL semantics requires synchronous streaming replication with synchronous_commit = remote_apply and a synchronous standby in the partner region, which is a self-managed capability. The tier-zero ledger is therefore self-managed PostgreSQL orchestrated by Patroni, detailed in section 7. The witness from ADR-05 is realized as the third member of the Patroni distributed configuration store, placed on premises in Canada. Aurora remains the right choice for the tier-one products and is detailed in section 8.

Naming and tagging

Every resource carries a consistent set of tags so that cost, ownership, residency, and data classification are queryable. The standard tag set is applied through Terraform default tags and enforced by an organization tag policy.

terraform, provider default tagshcl
provider "aws" {
  region = var.region            # ca-central-1 or ca-west-1
  default_tags {
    tags = {
      Platform           = "payments"
      Environment        = var.environment    # prod | perf | test | dev
      Region             = var.region
      DataClassification = var.data_class      # restricted | confidential | internal
      Residency          = "canada-only"
      Owner              = "payments-platform"
      ManagedBy          = "terraform"
      CostCenter         = var.cost_center
    }
  }
}

# Naming convention: pay-{env}-{region-short}-{component}
# Example: pay-prod-cac1-ledger,  pay-prod-caw1-eks
locals {
  region_short = { "ca-central-1" = "cac1", "ca-west-1" = "caw1" }
  name_prefix  = "pay-${var.environment}-${local.region_short[var.region]}"
}

2. Organization and account baseline

The landing zone is built on AWS Organizations with a dedicated account per environment and region. Each account receives the same baseline through a shared Terraform module, so that logging, detection, and guardrails are identical everywhere.

AccountOrganizational unitPurpose
managementrootAWS Organizations, consolidated billing, service control policies
log-archiveSecurityImmutable organization CloudTrail and AWS Config delivery, object lock
security-toolingSecurityGuardDuty, Security Hub, Amazon Inspector, and Detective delegated administrator
networkInfrastructureAWS Cloud WAN, Direct Connect gateway, central resolver, shared ingress
shared-servicesInfrastructureAmazon ECR, cosign signing keys, platform tooling, golden pipelines
prod-cac1, prod-caw1Workloads / ProdProduction clusters and data tier, one account per region
perf-cac1, perf-caw1Workloads / PerfPerformance and soak environment, per region
dev, testWorkloads / Non-prodDevelopment and integration

Region restriction service control policy

This policy denies every action outside the two Canadian regions, with a narrow exception for global services that have no regional endpoint. It is attached to the Workloads organizational unit so that residency is structural.

service control policy, deny-non-canada-regions.jsonjson
{
  "Version": "2012-10-17",
  "Statement": [{
    "Sid": "DenyOutsideCanada",
    "Effect": "Deny",
    "NotAction": [
      "iam:*", "organizations:*", "route53:*", "cloudfront:*",
      "waf:*", "wafv2:*", "support:*", "sts:*", "budgets:*"
    ],
    "Resource": "*",
    "Condition": {
      "StringNotEquals": {
        "aws:RequestedRegion": ["ca-central-1", "ca-west-1"]
      }
    }
  }]
}

Preventive guardrails service control policy

service control policy, preventive-guardrails.jsonjson
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyRootUser",
      "Effect": "Deny",
      "Action": "*",
      "Resource": "*",
      "Condition": { "StringLike": { "aws:PrincipalArn": "arn:aws:iam::*:root" } }
    },
    {
      "Sid": "ProtectSecurityControls",
      "Effect": "Deny",
      "Action": [
        "guardduty:DeleteDetector", "guardduty:DisassociateFromMasterAccount",
        "cloudtrail:StopLogging", "cloudtrail:DeleteTrail",
        "config:DeleteConfigurationRecorder", "config:StopConfigurationRecorder",
        "securityhub:DisableSecurityHub"
      ],
      "Resource": "*"
    },
    {
      "Sid": "NoPublicS3",
      "Effect": "Deny",
      "Action": "s3:PutBucketPublicAccessBlock",
      "Resource": "*",
      "Condition": { "Bool": { "s3:PublicAccessBlockConfiguration": "false" } }
    },
    {
      "Sid": "RequireImdsV2",
      "Effect": "Deny",
      "Action": "ec2:RunInstances",
      "Resource": "arn:aws:ec2:*:*:instance/*",
      "Condition": { "StringNotEquals": { "ec2:MetadataHttpTokens": "required" } }
    }
  ]
}

Account baseline

The baseline module enables the organization trail and config aggregation, registers the account with the delegated security administrator, and turns on detection. Delegated administration is configured once from the management account.

terraform, delegated administration and detectionhcl
resource "aws_guardduty_organization_admin_account" "this" {
  admin_account_id = var.security_tooling_account_id
}

resource "aws_securityhub_organization_admin_account" "this" {
  admin_account_id = var.security_tooling_account_id
}

# Per-account baseline (applied by stack set to every workload account)
resource "aws_guardduty_detector" "this" {
  enable = true
  datasources {
    kubernetes { audit_logs { enable = true } }
    malware_protection { scan_ec2_instance_with_findings { ebs_volumes { enable = true } } }
  }
}

resource "aws_inspector2_enabler" "this" {
  account_ids    = [data.aws_caller_identity.current.account_id]
  resource_types = ["ECR", "EC2", "LAMBDA"]
}

3. Network configuration

3.1 Address plan

Every virtual private cloud uses a non-overlapping block so that Cloud WAN can route between them without translation. Each environment and region gets its own block.

AccountRegionVPC CIDRNotes
prod-cac1ca-central-110.20.0.0/16Production, Central
prod-caw1ca-west-110.21.0.0/16Production, West
perf-cac1ca-central-110.30.0.0/16Performance, Central
perf-caw1ca-west-110.31.0.0/16Performance, West
testca-central-110.40.0.0/16Integration
devca-central-110.41.0.0/16Development

Within a production virtual private cloud the address space is divided into four subnet tiers, each spread across three Availability Zones. The application tier is sized large because the container networking plugin uses prefix delegation for high pod density.

TierAZ aAZ bAZ cUse
Ingress10.20.0.0/2410.20.1.0/2410.20.2.0/24Internal load balancer interfaces
Data10.20.4.0/2410.20.5.0/2410.20.6.0/24PostgreSQL, MSK, Redis
Endpoints10.20.8.0/2410.20.9.0/2410.20.10.0/24VPC interface endpoints
Application10.20.16.0/2010.20.32.0/2010.20.48.0/20EKS nodes and pods
Amazon VPC pay-prod-cac1 10.20.0.0/16 - no internet gateway Availability Zone a Availability Zone b Availability Zone c Ingress subnets internal NLB interfaces 10.20.0.0/24 10.20.1.0/24 10.20.2.0/24 Application subnets EKS nodes and pods, prefix delegation 10.20.16.0/20 10.20.32.0/20 10.20.48.0/20 Data subnets PostgreSQL, MSK, Redis 10.20.4.0/24 10.20.5.0/24 10.20.6.0/24 Endpoint subnets VPC interface endpoints 10.20.8.0/24 10.20.9.0/24 10.20.10.0/24 Firewall subnets AWS Network Firewall endpoints 10.20.12.0/28 10.20.12.16/28 10.20.12.32/28 Routing and egress AWS services Reached through interface endpoints in the endpoint subnets, private DNS on Amazon S3 Gateway endpoint on the private route tables, no hourly cost Controlled egress Default route 0.0.0.0/0 sends traffic to the Network Firewall endpoints Domain allow-list, default-deny Cross-region Partner region and on-premises reached over Cloud WAN and Direct Connect No public path No internet gateway, no NAT gateway No public load balancers anywhere Subnet sizing Application tier is a /20 per zone for high pod density under prefix delegation Other tiers are /24, firewall is /28
Figure 8. Network address and subnet plan. One non-overlapping block per environment and region, with four subnet tiers across three Availability Zones and egress through AWS Network Firewall.

3.2 Cloud WAN core network

The core network policy defines two segments, production and non-production, that do not share routes. Each virtual private cloud attaches to the segment that matches its environment tag, so attachment is automatic and policy driven.

terraform, cloud wan core network policyhcl
data "aws_networkmanager_core_network_policy_document" "this" {
  core_network_configuration {
    asn_ranges = ["64512-64555"]
    edge_locations { location = "ca-central-1" }
    edge_locations { location = "ca-west-1" }
  }

  segments { name = "production"    isolate_attachments = false require_attachment_acceptance = true }
  segments { name = "nonproduction" isolate_attachments = false require_attachment_acceptance = true }

  segment_actions {
    action  = "share"
    segment = "production"
    share_with = ["production"]   # production talks only to production
  }

  attachment_policies {
    rule_number     = 100
    condition_logic = "and"
    conditions { type = "tag-value" key = "Environment" operator = "equals" value = "prod" }
    action { association_method = "constant" segment = "production" }
  }
  attachment_policies {
    rule_number     = 200
    condition_logic = "or"
    conditions { type = "tag-value" key = "Environment" operator = "equals" value = "dev" }
    conditions { type = "tag-value" key = "Environment" operator = "equals" value = "test" }
    action { association_method = "constant" segment = "nonproduction" }
  }
}

3.3 Direct Connect

Two dedicated connections from separate Direct Connect locations terminate on a Direct Connect gateway, which associates with the Cloud WAN core network so that on-premises reaches both regions over private transit. Bidirectional Forwarding Detection speeds failure detection on the border gateway protocol sessions.

terraform, direct connect transit and gateway associationhcl
resource "aws_dx_gateway" "core" {
  name            = "pay-dxgw"
  amazon_side_asn = 64600
}

# One transit virtual interface per dedicated connection (two for redundancy)
resource "aws_dx_transit_virtual_interface" "vif" {
  for_each       = toset(["conn-a", "conn-b"])
  connection_id  = var.dx_connection_ids[each.key]
  dx_gateway_id  = aws_dx_gateway.core.id
  name           = "pay-tvif-${each.key}"
  vlan           = var.dx_vlans[each.key]
  address_family = "ipv4"
  bgp_asn        = 65000
  mtu            = 8500            # jumbo frames for replication throughput
}

# Associate the Direct Connect gateway with the Cloud WAN core network
resource "aws_networkmanager_dx_gateway_attachment" "this" {
  core_network_id            = var.core_network_id
  direct_connect_gateway_arn = aws_dx_gateway.core.arn
  edge_locations             = ["ca-central-1", "ca-west-1"]
}

3.4 Route 53 traffic management

A private hosted zone serves the platform domain over the AWS backbone. Each region publishes a latency record guarded by a health check, so a caller reaches the nearest healthy region and a failed region is withdrawn automatically.

terraform, latency records with health checkshcl
resource "aws_route53_health_check" "region" {
  for_each          = toset(["cac1", "caw1"])
  fqdn              = "ingress-${each.key}.pay.internal"
  type              = "HTTPS"
  resource_path     = "/healthz"
  port              = 443
  failure_threshold = 2
  request_interval  = 10           # fast detection, ~20 s to unhealthy
}

resource "aws_route53_record" "api" {
  for_each       = { cac1 = "ca-central-1", caw1 = "ca-west-1" }
  zone_id        = aws_route53_zone.private.zone_id
  name           = "api.pay.internal"
  type           = "A"
  set_identifier = each.key
  health_check_id = aws_route53_health_check.region[each.key].id
  latency_routing_policy { region = each.value }
  alias {
    name                   = var.nlb_dns[each.key]
    zone_id                = var.nlb_zone[each.key]
    evaluate_target_health = true
  }
}

3.5 VPC interface endpoints

Because there is no internet gateway, every AWS service is reached through an interface endpoint in the endpoints subnet tier. The set is created from a list so it stays consistent across accounts.

terraform, interface endpointshcl
locals {
  interface_endpoints = [
    "ecr.api", "ecr.dkr", "sts", "secretsmanager", "kms",
    "logs", "monitoring", "ssm", "ssmmessages", "ec2messages",
    "eks", "elasticloadbalancing", "kafka", "sts"
  ]
}

resource "aws_vpc_endpoint" "iface" {
  for_each            = toset(local.interface_endpoints)
  vpc_id             = var.vpc_id
  service_name       = "com.amazonaws.${var.region}.${each.key}"
  vpc_endpoint_type  = "Interface"
  subnet_ids         = var.endpoint_subnet_ids
  security_group_ids = [aws_security_group.endpoints.id]
  private_dns_enabled = true
}

# S3 reached through a gateway endpoint (no hourly cost, route-table based)
resource "aws_vpc_endpoint" "s3" {
  vpc_id            = var.vpc_id
  service_name      = "com.amazonaws.${var.region}.s3"
  vpc_endpoint_type = "Gateway"
  route_table_ids   = var.private_route_table_ids
}

3.6 Egress control with AWS Network Firewall

All outbound traffic from workload subnets routes through AWS Network Firewall. A stateful rule group permits only an explicit domain allow-list and a default-deny drops everything else. New external dependencies require an allow-list change, which is the intended friction.

terraform, stateful domain allow-listhcl
resource "aws_networkfirewall_rule_group" "egress_allow" {
  capacity = 256
  name     = "pay-egress-allow"
  type     = "STATEFUL"
  rule_group {
    rule_variables {
      ip_sets {
        key = "HOME_NET"
        ip_set { definition = ["10.20.0.0/16"] }
      }
    }
    rules_source {
      rules_source_list {
        generated_rules_type = "ALLOWLIST"
        target_types         = ["TLS_SNI", "HTTP_HOST"]
        targets = [
          ".ca-central-1.amazonaws.com",
          ".ca-west-1.amazonaws.com",
          ".pay-partner.example"          # named partner ranges only
        ]
      }
    }
    stateful_rule_options { rule_order = "STRICT_ORDER" }
  }
}

resource "aws_networkfirewall_firewall_policy" "this" {
  name = "pay-egress-policy"
  firewall_policy {
    stateless_default_actions          = ["aws:forward_to_sfe"]
    stateless_fragment_default_actions = ["aws:forward_to_sfe"]
    stateful_default_actions           = ["aws:drop_established", "aws:alert_established"]
    stateful_engine_options { rule_order = "STRICT_ORDER" }
    stateful_rule_group_reference { resource_arn = aws_networkfirewall_rule_group.egress_allow.arn priority = 100 }
  }
}

3.7 Security groups

Security groups reference each other by identifier rather than by address range, so access is expressed by role. The matrix summarizes the allowed paths.

FromToPortPurpose
nlbnodes30000-32767Ingress to node ports behind the mesh
nodesledger5432, 6432PostgreSQL and the connection pooler
nodesmsk9094Kafka over Transport Layer Security
nodesredis6379Cache with in-transit encryption
nodesendpoints443AWS service interface endpoints
ledgerledger5432Streaming replication between PostgreSQL nodes
terraform, ledger security grouphcl
resource "aws_security_group" "ledger" {
  name   = "${local.name_prefix}-ledger"
  vpc_id = var.vpc_id
}
resource "aws_vpc_security_group_ingress_rule" "ledger_from_nodes" {
  security_group_id            = aws_security_group.ledger.id
  referenced_security_group_id = aws_security_group.nodes.id
  from_port = 5432  to_port = 6432  ip_protocol = "tcp"
}
# Cross-region replication peer reached over Cloud WAN, restricted to the partner ledger CIDR
resource "aws_vpc_security_group_ingress_rule" "ledger_replication" {
  security_group_id = aws_security_group.ledger.id
  cidr_ipv4         = var.partner_region_data_cidr   # e.g. 10.21.4.0/22
  from_port = 5432  to_port = 5432  ip_protocol = "tcp"
}
1 Amazon Route 53 - private hosted zone Latency records guarded by HTTPS health checks api.pay.internal, ~20 s to unhealthy, automatic withdrawal VPC pay-prod-cac1 10.20.0.0/16 Internal NLB - ingress.cac1.pay.internal Cloud WAN attachment, Environment=prod Network Firewall egress endpoints default route to inspection, allow-list AWS Cloud WAN core network core ASN 64512-64555, edges in both regions production segment shares routes with production only nonproduction segment isolated, no route to production VPC pay-prod-caw1 10.21.0.0/16 Internal NLB - ingress.caw1.pay.internal Cloud WAN attachment, Environment=prod Network Firewall egress endpoints default route to inspection, allow-list attach On-premises data center - Canada redundant Direct Connect 2 AWS Direct Connect (two links) Two dedicated connections from separate Direct Connect locations Transit virtual interfaces, BGP ASN 65000 Bidirectional Forwarding Detection on Jumbo frames, MTU 8500 for replication 3 Direct Connect gateway Amazon side ASN 64600 Associated to the Cloud WAN core network Advertises both region edges Private transit only, no public VIF On-premises reaches both regions Systems of record Core banking and ledger integration Settlement and reconciliation exchange Tier-0 etcd witness member lives here Reached over private connectivity only No workload state stored on premises Direct Connect DXGW association to Cloud WAN Direct Connect Cloud WAN production segment Traffic and routing path Private connectivity
Figure 9. Connectivity. Production and non-production ride isolated Cloud WAN segments, redundant Direct Connect joins on-premises to both regions, and Route 53 steers each caller to the nearest healthy region.

4. EKS cluster configuration

Each production region runs one private cluster. The API endpoint is private only, secrets are encrypted with a customer-managed key, all control-plane log types are shipped, and access is governed by the access-entry model rather than the legacy authentication config map.

terraform, eks clusterhcl
resource "aws_eks_cluster" "this" {
  name     = "${local.name_prefix}-eks"
  role_arn = aws_iam_role.cluster.arn
  version  = "1.33"

  vpc_config {
    subnet_ids              = var.app_subnet_ids
    endpoint_private_access = true
    endpoint_public_access  = false                  # private only
    security_group_ids      = [aws_security_group.cluster.id]
  }

  access_config {
    authentication_mode                         = "API"
    bootstrap_cluster_creator_admin_permissions = false
  }

  encryption_config {
    provider { key_arn = aws_kms_key.eks_secrets.arn }
    resources = ["secrets"]                          # envelope-encrypt etcd secrets
  }

  kubernetes_network_config {
    ip_family         = "ipv4"
    service_ipv4_cidr = "172.20.0.0/16"
  }

  enabled_cluster_log_types = [
    "api", "audit", "authenticator", "controllerManager", "scheduler"
  ]
}

# Access by role, least privilege, no shared admin
resource "aws_eks_access_entry" "platform" {
  cluster_name  = aws_eks_cluster.this.name
  principal_arn = aws_iam_role.platform_admin.arn
  type          = "STANDARD"
}
resource "aws_eks_access_policy_association" "platform" {
  cluster_name  = aws_eks_cluster.this.name
  principal_arn = aws_iam_role.platform_admin.arn
  policy_arn    = "arn:aws:eks::aws:cluster-access-policy/AmazonEKSClusterAdminPolicy"
  access_scope { type = "cluster" }
}

Core add-ons

The container networking plugin runs with prefix delegation so that each node carries many more pod addresses, which is what allows the application subnets to be sized as they are. Add-ons are pinned to specific versions and updated through the pipeline.

terraform, managed add-onshcl
resource "aws_eks_addon" "vpc_cni" {
  cluster_name  = aws_eks_cluster.this.name
  addon_name    = "vpc-cni"
  addon_version = "v1.19.2-eksbuild.1"
  pod_identity_association {
    role_arn        = aws_iam_role.vpc_cni.arn
    service_account = "aws-node"
  }
  configuration_values = jsonencode({
    env = {
      ENABLE_PREFIX_DELEGATION = "true"
      WARM_PREFIX_TARGET       = "1"
      ENABLE_POD_ENI           = "true"     # security groups for pods
    }
  })
}

resource "aws_eks_addon" "pod_identity" {
  cluster_name  = aws_eks_cluster.this.name
  addon_name    = "eks-pod-identity-agent"
  addon_version = "v1.3.4-eksbuild.1"
}

# coredns, kube-proxy, aws-ebs-csi-driver follow the same pattern, version pinned.

Why the API authentication mode

The access-entry model keeps cluster access in AWS Identity and Access Management, where it is auditable and removable without editing an in-cluster config map. It pairs with the multi-account model so that a role in one account cannot silently gain access to a cluster in another.

5. Node strategy and Karpenter

A small managed node group runs the system components and Karpenter itself, on hardened Bottlerocket nodes. Karpenter then provisions everything else just in time. All node pools are AWS Graviton based.

terraform, system managed node grouphcl
resource "aws_eks_node_group" "system" {
  cluster_name    = aws_eks_cluster.this.name
  node_group_name = "system"
  node_role_arn   = aws_iam_role.node.arn
  subnet_ids      = var.app_subnet_ids
  ami_type        = "BOTTLEROCKET_ARM_64"
  capacity_type   = "ON_DEMAND"
  instance_types  = ["m7g.large"]
  scaling_config { min_size = 3  max_size = 6  desired_size = 3 }
  update_config   { max_unavailable_percentage = 33 }
  labels = { "pool" = "system" }
  taint { key = "CriticalAddonsOnly" value = "true" effect = "NO_SCHEDULE" }
  launch_template { id = aws_launch_template.system.id  version = "$Latest" }
}
# Launch template enforces IMDSv2 and encrypted volumes
resource "aws_launch_template" "system" {
  name = "${local.name_prefix}-system"
  metadata_options { http_tokens = "required"  http_put_response_hop_limit = 1 }
  block_device_mappings {
    device_name = "/dev/xvda"
    ebs { volume_size = 40  volume_type = "gp3"  encrypted = true  kms_key_id = aws_kms_key.ebs.arn }
  }
}
1 Amazon EKS managed control plane - v1.33, private endpoint, KMS-encrypted secrets Highly available across three AZs. All control-plane logs streamed. Access by IAM access entries. 2 Static baseline - system managed node group 3 x m7g.large, on-demand, Graviton Bottlerocket arm64, signed AMI IMDSv2 required, gp3 encrypted (KMS) Taint: CriticalAddonsOnly=true Runs the platform itself: CoreDNS, kube-proxy Karpenter controller ArgoCD, Istio control External Secrets, Kyverno Minimal, fixed, predictable. Everything else is provisioned by Karpenter. 3 Karpenter - just-in-time provisioning EC2NodeClass bottlerocket-arm amiFamily Bottlerocket, alias bottlerocket@latest - role KarpenterNodeRole-pay subnet & securityGroup selectors by tag - httpTokens required - gp3 80Gi encrypted (alias/pay-ebs) NodePool tier0-app arch arm64, capacity-type on-demand instance-family m7g, r7g (xlarge, 2xlarge) taint workload-tier=0:NoSchedule limits cpu 400, memory 1600Gi disruption WhenEmpty, consolidateAfter 5m NodePool tier1-app arch arm64, capacity-type spot & on-demand instance-family m7g, c7g, r7g limits cpu 1000, memory 4000Gi disruption WhenEmptyOrUnderutilized 30s packs aggressively for cost 4 Managed add-ons - version pinned, updated through the pipeline vpc-cniprefix delegation, SGs for pods eks-pod-identity-agentworkload identity corednscluster DNS kube-proxyservice routing aws-ebs-csi-driverencrypted volumes 5 Provisioning flow 1Pending pod unschedulable,matches a NodePool 2Karpenter evaluates picks cheapestinstance that fits 3Launch node Bottlerocket signedAMI boots ~45-60s 4Join and schedule node registers, podbinds, mesh identity 6 AWS Fargate - batch isolation Selector: namespace batch Each pod runs on its own dedicated kernel No shared node with cardholder workloads Right for short-lived jobs and isolation Pod execution role scoped to batch namespace
Figure 10. Node strategy. A small managed node group runs the system components and Karpenter, which then provisions Graviton Bottlerocket nodes just in time into separate pools per workload tier.

Karpenter node class and node pools

One node class defines how nodes are built, including the Bottlerocket family, encrypted storage, and enforced instance metadata version two. Two node pools follow. The tier-zero pool uses on-demand capacity for predictable performance, and the tier-one pool mixes Spot and on-demand for cost. Both restrict to Graviton.

kubernetes, karpenter EC2NodeClassyaml
apiVersion: karpenter.k8s.aws/v1
kind: EC2NodeClass
metadata:
  name: bottlerocket-arm
spec:
  amiFamily: Bottlerocket
  amiSelectorTerms:
    - alias: bottlerocket@latest
  role: KarpenterNodeRole-pay
  subnetSelectorTerms:
    - tags: { "kubernetes.io/role/internal-elb": "1", "Tier": "application" }
  securityGroupSelectorTerms:
    - tags: { "karpenter.sh/discovery": "pay-prod-cac1-eks" }
  metadataOptions:
    httpTokens: required
    httpPutResponseHopLimit: 1
  blockDeviceMappings:
    - deviceName: /dev/xvdb
      ebs: { volumeSize: 80Gi, volumeType: gp3, encrypted: true, kmsKeyID: "alias/pay-ebs" }
  tags: { "karpenter.sh/discovery": "pay-prod-cac1-eks", "Tier": "application" }
kubernetes, karpenter NodePools, tier-0 and tier-1yaml
apiVersion: karpenter.sh/v1
kind: NodePool
metadata: { name: tier0-app }
spec:
  template:
    metadata:
      labels: { pool: tier0, workload-tier: "0" }
    spec:
      nodeClassRef: { group: karpenter.k8s.aws, kind: EC2NodeClass, name: bottlerocket-arm }
      taints:
        - key: workload-tier
          value: "0"
          effect: NoSchedule
      requirements:
        - { key: kubernetes.io/arch, operator: In, values: ["arm64"] }
        - { key: karpenter.sh/capacity-type, operator: In, values: ["on-demand"] }
        - { key: karpenter.k8s.aws/instance-family, operator: In, values: ["m7g","r7g"] }
        - { key: karpenter.k8s.aws/instance-size, operator: In, values: ["xlarge","2xlarge"] }
  limits: { cpu: "400", memory: 1600Gi }
  disruption:
    consolidationPolicy: WhenEmpty          # tier-0 is conservative, no live consolidation
    consolidateAfter: 5m
    budgets: [ { nodes: "10%" } ]
---
apiVersion: karpenter.sh/v1
kind: NodePool
metadata: { name: tier1-app }
spec:
  template:
    metadata:
      labels: { pool: tier1, workload-tier: "1" }
    spec:
      nodeClassRef: { group: karpenter.k8s.aws, kind: EC2NodeClass, name: bottlerocket-arm }
      requirements:
        - { key: kubernetes.io/arch, operator: In, values: ["arm64"] }
        - { key: karpenter.sh/capacity-type, operator: In, values: ["spot","on-demand"] }
        - { key: karpenter.k8s.aws/instance-family, operator: In, values: ["m7g","c7g","r7g"] }
  limits: { cpu: "1000", memory: 4000Gi }
  disruption:
    consolidationPolicy: WhenEmptyOrUnderutilized   # tier-1 packs aggressively
    consolidateAfter: 30s

Short-lived batch jobs run on AWS Fargate for hard isolation from the node fleet, scoped to a single namespace.

terraform, fargate profile for batchhcl
resource "aws_eks_fargate_profile" "batch" {
  cluster_name           = aws_eks_cluster.this.name
  fargate_profile_name   = "batch"
  pod_execution_role_arn = aws_iam_role.fargate.arn
  subnet_ids             = var.app_subnet_ids
  selector { namespace = "batch" }
}

6. Kubernetes platform configuration

Product domains run in their own namespaces. Each namespace joins the ambient mesh, enforces the restricted pod security standard, and starts from default-deny networking. Quotas and limits prevent any one domain from starving the cluster.

kubernetes, namespace, quota, and default-denyyaml
apiVersion: v1
kind: Namespace
metadata:
  name: pay-auth
  labels:
    istio.io/dataplane-mode: ambient
    pod-security.kubernetes.io/enforce: restricted
    pod-security.kubernetes.io/enforce-version: latest
    workload-tier: "0"
---
apiVersion: v1
kind: ResourceQuota
metadata: { name: quota, namespace: pay-auth }
spec:
  hard:
    requests.cpu: "200"
    requests.memory: 400Gi
    limits.cpu: "400"
    limits.memory: 800Gi
    pods: "500"
---
apiVersion: v1
kind: LimitRange
metadata: { name: limits, namespace: pay-auth }
spec:
  limits:
    - type: Container
      default:        { cpu: "500m", memory: 512Mi }
      defaultRequest: { cpu: "250m", memory: 256Mi }
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: { name: default-deny, namespace: pay-auth }
spec:
  podSelector: {}
  policyTypes: [Ingress, Egress]    # nothing is allowed until a policy opens it

Mesh identity and authorization

Mutual Transport Layer Security is set to strict for the whole mesh, so unauthenticated traffic between workloads is rejected. Authorization policies then express which identity may call which service, which is stronger than network reachability alone.

kubernetes, Istio strict mTLS and an authorization policyyaml
apiVersion: security.istio.io/v1
kind: PeerAuthentication
metadata: { name: default, namespace: istio-system }
spec:
  mtls: { mode: STRICT }
---
apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata: { name: allow-ingress-to-auth, namespace: pay-auth }
spec:
  selector: { matchLabels: { app: payment-auth } }
  action: ALLOW
  rules:
    - from:
        - source:
            principals: ["cluster.local/ns/ingress/sa/gateway"]
      to:
        - operation:
            methods: ["POST"]
            paths: ["/v1/authorize"]
1 Private ingress and mesh identity Internal NLBscheme internal, target IP Gateway (istio)listener 443, TLS terminate HTTPRoutehost api.pay.internal, /v1/authorize Service payment-authport 8080, in pay-auth PeerAuthentication - mTLS STRICT, mesh-wide AuthorizationPolicy - only ingress gateway may POST /v1/authorize 2 Namespace isolation per product domain pay-auth - tier 0, in CDE - dedicated nodes (taint) - default-deny NetworkPolicy - ResourceQuota, LimitRange - PodSecurity: restricted, istio ambient pay-settlement - tier 0, in CDE - dedicated nodes (taint) - default-deny NetworkPolicy - ResourceQuota, LimitRange - PodSecurity: restricted, istio ambient pay-messaging - tier 1 - Karpenter nodes - default-deny NetworkPolicy - ResourceQuota, LimitRange - PodSecurity: restricted, istio ambient batch - tier 1 - AWS Fargate - default-deny NetworkPolicy - ResourceQuota, LimitRange - PodSecurity: restricted, mesh ztunnel 3 Admission control - Kyverno On every pod create, the cluster verifies provenance before the pod runs. kube-apiserveradmission webhook call Kyverno policyverify-signed-images (Enforce) Verify cosignkey awskms:///alias/pay-cosign Resolve digestmutate tag to verified digest Disallow latestreject floating tags Admit only if signed and approved Otherwise deny the pod 4 Runtime secrets - External Secrets AWS Secrets Managerreached via interface endpoint ClusterSecretStoreauth by EKS Pod Identity (JWT) ExternalSecretkey pay/prod/ledger/dsn, refresh 1h Kubernetes Secretcreated in the namespace Podconsumes as env or file, never in Git
Figure 11. Platform controls. Private ingress through the Gateway API, namespace isolation with default-deny, strict mesh identity, signature verification at admission, and secrets pulled at runtime.

Private ingress with the Gateway API

Ingress is an internal network load balancer fronting an Istio gateway. The load balancer is internal and target-type IP, so there is no public listener anywhere. Routes attach to the gateway per product.

kubernetes, Gateway and HTTPRoute on an internal NLByaml
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: pay-gateway
  namespace: ingress
  annotations:
    service.beta.kubernetes.io/aws-load-balancer-type: external
    service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: ip
    service.beta.kubernetes.io/aws-load-balancer-scheme: internal
spec:
  gatewayClassName: istio
  listeners:
    - name: https
      protocol: HTTPS
      port: 443
      tls:
        mode: Terminate
        certificateRefs: [ { name: pay-tls } ]
      allowedRoutes: { namespaces: { from: Selector, selector: { matchLabels: { ingress: "true" } } } }
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata: { name: auth, namespace: pay-auth }
spec:
  parentRefs: [ { name: pay-gateway, namespace: ingress } ]
  hostnames: ["api.pay.internal"]
  rules:
    - matches: [ { path: { type: PathPrefix, value: /v1/authorize } } ]
      backendRefs: [ { name: payment-auth, port: 8080 } ]

Secrets from AWS Secrets Manager

The External Secrets operator pulls secrets at runtime using a workload identity, so no secret is ever stored in Git or in an image. The store authenticates through EKS Pod Identity.

kubernetes, ClusterSecretStore and ExternalSecretyaml
apiVersion: external-secrets.io/v1
kind: ClusterSecretStore
metadata: { name: aws-secrets }
spec:
  provider:
    aws:
      service: SecretsManager
      region: ca-central-1
      auth: { jwt: { serviceAccountRef: { name: external-secrets, namespace: external-secrets } } }
---
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata: { name: ledger-credentials, namespace: pay-auth }
spec:
  refreshInterval: 1h
  secretStoreRef: { name: aws-secrets, kind: ClusterSecretStore }
  target: { name: ledger-credentials, creationPolicy: Owner }
  data:
    - secretKey: dsn
      remoteRef: { key: pay/prod/ledger/dsn }

Admission time signature verification

Kyverno verifies that every image is signed by the platform cosign key held in AWS Key Management Service, rejects unsigned images, resolves tags to digests, and forbids the floating latest tag. This is the in-cluster end of the supply chain that the pipeline builds.

kubernetes, Kyverno image verification policyyaml
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata: { name: verify-signed-images }
spec:
  validationFailureAction: Enforce
  webhookTimeoutSeconds: 30
  rules:
    - name: verify-cosign-kms
      match:
        any: [ { resources: { kinds: ["Pod"] } } ]
      verifyImages:
        - imageReferences: ["ACCOUNT.dkr.ecr.ca-central-1.amazonaws.com/pay/*"]
          mutateDigest: true            # pin tag to the verified digest
          required: true
          attestors:
            - count: 1
              entries:
                - keys:
                    kms: "awskms:///alias/pay-cosign"
    - name: disallow-latest
      match: { any: [ { resources: { kinds: ["Pod"] } } ] }
      validate:
        message: "Floating tags are not allowed, use a digest."
        pattern: { spec: { containers: [ { image: "!*:latest" } ] } }

7. Tier-zero ledger configuration

The tier-zero ledger is self-managed PostgreSQL 16 on AWS Graviton instances, orchestrated by Patroni, with synchronous streaming replication that requires the partner-region standby to acknowledge before a commit returns. This delivers a literal zero recovery point objective across a region loss with PostgreSQL semantics, which no managed AWS service offers inside Canada today.

1 Application tier - reaches the ledger through PgBouncer (transaction pooling, port 6432) Smart client and cell router send each write to the region that owns the cell. Reads stay local. The pooler fronts thousands of clients on few server connections. ca-central-1 - Patroni cluster scope pay-ledger PostgreSQL primary - pay_cac1_primary writer C1-C8 - synchronous_commit = remote_apply leader, elected via etcd local replica(sync candidate) local replica(read, async) Synchronous standby - pay_cac1_standby holds partner cells C9-C16, promotion target on partner loss two local replicas give fast in-region failover ca-west-1 - Patroni cluster scope pay-ledger PostgreSQL primary - pay_caw1_primary writer C9-C16 - synchronous_commit = remote_apply leader, elected via etcd local replica(sync candidate) local replica(read, async) Synchronous standby - pay_caw1_standby holds partner cells C1-C8, promotion target on partner loss two local replicas give fast in-region failover ANY 1 partner standby required remote_apply strict mode etcd - Patroni DCS (quorum across three Canadian failure domains) etcd-cac1 ca-central-1 voter, holds cluster state and the cell map etcd-caw1 ca-west-1 voter, holds cluster state and the cell map etcd-onprem (WITNESS) on-premises Canada voter only, no ledger data, breaks ties Raft quorum, majority of 3 Sync replication C1-C8 Sync replication C9-C16 etcd quorum and election Partition behavior: If the partner region is unreachable, strict synchronous mode blocks commits on the affected cells rather than risk loss. The etcd quorum, including the on-premises witness, decides which side keeps its leader, so there is no split-brain.
Figure 12. Tier-zero ledger topology. Self-managed PostgreSQL per region with a synchronous standby in the partner region, and an etcd quorum whose third member is the on-premises witness.

Topology and quorum

Each region runs a Patroni-managed PostgreSQL cluster with one primary and two local replicas for fast in-region failover, plus a synchronous standby in the partner region. Patroni stores cluster state in an etcd cluster whose members sit in Canada Central, Canada West, and the on-premises data center. That third on-premises member is the witness from ADR-05. It holds only cluster-coordination metadata, never ledger data, and it gives the cluster an odd number of voters inside Canada so that leader election survives a clean partition without split-brain.

How zero recovery point objective is actually guaranteed

The setting synchronous_commit = remote_apply with a synchronous standby list that requires the partner-region node means a commit is acknowledged only after the partner region has applied it. With synchronous_mode_strict enabled, Patroni never silently falls back to asynchronous replication, so the guarantee is never quietly weakened. If the partner region is unreachable, commits on the affected cells block rather than risk loss, which is the deliberate partition behavior from ADR-05. Patroni promotes only a node known to hold every synchronously confirmed transaction, so failover is also lossless. Operators may add a local synchronous member for faster in-region failover, accepting that a local synchronous node outage then blocks writes until Patroni reconfigures the set.

postgresql.conf, durability and replicationini
wal_level                  = replica
synchronous_commit         = remote_apply
synchronous_standby_names  = 'ANY 1 (pay_caw1_standby)'   # partner region required
max_wal_senders            = 16
max_replication_slots      = 16
hot_standby                = on
wal_compression            = on
wal_keep_size              = 8GB
max_connections            = 600          # PgBouncer fronts client connections
shared_buffers             = 32GB         # on the r7g.4xlarge, 128 GiB class
effective_cache_size       = 96GB
checkpoint_completion_target = 0.9
ssl                        = on
ssl_min_protocol_version   = 'TLSv1.3'
password_encryption        = scram-sha-256
log_min_duration_statement = 500ms
patroni.yml, synchronous orchestration with on-premises witnessyaml
scope: pay-ledger
namespace: /pay/
name: pay_cac1_primary

etcd3:
  hosts:
    - etcd-cac1.pay.internal:2379
    - etcd-caw1.pay.internal:2379
    - etcd-onprem.pay.internal:2379      # witness, quorum only, no data

bootstrap:
  dcs:
    ttl: 30
    loop_wait: 10
    retry_timeout: 10
    synchronous_mode: true
    synchronous_mode_strict: true        # never degrade to async, preserve 0 RPO
    synchronous_node_count: 1
    postgresql:
      use_pg_rewind: true
      parameters:
        synchronous_commit: remote_apply
        synchronous_standby_names: 'ANY 1 (pay_caw1_standby)'

postgresql:
  listen: 0.0.0.0:5432
  data_dir: /pgdata/16
  authentication:
    replication: { username: replicator }
  callbacks:
    on_role_change: /opt/pay/on_role_change.sh   # updates the cell map on promotion

tags:
  nofailover: false
  clonefrom: true

Storage, encryption, and backups

Data sits on io2 Block Express volumes with provisioned input and output operations for predictable commit latency, encrypted with a customer-managed key. Backups use pgBackRest to an Amazon S3 bucket with object lock for write-once retention, which gives point-in-time recovery that a deletion or a faulty change cannot tamper with.

SettingValueReason
Instancer7g.4xlarge, GravitonMemory-heavy ledger, efficient cores
Volumeio2 Block Express, 64000 provisioned operationsConsistent commit latency under load
EncryptionKMS customer-managed keyKey ownership and audit
BackuppgBackRest to Amazon S3 with object lockImmutable point-in-time recovery
PoolingPgBouncer, transaction mode, port 6432Thousands of clients on few server connections

Cells and routing

The ledger keyspace is split into sixteen cells by a consistent hash of the account key. Cells one through eight are homed to Canada Central and nine through sixteen to Canada West, which keeps a single writer per cell. The cell map is a small, strongly consistent record of which region owns which cell, held in the same etcd cluster that backs Patroni. On a promotion the role-change callback updates the owning region for the affected cells, and the smart client and router read the map to direct each write to the owning region.

cell map record, held in etcd and read by the routerjson
{
  "version": 184,
  "cells": {
    "C1":  { "home": "ca-central-1", "writer": "pay_cac1_primary", "state": "active" },
    "C8":  { "home": "ca-central-1", "writer": "pay_cac1_primary", "state": "active" },
    "C9":  { "home": "ca-west-1",    "writer": "pay_caw1_primary", "state": "active" },
    "C16": { "home": "ca-west-1",    "writer": "pay_caw1_primary", "state": "active" }
  },
  "adoption": { "in_progress": false, "adopted_by": null }
}

8. Tier-one data services

Transfer rails and messaging tolerate a recovery point objective of one to five minutes, so they use managed services with asynchronous cross-region replication. This is where Amazon Aurora is the right tool.

ca-central-1 (primary) ca-west-1 (secondary, promotable) 1 Aurora PostgreSQL Global Database Cluster pay-rails-cac1, engine 16.4 Writer plus reader, db.r7g.2xlarge Multi-AZ, encrypted with KMS Backup retention 35 days, IAM auth Aurora global replication, ~ < 1 s Secondary cluster pay-rails-caw1 Asynchronous replica of the global cluster Promotable on managed failover Serves local reads in region Becomes writer if Central is lost 2 Amazon MSK - pay-events Provisioned, 3 brokers, one per AZ kafka.m7g.large, 1 TB storage each TLS in transit, KMS at rest SASL with IAM client authentication MSK Replicator topics + groups Amazon MSK - pay-events-caw1 Replication target cluster Receives mirrored topics Consumer group offsets synced Consumers resume after a region loss 3 ElastiCache for Redis - pay-cache-cac1 Cluster mode, 3 shards, 1 replica each cache.r7g.large, automatic failover Multi-AZ enabled Transit and at-rest encryption, KMS Redis global datastore, async Secondary replication group - caw1 Global datastore secondary Low-latency local reads in region Promotable to primary on failover Same encryption posture Recovery point objective for tier 1: A region loss can drop at most the replication-lag window, which stays within one to five minutes and is acceptable for these products. Consumers are idempotent and tolerate at-least-once delivery, so a brief replay on failover does not double-apply.
Figure 13. Tier-one data services. Aurora Global Database, Amazon MSK with the Replicator, and ElastiCache global datastore each replicate asynchronously across regions.

Aurora PostgreSQL Global Database

terraform, aurora global databasehcl
resource "aws_rds_global_cluster" "rails" {
  global_cluster_identifier = "pay-rails"
  engine                    = "aurora-postgresql"
  engine_version            = "16.4"
  storage_encrypted         = true
}

resource "aws_rds_cluster" "rails_primary" {
  cluster_identifier        = "pay-rails-cac1"
  engine                    = "aurora-postgresql"
  engine_version            = "16.4"
  global_cluster_identifier = aws_rds_global_cluster.rails.id
  db_subnet_group_name      = aws_db_subnet_group.data.name
  vpc_security_group_ids    = [aws_security_group.aurora.id]
  kms_key_id                = aws_kms_key.rds.arn
  storage_encrypted         = true
  backup_retention_period   = 35
  iam_database_authentication_enabled = true
  db_cluster_parameter_group_name     = aws_rds_cluster_parameter_group.rails.name
}

resource "aws_rds_cluster_instance" "rails_primary" {
  count              = 2                       # writer plus reader, Multi-AZ
  cluster_identifier = aws_rds_cluster.rails_primary.id
  instance_class     = "db.r7g.2xlarge"        # Graviton
  engine             = "aurora-postgresql"
}

# Secondary region cluster, asynchronous replica, promotable on failover
resource "aws_rds_cluster" "rails_secondary" {
  provider                  = aws.caw1
  cluster_identifier        = "pay-rails-caw1"
  engine                    = "aurora-postgresql"
  engine_version            = "16.4"
  global_cluster_identifier = aws_rds_global_cluster.rails.id
  kms_key_id                = aws_kms_key.rds_caw1.arn
  depends_on                = [aws_rds_cluster_instance.rails_primary]
}

Amazon MSK and cross-region replication

The event backbone is a provisioned MSK cluster with one broker per Availability Zone, encryption in transit and at rest, and identity-based client authentication. MSK Replicator copies topics to the partner region so consumers continue after a region loss.

terraform, msk cluster and replicatorhcl
resource "aws_msk_cluster" "events" {
  cluster_name           = "pay-events"
  kafka_version          = "3.7.x"
  number_of_broker_nodes = 3
  broker_node_group_info {
    instance_type   = "kafka.m7g.large"
    client_subnets  = var.data_subnet_ids
    security_groups = [aws_security_group.msk.id]
    storage_info { ebs_storage_info { volume_size = 1000 } }
  }
  encryption_info {
    encryption_at_rest_kms_key_arn = aws_kms_key.msk.arn
    encryption_in_transit { client_broker = "TLS"  in_cluster = true }
  }
  client_authentication { sasl { iam = true } }
  open_monitoring { prometheus { jmx_exporter { enabled_in_broker = true } node_exporter { enabled_in_broker = true } } }
}

resource "aws_msk_replicator" "to_caw1" {
  replicator_name = "pay-events-to-caw1"
  kafka_cluster {
    amazon_msk_cluster { msk_cluster_arn = aws_msk_cluster.events.arn }
    vpc_config { subnet_ids = var.data_subnet_ids  security_group_ids = [aws_security_group.msk.id] }
  }
  kafka_cluster {
    amazon_msk_cluster { msk_cluster_arn = var.caw1_msk_arn }
    vpc_config { subnet_ids = var.caw1_data_subnet_ids  security_group_ids = [var.caw1_msk_sg] }
  }
  replication_info_list {
    target_compression_type = "GZIP"
    topic_replication { topic_name_configuration_sync_ms = 60000  copy_topic_configurations = true }
    consumer_group_replication { consumer_groups_to_replicate = ["*"] }
  }
}

ElastiCache for Redis, global datastore

terraform, redis replication group with global datastorehcl
resource "aws_elasticache_replication_group" "cache" {
  replication_group_id       = "pay-cache-cac1"
  description                = "payments cache, primary"
  engine                     = "redis"
  engine_version             = "7.1"
  node_type                  = "cache.r7g.large"
  num_node_groups            = 3            # cluster mode, sharded
  replicas_per_node_group    = 1
  automatic_failover_enabled = true
  multi_az_enabled           = true
  transit_encryption_enabled = true
  at_rest_encryption_enabled = true
  kms_key_id                 = aws_kms_key.redis.arn
  subnet_group_name          = aws_elasticache_subnet_group.data.name
  security_group_ids         = [aws_security_group.redis.id]
}

resource "aws_elasticache_global_replication_group" "cache" {
  global_replication_group_id_suffix = "pay-cache"
  primary_replication_group_id       = aws_elasticache_replication_group.cache.id
}

9. Identity, access, and keys

Workload access to AWS is granted through EKS Pod Identity, so a pod assumes a role scoped to exactly what it needs, with no static keys on nodes and no shared node role for application access. Each association binds a Kubernetes service account to an IAM role.

terraform, pod identity and a least-privilege rolehcl
resource "aws_eks_pod_identity_association" "auth" {
  cluster_name    = aws_eks_cluster.this.name
  namespace       = "pay-auth"
  service_account = "payment-auth"
  role_arn        = aws_iam_role.payment_auth.arn
}

resource "aws_iam_role" "payment_auth" {
  name = "${local.name_prefix}-payment-auth"
  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect = "Allow"
      Principal = { Service = "pods.eks.amazonaws.com" }
      Action = ["sts:AssumeRole", "sts:TagSession"]
    }]
  })
}

resource "aws_iam_role_policy" "payment_auth" {
  role = aws_iam_role.payment_auth.id
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Sid    = "ReadOwnSecret"
        Effect = "Allow"
        Action = ["secretsmanager:GetSecretValue"]
        Resource = "arn:aws:secretsmanager:ca-central-1:ACCOUNT:secret:pay/prod/ledger/*"
      },
      {
        Sid    = "DecryptWithTenantKey"
        Effect = "Allow"
        Action = ["kms:Decrypt"]
        Resource = aws_kms_key.tenant_auth.arn
      }
    ]
  })
}
Workload identity - EKS Pod Identity 1 Pod, service account payment-auth namespace pay-auth, no credentials mounted 2 EKS Pod Identity association binds (cluster, namespace, serviceAccount) to a role 3 IAM role pay-prod-cac1-payment-auth trusts pods.eks.amazonaws.com, AssumeRole and TagSession 4 Scoped permission policy GetSecretValue on pay/prod/ledger/*, kms:Decrypt on the tenant key 5 AWS resources Secrets Manager and the tenant KMS key, reached via private endpoints No long-lived IAM users, no keys on nodes, sessions are short-lived Customer-managed keys - AWS KMS alias/pay-eks-secrets symmetric, rotate 90d used by: etcd secret envelope policy scoped to its roles alias/pay-ebs symmetric, rotate 90d used by: node and PV volumes policy scoped to its roles alias/pay-rds symmetric, rotate 90d used by: Aurora at rest policy scoped to its roles alias/pay-msk symmetric, rotate 90d used by: MSK at rest policy scoped to its roles alias/pay-redis symmetric, rotate 90d used by: ElastiCache at rest policy scoped to its roles alias/pay-observability symmetric, rotate 90d used by: Thanos S3 and logs policy scoped to its roles alias/pay-tenant-auth symmetric, rotate 90d used by: per-tenant, auth data policy scoped to its roles alias/pay-tenant-settle symmetric, rotate 90d used by: per-tenant, settlement policy scoped to its roles alias/pay-cosign ECC_NIST_P256, sign used by: image signing and verify policy scoped to its roles Principles: Keys are separated by purpose and by tenant where isolation requires it. Each key policy grants use only to the specific roles and services that need it and denies everything else. Symmetric keys auto-rotate. The asymmetric signing key is rotated by re-issuing and re-signing.
Figure 14. Identity and keys. Workloads reach AWS through EKS Pod Identity with no static keys, and encryption uses purpose-scoped customer-managed keys with rotation.

Key management

Encryption uses customer-managed keys, with separate keys per purpose and per tenant where isolation requires it. Rotation is enabled. The key policy grants use to the specific roles and services that need it and denies everything else.

terraform, customer-managed key with a scoped policyhcl
resource "aws_kms_key" "tenant_auth" {
  description             = "pay tenant key, payment-auth"
  enable_key_rotation     = true
  rotation_period_in_days = 90
  deletion_window_in_days = 30
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Sid = "RootAdmin"
        Effect = "Allow"
        Principal = { AWS = "arn:aws:iam::ACCOUNT:root" }
        Action = ["kms:Create*","kms:Describe*","kms:Enable*","kms:List*","kms:Put*","kms:ScheduleKeyDeletion"]
        Resource = "*"
      },
      {
        Sid = "WorkloadUse"
        Effect = "Allow"
        Principal = { AWS = aws_iam_role.payment_auth.arn }
        Action = ["kms:Decrypt","kms:GenerateDataKey"]
        Resource = "*"
      }
    ]
  })
}

# The cosign signing key is asymmetric on the elliptic curve, used by CI to sign and by Kyverno to verify
resource "aws_kms_key" "cosign" {
  description              = "pay image signing"
  customer_master_key_spec = "ECC_NIST_P256"
  key_usage                = "SIGN_VERIFY"
  enable_key_rotation      = false       # asymmetric keys are not auto-rotated
}
resource "aws_kms_alias" "cosign" { name = "alias/pay-cosign"  target_key_id = aws_kms_key.cosign.id }

10. Security services

Detection runs at the account, cluster, and host levels. The account baseline in section 2 turns on the AWS managed services. Inside the cluster a runtime sensor watches for suspicious process and file activity. The table summarizes the layers and what each one covers.

LayerServiceCoverage
Threat detectionAmazon GuardDuty, EKS protection and runtime monitoringControl-plane audit anomalies and on-host runtime threats
PostureAWS Security HubAggregated findings against the Foundational and PCI standards
VulnerabilitiesAmazon InspectorContinuous image, host, and function scanning
ConfigurationAWS Config conformance packDrift from required controls, encryption, public access
Runtime, in-clusterFalcoProcess, file, and network syscall rules on every node
Sensitive dataAmazon MacieDetection of card data patterns in any storage bucket
Cloud and account 1 Amazon GuardDuty EKS protection and runtime monitoring AWS Security Hub Foundational and PCI standards Amazon Inspector image, host, and function CVEs AWS Config conformance pack, drift detection Cluster and host 2 EKS control-plane audit api, audit, authenticator logs Falco eBPF runtime syscall rules per node Kyverno admission policy decisions and denials Node and Karpenter events provisioning and lifecycle Data and network 3 Amazon Macie card data patterns in S3 VPC Flow Logs every flow recorded Network Firewall egress alerts and drops AWS CloudTrail every API call, organization trail Delegated administration - security-tooling account findings aggregated across all accounts 4 Central SIEM Immutable retention with S3 object lock Correlation and alerting rules Long-term forensic search Compliance evidence store 5 Detection and response Automated containment playbooks On-call escalation to the security team Break-glass is logged and time-boxed Post-incident review feeds new rules Protected by policy: A service control policy denies disabling GuardDuty, stopping CloudTrail, or turning off AWS Config, so the detection layers cannot be quietly switched off, even by an account administrator.
Figure 15. Detection layers. Cloud, cluster, host, and data signals are aggregated by a delegated administrator into a central security information and event management system with immutable retention.

The runtime sensor runs as a privileged DaemonSet on a dedicated path, sending alerts to the central security information and event management system. A small set of rules is tuned to the payments workloads, for example flagging any shell spawned inside a cardholder container.

helm values, runtime sensor highlightsyaml
driver: { kind: modern_ebpf }      # no kernel module, eBPF probe
falco:
  rules_files:
    - /etc/falco/falco_rules.yaml
    - /etc/falco/pay_rules.yaml
  json_output: true
customRules:
  pay_rules.yaml: |-
    - rule: Shell in cardholder container
      desc: A shell was started inside a CDE workload
      condition: spawned_process and container and proc.name in (bash, sh, zsh)
                 and k8s.ns.name in (pay-auth, pay-settlement)
      output: "Shell in CDE (pod=%k8s.pod.name ns=%k8s.ns.name cmd=%proc.cmdline)"
      priority: CRITICAL
falcosidekick:
  enabled: true
  config: { aws: { sns: { topicarn: "arn:aws:sns:ca-central-1:ACCOUNT:pay-security" } } }

11. CI/CD configuration

GitHub Actions authenticates to AWS with OpenID Connect, so no static cloud credentials exist in the pipeline. The role that the pipeline assumes trusts only the specific repository and only on protected references.

terraform, github oidc trust for the ci rolehcl
resource "aws_iam_role" "ci_build" {
  name = "pay-ci-build"
  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect = "Allow"
      Principal = { Federated = "arn:aws:iam::ACCOUNT:oidc-provider/token.actions.githubusercontent.com" }
      Action = "sts:AssumeRoleWithWebIdentity"
      Condition = {
        StringEquals = { "token.actions.githubusercontent.com:aud" = "sts.amazonaws.com" }
        StringLike   = { "token.actions.githubusercontent.com:sub" = "repo:payments-org/pay-services:ref:refs/heads/main" }
      }
    }]
  })
}

Build, test, sign, and push

The pipeline builds with a rootless builder, runs tests and static and composition analysis, generates a software bill of materials, scans, then signs with the cosign key in Key Management Service and pushes by digest. A failing security gate fails the build.

.github/workflows/build.ymlyaml
name: build-sign-push
on:
  push: { branches: [main] }
  pull_request: {}
permissions:
  id-token: write        # required for OIDC
  contents: read
env:
  ECR: ACCOUNT.dkr.ecr.ca-central-1.amazonaws.com/pay/payment-auth
jobs:
  build:
    runs-on: [self-hosted, linux, arm64]    # private Fargate runners, see prior reference
    steps:
      - uses: actions/checkout@v4
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::ACCOUNT:role/pay-ci-build
          aws-region: ca-central-1
      - uses: aws-actions/amazon-ecr-login@v2
      - name: Unit and contract tests
        run: make test
      - name: Static analysis (SAST)
        run: semgrep ci --error
      - name: Build with rootless BuildKit
        id: build
        uses: docker/build-push-action@v6
        with:
          platforms: linux/arm64
          push: true
          tags: ${{ env.ECR }}:${{ github.sha }}
          provenance: true
          sbom: true
      - name: Generate SBOM
        uses: anchore/sbom-action@v0
        with: { image: "${{ env.ECR }}@${{ steps.build.outputs.digest }}", format: cyclonedx-json }
      - name: Vulnerability scan (fail on high)
        uses: aquasecurity/trivy-action@0.24.0
        with:
          image-ref: "${{ env.ECR }}@${{ steps.build.outputs.digest }}"
          severity: HIGH,CRITICAL
          exit-code: "1"
      - name: Sign image with cosign and KMS
        run: |
          cosign sign --yes --key awskms:///alias/pay-cosign \
            ${{ env.ECR }}@${{ steps.build.outputs.digest }}
      - name: Attest the SBOM
        run: |
          cosign attest --yes --key awskms:///alias/pay-cosign \
            --predicate sbom.cdx.json --type cyclonedx \
            ${{ env.ECR }}@${{ steps.build.outputs.digest }}
      - name: Record digest for delivery
        run: echo "${{ steps.build.outputs.digest }}" > image-digest.txt
      - uses: actions/upload-artifact@v4
        with: { name: image-digest, path: image-digest.txt }

Promote the digest into Git

Delivery is pull based. After a successful build, a second workflow updates the image digest in the configuration repository and opens a pull request, so every deployment is a reviewed Git change rather than a push from the pipeline.

.github/workflows/promote.ymlyaml
name: promote-digest
on:
  workflow_run: { workflows: [build-sign-push], types: [completed], branches: [main] }
jobs:
  bump:
    if: ${{ github.event.workflow_run.conclusion == 'success' }}
    runs-on: [self-hosted, linux, arm64]
    steps:
      - uses: actions/checkout@v4
        with: { repository: payments-org/pay-config, ssh-key: ${{ secrets.CONFIG_DEPLOY_KEY }} }
      - uses: actions/download-artifact@v4
        with: { name: image-digest }
      - name: Update the non-production overlay first
        run: |
          DIGEST=$(cat image-digest.txt)
          cd overlays/nonprod
          kustomize edit set image pay/payment-auth=ACCOUNT.dkr.ecr.ca-central-1.amazonaws.com/pay/payment-auth@${DIGEST}
      - uses: peter-evans/create-pull-request@v6
        with:
          branch: promote/payment-auth
          title: "Promote payment-auth to ${{ github.sha }}"
          labels: automated, promotion

ArgoCD project and application set

An AppProject confines what may be deployed and where. An ApplicationSet then generates one application per environment and region from a list, each pointing at the matching overlay, with sync waves so that namespaces and policies land before workloads.

kubernetes, ArgoCD AppProject and ApplicationSetyaml
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata: { name: payments, namespace: argocd }
spec:
  sourceRepos: ["https://github.com/payments-org/pay-config.git"]
  destinations:
    - { server: "https://kubernetes.default.svc", namespace: "pay-*" }
    - { name: "prod-caw1", namespace: "pay-*" }
  clusterResourceWhitelist: [ { group: "", kind: Namespace } ]
  roles:
    - name: deployer
      policies: ["p, proj:payments:deployer, applications, sync, payments/*, allow"]
---
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata: { name: payment-auth, namespace: argocd }
spec:
  generators:
    - list:
        elements:
          - { env: prod, region: cac1, cluster: "https://kubernetes.default.svc", wave: "2" }
          - { env: prod, region: caw1, cluster: "prod-caw1",                      wave: "3" }
  template:
    metadata:
      name: "payment-auth-{{env}}-{{region}}"
      annotations: { argocd.argoproj.io/sync-wave: "{{wave}}" }
    spec:
      project: payments
      source:
        repoURL: "https://github.com/payments-org/pay-config.git"
        targetRevision: main
        path: "overlays/{{env}}-{{region}}"
      destination: { server: "{{cluster}}", namespace: pay-auth }
      syncPolicy:
        automated: { prune: true, selfHeal: true }
        retry: { limit: 5 }

Progressive delivery with metric analysis

Workloads are delivered as Argo Rollouts using a canary that shifts mesh traffic in steps and runs an automated analysis at each step. The analysis queries Prometheus for success rate and latency, and a breach fails the rollout, which halts the wave and triggers the rollback path.

kubernetes, Argo Rollouts canary and AnalysisTemplateyaml
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata: { name: payment-auth, namespace: pay-auth }
spec:
  strategy:
    canary:
      canaryService: payment-auth-canary
      stableService: payment-auth-stable
      trafficRouting: { plugins: { argoproj-labs/gatewayAPI: { httpRoute: auth, namespace: pay-auth } } }
      steps:
        - setWeight: 5
        - pause: { duration: 5m }
        - analysis: { templates: [ { templateName: canary-health } ] }
        - setWeight: 25
        - pause: { duration: 10m }
        - analysis: { templates: [ { templateName: canary-health } ] }
        - setWeight: 50
        - pause: { duration: 10m }
---
apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata: { name: canary-health, namespace: pay-auth }
spec:
  metrics:
    - name: success-rate
      interval: 1m
      successCondition: result[0] >= 0.999
      failureLimit: 1
      provider:
        prometheus:
          address: http://thanos-query.monitoring:9090
          query: |
            sum(rate(http_requests_total{app="payment-auth",code!~"5.."}[2m]))
            / sum(rate(http_requests_total{app="payment-auth"}[2m]))
    - name: p99-latency
      interval: 1m
      successCondition: result[0] <= 0.250
      provider:
        prometheus:
          address: http://thanos-query.monitoring:9090
          query: |
            histogram_quantile(0.99,
              sum(rate(http_request_duration_seconds_bucket{app="payment-auth"}[2m])) by (le))

Promotion across the waves is driven by the sync-wave annotations in the application set and by the analysis results. Non-production reconciles first, then the performance environment runs load and soak, then production in Canada Central, and only after its bake time passes does production in Canada West proceed. A failed analysis at any wave reverts the Git change and ArgoCD reconciles the previous signed digest, which is the automated rollback from ADR-13.

Continuous integration and supply chain GitHub Actions, OIDC to AWS, no static keys 1 Commit signed, trunk-based 2 Build rootless BuildKit, arm64 3 Test, SAST, SCA unit, semgrep, deps 4 SBOM CycloneDX generated 5 Scan Inspector, fail on HIGH 6 Sign cosign + KMS ECC_P256 7 Push ECR by digest, attest image digest + provenance and SBOM attestations Continuous delivery GitOps with ArgoCD and Argo Rollouts 8 ApplicationSet generators env x region, sync-waves 9 ArgoCD reconcile pull-based, prune, self-heal 10 Admission verify Kyverno checks the cosign signature 11 Rollouts canary 5, 25, 50 percent, mesh shift 12 Analysis Prometheus success>=0.999, p99<=250ms the cosign KMS key signs in CI and verifies at admission Promotion waves separate account per environment and region, metric-gated A Non-prod dev and test, functional gates B Performance perf account, load and soak C Prod ca-central-1 canary then full, bake D Prod ca-west-1 wave 2 after bake perf wave 1 wave 2 Automatic rollback A failed analysis or health gate halts the wave, reverts the Git change, and ArgoCD reconciles the previous signed digest. A region is never promoted while the prior wave is unhealthy.
Figure 16. Delivery pipeline. Signed and attested images from GitHub Actions, reconciled by ArgoCD, verified again at admission, and rolled out region by region with metric-gated canaries and automatic rollback.

12. Observability stack

Each region runs Prometheus through the kube-prometheus-stack, with a Thanos sidecar shipping blocks to Amazon S3. A Thanos querier fans out across both regions to give a single global view, which an active-active platform needs. Traces flow through an OpenTelemetry collector, and Grafana renders the dashboards.

thanos objstore.yml and the OpenTelemetry collectoryaml
# thanos object store, per region, S3 with the regional KMS key
type: S3
config:
  bucket: pay-thanos-cac1
  endpoint: s3.ca-central-1.amazonaws.com
  sse_config: { type: SSE-KMS, kms_key_id: "alias/pay-observability" }
---
# OpenTelemetry collector, traces and metrics
receivers:
  otlp: { protocols: { grpc: {}, http: {} } }
processors:
  batch: {}
  memory_limiter: { check_interval: 1s, limit_percentage: 80 }
exporters:
  prometheusremotewrite: { endpoint: "http://prometheus.monitoring:9090/api/v1/write" }
  otlphttp/tempo: { endpoint: "http://tempo.monitoring:4318" }
service:
  pipelines:
    traces:  { receivers: [otlp], processors: [memory_limiter, batch], exporters: [otlphttp/tempo] }
    metrics: { receivers: [otlp], processors: [memory_limiter, batch], exporters: [prometheusremotewrite] }
Observability - ca-central-1 ca-central-1 Prometheus (kube-prometheus-stack) scrapes workloads, OTel remote-write Thanos sidecar uploads 2h blocks to object storage S3 object store pay-thanos-cac1 SSE-KMS with alias/pay-observability OpenTelemetry collector traces to Tempo, batched and limited Thanos Querier Global fan-out across both regions, sidecars and store gateways. One cross-region view for an active-active platform Observability - ca-west-1 ca-west-1 Prometheus (kube-prometheus-stack) scrapes workloads, OTel remote-write Thanos sidecar uploads 2h blocks to object storage S3 object store pay-thanos-caw1 SSE-KMS with alias/pay-observability OpenTelemetry collector traces to Tempo, batched and limited single global view to Grafana and rules 1 Grafana Queries the Thanos Querier for metrics Queries Tempo for traces Dashboards per product and per SLO Read-only, identity-federated access 2 Service level objective PrometheusRule, recording and alerting 99.999 percent target, tiny error budget Multi-window burn-rate alerts Budget tied to change policy 3 Alerting and response Alertmanager routes by severity Fast burn pages, slow burn warns Symptom-based, customer-facing first Runbook links on every alert Error budget example At 99.999 percent the monthly budget is about 26 seconds of downtime. A fast-burn alert fires when the budget is spent roughly fourteen times faster than sustainable over a short and a long window at once.
Figure 17. Observability. Each region runs Prometheus with a Thanos sidecar to Amazon S3, a global querier gives one cross-region view, and an error budget governs the availability objective.

Service level objective and error budget

The objective is expressed as recording and alerting rules. A 99.999 percent target leaves a very small budget, so the alerts use multi-window burn-rate logic, which pages on a fast burn and warns on a slow burn rather than on every brief dip.

kubernetes, PrometheusRule for the authorization objectiveyaml
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata: { name: pay-auth-slo, namespace: monitoring }
spec:
  groups:
    - name: payment-auth.slo
      rules:
        - record: job:auth_request_error:ratio_rate5m
          expr: |
            sum(rate(http_requests_total{app="payment-auth",code=~"5.."}[5m]))
            / sum(rate(http_requests_total{app="payment-auth"}[5m]))
        - alert: AuthErrorBudgetFastBurn
          # 99.999 percent objective, page when burning the budget 14x over a short window
          expr: |
            job:auth_request_error:ratio_rate5m > (14.4 * 0.00001)
            and ignoring(window)
            job:auth_request_error:ratio_rate1h > (14.4 * 0.00001)
          for: 2m
          labels: { severity: page }
          annotations: { summary: "Authorization error budget fast burn" }

13. Resilience testing

The resilience claims are verified, not assumed. AWS Fault Injection Service runs scheduled experiments that remove an Availability Zone, terminate ledger instances, and impair the cross-region path, while the team measures whether the recovery point and recovery time objectives hold. The example experiment fails a ledger primary and confirms that failover is lossless.

terraform, fault injection experiment, ledger primary losshcl
resource "aws_fis_experiment_template" "ledger_primary_loss" {
  description = "Terminate the ledger primary and verify zero data loss failover"
  role_arn    = aws_iam_role.fis.arn

  stop_condition {
    source = "aws:cloudwatch:alarm"
    value  = aws_cloudwatch_metric_alarm.auth_error_budget.arn   # abort if customers are harmed
  }
  action {
    name      = "stop-primary"
    action_id = "aws:ec2:stop-instances"
    target { key = "Instances" value = "ledger-primary" }
  }
  target {
    name           = "ledger-primary"
    resource_type  = "aws:ec2:instance"
    selection_mode = "ALL"
    resource_tag { key = "Role" value = "ledger-primary" }
  }
  experiment_options { account_targeting = "single-account" empty_target_resolution_mode = "fail" }
}

Each experiment has a documented expected outcome. For a region loss the expected result is a recovery point objective of zero, confirmed by reconciling the last committed transaction identifier on both sides, and a recovery time objective measured in seconds, confirmed by the gap in the authorization success metric. Results are recorded and reviewed, and a regression in either objective blocks the next release train.