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-arnSSM output (underapi-gateway/{...future-leaf...}) is planned for the custom-domain release; not currently published in V1. Tracked as follow-up. Theaws.wafv2.WebAclresource lives inoutputs.tsas 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-timeMOD_075_REGISTERED_SERVICESenv 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_SERVICESand 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.