Technical design — MOD-104 AWS shared infrastructure bootstrap¶
Module: MOD-104 — AWS shared infrastructure bootstrap
System: SD07 — Data Platform & Governance Infrastructure
Repo: bank-platform
FR scope: FR-417, FR-418, FR-419, FR-420
Policies satisfied: GOV-005 (GATE), GOV-006 (LOG), DT-002 (GATE)
Author: AI agent (Claude Opus 4.7)
Date: 2026-04-21
Stage covered: dev (account 647751526084, region ap-southeast-2, org o-1gqwjyxxhw)
SST permalink: https://sst.dev/u/73920289
Objective¶
MOD-104 provisions the foundational AWS infrastructure every other module across the 8 system
domains depends on. It is the bottom of the dependency tree. The module is deployed once per
environment via sst deploy and must be fully deployed before any other module.
This document describes the as-deployed state in dev. The implementation conforms to the
wiki spec and all 98 integration tests pass. build_status: Built in the wiki.
Execution model¶
| Aspect | Decision |
|---|---|
| IaC tool | SST v3 Ion + raw @pulumi/aws resources (ADR-025) |
| State management | SST Ion home: "aws" — auto-bootstrap via SSM /sst/bootstrap |
| Environments | local | dev | uat | prod (environments-and-deployment) |
| Tenant model | Single-tenant. Each customer gets a complete new stack. tenant_id=totara-bank for this deployment. |
| Region | Primary ap-southeast-2, secondary ap-southeast-4 (ADR-023) |
| Tagging | Provider defaultTags applies tenant_id, module_id, environment, plus system_id, cost_center, managed_by |
| Deployment identity | GitHub OIDC CI/CD role; dev work via AWS_PROFILE=bank-dev |
Stack layout¶
MOD-104-bootstrap/
├── sst.config.ts — app() config + async run() orchestration
├── src/
│ ├── config/
│ │ ├── stages.ts — Environment = local | dev | uat | prod
│ │ ├── tags.ts — baseTags(moduleId, env)
│ │ ├── regions.ts — primary/secondary + AZs
│ │ └── naming.ts — SystemDomain, DataClassification, name helpers
│ ├── stacks/
│ │ ├── networking.ts — VPC, 6 subnets, IGW, RTs, S3/DDB gateway endpoints
│ │ ├── kms.ts — 3 classification CMKs + aliases + policies
│ │ ├── storage.ts — 6 S3 buckets with SSE-KMS, SSL-only, lifecycle
│ │ ├── eventbridge.ts — 8 domain buses + cross-acct policy + archive + DLQ
│ │ ├── cognito.ts — 2 user pools + 4 app clients + custom attrs
│ │ ├── firehose.ts — 2 delivery streams (CDC + usage) to S3 iceberg
│ │ ├── cloudtrail.ts — trail + log-bucket immutability + CW Logs
│ │ ├── lambda-roles.ts — 8 system-domain Lambda execution roles
│ │ ├── iam.ts — CI/CD role (OIDC), break-glass role (MFA)
│ │ ├── secrets.ts — bank-secrets-read attachable policy
│ │ ├── observability.ts — shared log group, alerts SNS, X-Ray sampling
│ │ └── tag-enforcement.ts — SCP via assume-role to org management account
│ └── outputs.ts — ~45 SSM parameters under /bank/{env}/...
└── __tests__/integration/ — 7 FR+policy test files, Jest + AWS SDK v3
AWS resources provisioned (dev stage)¶
Networking — VPC and endpoints¶
| Resource | ID / ARN |
|---|---|
VPC (10.40.0.0/16) |
vpc-090237e1cdec5e3bc |
| Internet Gateway | igw-068ad8d85d8e23fdb |
| Public subnets ×3 | subnet-08907ebcf008a1702, -08523bc96f49b7ac7, -090d7605be0708298 |
| Private subnets ×3 | subnet-04eeb916c183c86c0, -0e4afe5a479b12005, -08e0e30275673f360 |
| S3 gateway endpoint | vpce-01031da9d5ae9f32b |
| DynamoDB gateway endpoint | vpce-01d1491aae464eb7a |
No NAT gateway. Private subnets reach AWS-native storage via gateway endpoints (free); when a Lambda needs other AWS APIs from a VPC attachment, that module adds interface endpoints.
KMS — three classification-aligned CMKs (FR-419)¶
| Classification | Alias | Rotation | Key policy |
|---|---|---|---|
| PII | alias/bank/pii |
annual | root account |
| Financial | alias/bank/financial |
annual | root account |
| Operational | alias/bank/operational |
annual | root account + CloudTrail + CloudWatch Logs + SNS service principals |
ARNs are published to SSM at /bank/{env}/kms/{classification}/arn.
S3 — six buckets (FR-418)¶
| Bucket | Purpose | CMK | Lifecycle |
|---|---|---|---|
bank-iceberg-dev |
Iceberg data lake / CDC output | financial | Glacier @ 90d · expire @ 7y |
bank-firehose-landing-dev |
Firehose landing zone | operational | expire @ 1d (ephemeral) |
bank-documents-dev |
Customer documents (KYC scans, statements) | pii | Glacier @ 1y · expire @ 7y |
bank-artefacts-dev |
Build/deploy artefacts | operational | expire @ 90d |
bank-reports-dev |
Regulatory report output | financial | Glacier @ 1y · expire @ 7y |
bank-cloudtrail-dev |
CloudTrail logs | operational | Glacier @ 90d · expire @ 7y · Object Lock (GOVERNANCE, 7y) |
bank-pulumi-state-dev |
Pulumi state backend (non-SST modules — bank-risk-platform's bare-Pulumi runner per ADR-025) | operational | Versioning ON · no expiration (state must persist for stack lifetime) |
All buckets: SSE-KMS, versioning, public-access-block (all 4 flags), SSL-only Deny policy, DenyUnencryptedPut policy, DenyIncorrectEncryption policy.
EventBridge — eight system-domain buses (FR-417)¶
Bus names (no env suffix, per spec): bank-core, bank-kyc, bank-aml, bank-payments, bank-credit, bank-risk-platform, bank-platform, bank-app. Each bus has:
- Cross-account resource policy (root account + org aws:PrincipalOrgID=o-1gqwjyxxhw)
- 30-day event archive (EventArchive)
- Dead-letter SQS queue ({domain}-events-dlq-{env}, KMS-encrypted, 14-day retention)
Cognito — two user pools (FR-420)¶
| Pool | MFA | Custom attrs | App clients |
|---|---|---|---|
bank-customers-dev |
TOTP required (biometric via app layer) | custom:user_id, custom:party_id, custom:jurisdiction |
mobile (PKCE, public), web (PKCE, public) |
bank-staff-dev |
TOTP required | custom:staff_id |
back-office (confidential), api-authoriser (confidential) |
Password policy: min length 12 (customers) / 14 (staff), all character classes required. Deletion protection: INACTIVE in dev, ACTIVE in prod.
Kinesis Firehose — two delivery streams (FR-420)¶
| Stream | Destination | Partitioning | Buffer |
|---|---|---|---|
bank-cdc-stream |
bank-iceberg-dev under cdc/source=neon/year=.../month=.../day=.../ |
time-based | 128 MB / 300 s |
bank-usage-events |
bank-iceberg-dev under usage/year=.../month=.../day=.../ |
time-based | 128 MB / 300 s |
Both: SSE with alias/bank/financial, GZIP compression, CloudWatch error logging, Firehose DLQ.
Lambda execution IAM roles — eight system-domain roles (FR-420)¶
Roles BankCoreRole, BankKycRole, BankAmlRole, BankPaymentsRole, BankCreditRole, BankRiskRole, BankPlatformRole, BankAppRole — each with Lambda trust (scoped to this account), AWSLambdaBasicExecutionRole + AWSLambdaVPCAccessExecutionRole managed-policy attachments, and a domain-permissions inline policy covering:
- CloudWatch Logs on /aws/lambda/{domain}-*
- Secrets Manager read on {domain}/* (and bank-neon/*/{domain_db}/* where neonDbName is set)
- SSM read (GetParameter/GetParameters/GetParametersByPath) on /bank/* — every domain Lambda resolves shared infra discovery (Neon pooler-host, S3 artefacts bucket, EventBridge bus ARNs, Snowflake DB names) at cold start. Added 2026-05-04 after bank-aml's cold-start crash on /bank/dev/neon/pooler-host. Asserted by fr-420-… test.
- EventBridge PutEvents on the domain's bus
- SNS Publish on bank-* topics — universal alerting pattern (ADR-031). MOD-076 publishes the alarm-intake topic; per-domain modules also stand up service-level alert topics. Added 2026-05-04 after the broader IAM audit found BankKycRole (5 modules: MOD-010/011/013/014/015) and BankRiskRole (MOD-085) missing this grant.
- KMS Decrypt on the classification CMK(s) appropriate to the domain (e.g. BankKycRole → pii+financial)
- X-Ray PutTraceSegments
BankAmlRole has additional appconfigdata:GetLatestConfiguration + appconfig:StartConfigurationSession for ADR-033 live-rule-parameter loading (MOD-016 typology engine, MOD-018 SAR threshold loader). Its cross-bus EventBridge grant (events:PutRule/PutTargets/DescribeRule/ListTargetsByRule on bank-core's bus, scoped to bank-aml-mod-* rule names) is intentionally narrowed — DeleteRule / RemoveTargets were removed 2026-05-04 because handler code only provisions rules, never tears them down at runtime; teardown happens via SST/Pulumi at deploy time using the CICD role.
BankPaymentsRole grants lambda:InvokeFunction on arn:aws:lambda:*:{accountId}:function:bank-core-mod-004-*FxConversionHandler* (FR-142 — MOD-025 ConfirmConversionHandler synchronously invokes MOD-004 to confirm an FX rate before persisting the payment leg). The wildcards on either side of FxConversionHandler accommodate SST's CDK-style suffixed function names.
Trust has aws:SourceAccount condition and aws:RequestTag/module_id condition (modules attaching Lambdas to these roles must tag with module_id).
CloudTrail — multi-region trail (FR-420, GOV-006)¶
| Property | Value |
|---|---|
| Trail name | bank-platform |
| Log bucket | bank-cloudtrail-dev (Object Lock GOVERNANCE, 7y default retention) |
| Multi-region | true |
| Include global service events | true |
| Log-file validation | enabled |
| KMS | alias/bank/operational |
| CloudWatch Logs integration | /aws/cloudtrail/bank-platform-dev |
| Management events | read + write |
| Data events | all S3 objects |
Log bucket policy includes explicit Deny on DeleteObject/DeleteObjectVersion (belt + braces alongside Object Lock GOVERNANCE).
Observability¶
| Resource | Value |
|---|---|
| CloudWatch log group | /aws/bank-platform/dev |
| Alerts SNS topic | bank-platform-alerts-dev |
| X-Ray sampling rule | bank-platform-default-dev (5% fixed, reservoir 1) |
Platform IAM (CI/CD + break-glass)¶
| Role | Trust | Permissions |
|---|---|---|
bank-platform-cicd |
GitHub OIDC (repo:totara-bank/bank-platform:*) |
AdministratorAccess |
bank-platform-break-glass |
root account + MFA | AdministratorAccess, 1h max session |
Secrets — attachable namespace read policy¶
| Policy | ARN pattern | Grants |
|---|---|---|
bank-secrets-read |
arn:aws:iam::647751526084:policy/bank-secrets-read |
GetSecretValue + DescribeSecret on bank-*/* secrets; Decrypt on all three classification CMKs |
Tag enforcement — SCP (GOV-005 GATE)¶
Provisioned via a second Pulumi provider configured with the bank-org management-account profile (via BANK_ORG_PROFILE=bank-org env var). SCP bank-platform-required-tags (policy id p-pyh0z523) attached to the organization root r-2qho. Policy denies Create* on 7 taggable services (S3/EC2/KMS/Events/IAM/Lambda/CloudTrail) unless tenant_id, module_id, and environment request tags are present.
Deploy command: AWS_PROFILE=bank-dev BANK_ORG_PROFILE=bank-org pnpm sst deploy --stage <env>
One-time org precondition: aws organizations enable-policy-type --root-id r-2qho --policy-type SERVICE_CONTROL_POLICY (idempotent after first run).
The GOV-005 negative test now passes — a direct-SDK CreateBucket without the required tags returns AccessDenied before the request reaches S3.
SSM outputs table (consumer contract)¶
All under arn:aws:ssm:ap-southeast-2:647751526084:parameter. Path convention per spec:
/bank/{env}/{service}/{parameter}. Downstream modules resolve these at deploy time.
| SSM path | Value | Consumed by |
|---|---|---|
/bank/{env}/network/vpc-id |
VPC ID | MOD-103 (Neon), MOD-075 (API gateway), any VPC-attached Lambda |
/bank/{env}/network/private-subnet-ids |
CSV of 3 subnet IDs | MOD-103, MOD-075, MOD-076, MOD-097 |
/bank/{env}/network/public-subnet-ids |
CSV of 3 subnet IDs | MOD-075 (ALB), MOD-100 (external connector) |
/bank/{env}/kms/pii/arn |
KMS ARN | bank-kyc (MOD-009), bank-app (MOD-044 JWT), MOD-103 PII secrets |
/bank/{env}/kms/financial/arn |
KMS ARN | bank-core (MOD-001 ledger), MOD-042 CDC pipeline, Snowflake ingest |
/bank/{env}/kms/operational/arn |
KMS ARN | CloudTrail, CloudWatch Logs, Firehose landing, misc ops |
/bank/{env}/eventbridge/bank-core/arn |
Bus ARN | bank-core producers/consumers |
/bank/{env}/eventbridge/bank-kyc/arn |
Bus ARN | bank-kyc producers/consumers |
/bank/{env}/eventbridge/bank-aml/arn |
Bus ARN | bank-aml producers/consumers |
/bank/{env}/eventbridge/bank-payments/arn |
Bus ARN | bank-payments producers/consumers |
/bank/{env}/eventbridge/bank-credit/arn |
Bus ARN | bank-credit producers/consumers |
/bank/{env}/eventbridge/bank-risk-platform/arn |
Bus ARN | bank-risk-platform producers/consumers |
/bank/{env}/eventbridge/bank-platform/arn |
Bus ARN | CDC (MOD-042), usage (MOD-097), notifications (MOD-063) |
/bank/{env}/eventbridge/bank-app/arn |
Bus ARN | bank-app producers/consumers |
/bank/{env}/eventbridge/{domain}/dlq-arn |
SQS ARN | Rule DeadLetterConfig target for each domain |
/bank/{env}/eventbridge/{domain}/archive-arn |
Archive ARN | Replay operations for each domain |
/bank/{env}/s3/iceberg/name |
Bucket name | MOD-042 (CDC output), MOD-102 (Snowflake ingest), Firehose |
/bank/{env}/s3/iceberg/arn |
Bucket ARN | Bucket-ARN-scoped IAM policies in downstream modules |
/bank/{env}/s3/firehose-landing/name |
Bucket name | Firehose error output |
/bank/{env}/s3/documents/name |
Bucket name | bank-kyc (MOD-009 KYC scans), bank-app (statements) |
/bank/{env}/s3/artefacts/name |
Bucket name | All 8 repos' CI/CD deploy intermediates |
/bank/{env}/s3/reports/name |
Bucket name | MOD-099 usage/billing reports, regulatory reporting |
/bank/{env}/s3/cloudtrail/name |
Bucket name | Ops read (immutable logs) |
/bank/{env}/s3/pulumi-state/name |
Bucket name | bank-risk-platform pulumi login s3://...; any non-SST Pulumi state backend |
/bank/{env}/s3/pulumi-state/arn |
Bucket ARN | IAM policies scoping deploy roles to the state bucket |
/bank/{env}/iam/lambda/{domain}/arn |
Role ARN (×8) | Each domain's Lambda handlers attach to their domain role |
/bank/{env}/iam/cicd/arn |
Role ARN | All 8 repos' GitHub Actions |
/bank/{env}/iam/break-glass/arn |
Role ARN | Ops incident response only |
/bank/{env}/iam/secrets-read/arn |
Policy ARN | Attach to any service role needing bank-*/* secrets |
/bank/{env}/cognito/customers/pool-id |
Pool ID | bank-app (MOD-044 JWT issuer), MOD-022 onboarding |
/bank/{env}/cognito/customers/arn |
Pool ARN | bank-app IAM policies |
/bank/{env}/cognito/customers/client/{client-name} |
Client ID | Mobile/web frontends (client-name ∈ {mobile, web}) |
/bank/{env}/cognito/staff/pool-id |
Pool ID | Back-office auth |
/bank/{env}/cognito/staff/arn |
Pool ARN | Back-office IAM policies |
/bank/{env}/cognito/staff/client/{client-name} |
Client ID | Back-office web, API Gateway authoriser (client-name ∈ {back-office, api-authoriser}) |
/bank/{env}/firehose/cdc/arn |
Stream ARN | MOD-042 CDC producer |
/bank/{env}/firehose/usage/arn |
Stream ARN | MOD-097 usage producer |
/bank/{env}/cloudtrail/arn |
Trail ARN | Ops monitoring |
/bank/{env}/cloudtrail/log-group-arn |
Log group ARN | CloudWatch Logs integration |
/bank/{env}/logs/bank-platform/arn |
Log group ARN | Cross-module infra log events |
/bank/{env}/sns/alerts/arn |
Topic ARN | MOD-076 alarm targets |
/bank/{env}/xray/sampling/arn |
Sampling rule ARN | All bank-platform Lambdas default sampling |
Resolution example¶
// In downstream module (Pulumi)
const piiCmkArn = aws.ssm.getParameterOutput({
name: `/bank/${stage}/kms/pii/arn`,
}).value;
// In Lambda runtime (AWS SDK v3)
const ssm = new SSMClient({ region });
const { Parameter } = await ssm.send(
new GetParameterCommand({ Name: `/bank/${stage}/s3/documents/name` }),
);
const bucketName = Parameter?.Value;
Acceptance criteria status (dev stage, 2026-04-21)¶
Run: AWS_PROFILE=bank-dev STAGE=dev pnpm test
| FR / Policy | Mode | Tests | Pass | Fail | Status |
|---|---|---|---|---|---|
| FR-417 — 8 EventBridge buses + cross-acct policy + 30d archive + DLQ | — | 32 | 32 | 0 | PASS |
| FR-418 — 5 named S3 buckets + KMS by classification + SSL-only + lifecycle | — | 36 | 36 | 0 | PASS |
| FR-419 — 3 classification CMKs + rotation + SSM export | — | 9 | 9 | 0 | PASS |
| FR-420 — Cognito + Firehose + 8 IAM roles + CloudTrail | — | 14 | 14 | 0 | PASS |
| GOV-005 — untagged resources blocked | GATE | 4 | 4 | 0 | PASS |
| GOV-006 — CloudTrail enabled + logs immutable | LOG | 4 | 4 | 0 | PASS |
| DT-002 — CMKs per classification + DenyUnencryptedPut | GATE | 4 | 4 | 0 | PASS |
| Total | 98 | 98 | 0 | 100% |
All quality gates for IaC modules met: one infrastructure integration test per FR, one policy satisfaction test per row, GATE modes with negative tests, LOG mode with immutability test, SSM outputs table present and accurate.
Test approach¶
Jest + AWS SDK v3 integration tests in MOD-104-bootstrap/__tests__/integration/. No mocks; every
assertion queries live AWS in the dev stage.
| File | Coverage |
|---|---|
fr-417-eventbridge-buses.test.ts |
Bus + cross-account policy + archive + DLQ per domain |
fr-418-s3-buckets.test.ts |
Existence, KMS-SSE, versioning, PAB, SSL-only, lifecycle per bucket |
fr-419-kms-classifications.test.ts |
Alias presence, rotation, SSM export per classification |
fr-420-cognito-firehose-iam-cloudtrail.test.ts |
Cognito pools + attrs, Firehose streams, Lambda roles, CloudTrail |
gov-005-tag-enforcement.test.ts |
Positive: spec tag keys present. Negative: untagged CreateBucket rejected |
gov-006-cloudtrail-immutability.test.ts |
Trail + logging + log-bucket immutability + DeleteObject probe |
dt-002-encryption-at-rest.test.ts |
Classification CMKs exist + unencrypted PUT rejected |
Run: AWS_PROFILE=bank-dev STAGE=dev pnpm test from MOD-104-bootstrap/.
Operational notes¶
- Deploy (full, including SCP):
AWS_PROFILE=bank-dev BANK_ORG_PROFILE=bank-org pnpm sst deploy --stage <env> - Deploy (skip SCP, e.g. from CI without mgmt-account profile):
AWS_PROFILE=bank-dev pnpm sst deploy --stage <env>— all member-account resources deploy; SCP stack is skipped - Remove:
AWS_PROFILE=bank-dev pnpm sst remove --stage <env>— KMS keys enter 30-day pending deletion, S3 buckets with versioning need manual version purge, CloudTrail log bucket has Object Lock (use--bypass-governance-retentionto purge) - One-time org precondition (idempotent):
AWS_PROFILE=bank-org aws organizations enable-policy-type --root-id r-2qho --policy-type SERVICE_CONTROL_POLICY - SST permalink (latest dev deploy): https://sst.dev/u/73920289
Related artefacts¶
- Wiki spec:
bank-wiki/source/entities/modules/MOD-104.{yaml,md} - Wiki outputs contract:
bank-wiki/source/entities/modules/MOD-104.yaml→outputs:section - Handoff:
docs/handoffs/MOD-104-complete.handoff.md - Bootstrap README:
MOD-104-bootstrap/README.md - Methodology: https://bank-wiki.pages.dev/delivery/methodology/
- Environments & deployment: https://bank-wiki.pages.dev/architecture/environments-and-deployment/
- ADRs in effect: ADR-023, ADR-025, ADR-026, ADR-027, ADR-029, ADR-030, ADR-031, ADR-035, ADR-040, ADR-042