0013 — Security hardening pass¶
Status: accepted (2026-07-23)
Context¶
A documentation-time review of the deployed NestJS backend and edge config surfaced several concrete issues. They were fixed together in one pass.
Decision¶
Applied and deployed (customer + admin API, Caddy):
- Webhook HMACs verify raw bytes. The Fastify app now captures the raw
request body (
rawBody: true), so Razorpay, RazorpayX, and KYC webhook signature checks hash the exact bytes the provider signed instead of a re-serialized JSON body. Files:apps/api/src/main.ts, the three webhook controllers,kyc.service.ts. - Bank-account penny-drop is owner-scoped.
PATCH /v1/bank-accounts/:id/verifypreviously looked the account up by id only (an IDOR); it now scopes to the caller. Files:modules/payouts/bank-accounts.{controller,service}.ts. - Payout retry respects the approval cap.
POST /v1/payouts/:id/retryre-approves as the retrying admin's role, not a forcedSUPER_ADMIN, so anADMINcan no longer exceed the ₹1L cap by failing then retrying. Files:apps/admin-api/.../payouts.{controller,service}.ts. - Rate limiting is enforced.
ThrottlerGuardis now registered as a global guard (the module was configured at 100 req/60 s but no guard was wired). Provider webhooks,/health, and/metricscarry@SkipThrottleso callbacks/scrapes are never dropped (an adversarial review caught that@Publicskips auth but not throttling).trustProxyis set to one hop so the rate-limit key is the real client IP Caddy appends, not a spoofableX-Forwarded-For. Files:apps/api/src/app.module.ts,apps/api/src/main.ts, the webhook/health/metrics controllers. - Admin API CORS defaults closed. When
ADMIN_PORTAL_ORIGINis unset the admin API no longer reflects any origin — the portal calls it server-side, so browser cross-origin access is not needed.ADMIN_PORTAL_ORIGINis now ingen-env.sh. File:apps/admin-api/src/main.ts. /metricsis not publicly reachable. Caddy returns 404 for/v1/metricsand/metricson the API hosts (usinghandleblocks, since a barerespondsorts afterreverse_proxy); Prometheus still scrapes the endpoints container-to-container onaim_edge. File:deploy/Caddyfile.
Merged via branch security/externalize-provider-credentials (PR #1,
merged 2026-07-23) and live in production:
- Provider credentials removed from source. The alots.in SMS and SafeGold
API keys were hardcoded as fallback defaults in two places —
libs/config/src/app.config.tsandlibs/common/src/sms/sms.service.ts(and thus in git history). Both are now env-only and required in production —libs/config/src/validation.schema.tsfails boot ifSAFEGOLD_API_KEY,SAFEGOLD_GOLD_TOKEN,SMS_USERNAME, orSMS_APIKEYis missing. The values now live only in/opt/aim/deploy/.env.production. Still open: the old values are in git history and must be rotated at the providers (tracked as an operational follow-up, not a code change).
Consequences¶
- Webhook verification is now correct against real provider payloads.
- The two access-control bugs (IDOR, cap bypass) are closed.
- Rate limiting adds a
429path; clients must handle it (the app/website already do single-flight refresh on401, not429, so a burst returns429to the caller). - Metrics are no longer information-disclosed publicly.
- Item 7 requires an operational step (rotation) that only the account owners can perform; until then the compromised keys remain valid at the providers.
- Not addressed here (tracked separately): purging the leaked keys from git history, and the website's client-side-only MPIN unlock (see the open question in Features).