Skip to content

MOD-075 — Internal API gateway

Purpose

The chokepoint AWS API Gateway HTTP API (v2) for every internal service-to-service and app-to-backend HTTPS call within the platform. Handles TLS termination, JWT-based authentication, per-service rate limiting (CAP-121 GATE), API-version routing (CAP-122 AUTO), structured access logging (FR-304, NFR-024), and WAF protection (DT-002 GATE) — all with ≤5 ms p99 overhead per FR-303 / NFR-018.

FR scope: FR-301, FR-302, FR-303, FR-304, NFR-018, NFR-020, NFR-024.

Architecture

              consumers (mobile, back-office, service-to-service)
                            HTTPS only (DT-001)
                       ┌───────────────────┐
                       │ AWS WAF v2 (DT-002)│  ← rate-based + managed rules
                       └─────────┬─────────┘
                       ┌───────────────────┐
                       │ HTTP API v2 stage │  ← stage throttle (CAP-121)
                       │  $default         │
                       └────┬──────────────┘
            ┌───────────────┼───────────────┐
            ▼               ▼               ▼
    JWT authoriser    JWT authoriser     access log
    (staff pool)     (customers pool)    CloudWatch (90d, immutable)
            │               │
            └─────┬─────────┘
        ┌─────────────────┐
        │ per-service     │  ← discovered at deploy time via SSM
        │ HTTP_PROXY      │     /bank/{env}/api-services/{id}/url
        │ integrations    │
        └─────┬───────────┘
   bank-core, bank-kyc, bank-payments, …  (each domain's HTTPS service)

What MOD-075 owns

Resource Purpose
aws.apigatewayv2.Api The HTTP API itself
aws.apigatewayv2.Stage ($default) Auto-deploy stage with structured access-log format
aws.apigatewayv2.Authorizer × 2 JWT authorizers wired to MOD-104's Cognito staff + customers pools
aws.apigatewayv2.Integration × N One HTTP_PROXY integration per registered service
aws.apigatewayv2.Route × 2N Exact + wildcard route per registered service
aws.cloudwatch.LogGroup Access log group, 90-day retention, KMS-encrypted, protect: true (NFR-024)
aws.wafv2.WebAcl 3 rules: AWS managed common, AWS managed bad inputs, DT-002 rate-based (1000/5min/IP)

Service registry

Domain modules register HTTP services by writing SSM parameters under /bank/{env}/api-services/{service-id}/:

Parameter Required Description
url yes Upstream HTTPS URL
version yes Logical version (v1, v2, …)
route-prefix yes Path prefix (e.g. /v1/accounts)
authorizer yes staff or customers
throttle-rate no Per-route req/s (default 200)
throttle-burst no Per-route burst (default 400)

MOD-075 reads these at deploy time (driven by the MOD_075_REGISTERED_SERVICES env var listing the service-ids to include — explicit list is reviewable in the deploy diff). Adding a service is a small Pulumi config change + redeploy.

SSM contract

Read (upstream)

Path Owner
/bank/{env}/kms/operational/arn MOD-104
/bank/{env}/cognito/staff/pool-id MOD-104
/bank/{env}/cognito/staff/client/back-office, /api-authoriser MOD-104
/bank/{env}/cognito/customers/pool-id MOD-104
/bank/{env}/cognito/customers/client/mobile, /web MOD-104
/bank/{env}/api-services/{service-id}/... each domain (registered services)

Write (downstream contract)

Path Value
/bank/{env}/api-gateway/url Base URL consumers point at
/bank/{env}/api-gateway/api-id API ID — for execute-api:Invoke IAM
/bank/{env}/api-gateway/stage-arn For WAF / alarm targets
/bank/{env}/api-gateway/log-group-name, /log-group-arn For MOD-076 dashboards + alerts
/bank/{env}/api-gateway/default-throttle-rate, /default-throttle-burst Stage defaults (200 / 400)
/bank/{env}/api-gateway/jwt-authorizer/staff-id, /customers-id For per-route wiring
/bank/{env}/api-gateway/registered-services CSV of currently-registered service-ids (observability)

Deferred: the waf-acl-arn SSM output (under api-gateway/{...future-leaf...}) is planned for the custom-domain release; not currently published in V1. Tracked as follow-up. The aws.wafv2.WebAcl resource lives in outputs.ts as a commented-out future addition; CI's reusable-iac SSM verification intentionally does not declare this path until publication lands.

FR coverage

FR Where it lives
FR-301 (path-based routing + JWT propagation) routes.ts HTTP_PROXY integration; HTTP API forwards Authorization header by default
FR-302 (per-service rate limit + 429 + access log) api-gateway.ts stage defaultRouteSettings; route-level overrides per service; WAF rate rule as second layer
FR-303 (≤5 ms p99 overhead) HTTP API (v2) chosen over REST; native JWT authorizer (no Lambda hop); regional endpoint
FR-304 (log every request, 90d retention) access-log.ts JSON format with all required fields; log group retentionInDays: 90 + protect: true
NFR-018 (same as FR-303) same
NFR-020 (≥99.95% gateway support) AWS HTTP API has 99.95% SLA
NFR-024 (audit log immutability) log group protect: true, KMS-encrypted; CloudWatch records aren't user-mutable

Policy coverage

Policy Mode How verified
DT-001 GATE (TLS + mutual auth) GATE V1: HTTPS-only (HTTP API has no port-80 listener) + JWT authorizer required on every route. Full client-cert mTLS deferred to next release with custom domain. Negative test: cleartext HTTP attempt fails.
DT-002 GATE (rate limiting + signed-request) GATE WAF rate-based rule (1000/5min/IP) + JWT-required on every route. Negative test: unauthenticated request returns 401; >1000 reqs/5min from one IP returns 403.

Tests

Layer What
Unit access-log format covers FR-304 fields, valid JSON; service-registry config invariants (rate ≤ ceiling, burst > rate); type-level authorizer enum
Integration scripts/verify-deployment.mjs — 1 assertion per FR + auth + downstream contract (~25 assertions against deployed dev)
Policy DT-001 (HTTPS-only signal in scripts) + DT-002 (WAF rule presence) — both inside the verify script

mTLS deferral

DT-001 GATE asks for "TLS-terminated endpoints with mutual authentication". HTTP API mTLS requires a custom domain — V1 ships without one. V1 satisfies DT-001 by:

  • TLS termination ✓ (HTTPS-only via API Gateway)
  • Server-side authentication ✓ (AWS-issued cert)
  • Client-side authentication ✓ (JWT authorizer on every route)

Full client-cert mTLS lands in the next release alongside a custom domain (e.g. api-{env}.bank-platform). The mTLS truststore S3 bucket is also added at that point. No breaking change to consumers — they keep pointing at the same SSM-published URL, which simply redirects to the custom domain when ready.

Operational notes

  • Adding a new service. Domain module's IaC writes the 4 required SSM params under /bank/{env}/api-services/{service-id}/.... Then MOD-075's deploy-time MOD_075_REGISTERED_SERVICES env var is bumped to include the new service-id, and CI redeploys MOD-075.
  • Per-route throttle tuning. Domain writes /bank/{env}/api-services/{service-id}/throttle-rate (and burst). MOD-075 picks them up on next redeploy.
  • Removing a service. Drop the service-id from MOD_075_REGISTERED_SERVICES and redeploy. Pulumi removes the routes + integration. Domain module can then delete the SSM params on its own cadence.
  • Custom domain (next release). Adds an ACM cert + custom domain + mTLS truststore S3 bucket. Existing consumers point at the custom domain; the AWS-generated URL gets disabled via disableExecuteApiEndpoint = true.