f9f5f171eb18cfd3a52f5e03d488754dc3818ba5
## Summary
Fix `ERR_ERL_KEY_GEN_IPV6` raised by `express-rate-limit` at BFF boot: the rate-limit middleware's custom `keyGenerator` was using `req.ip` verbatim, which the library v8 refuses because it lets an IPv6 attacker rotate through the host bits of their own subnet to escape per-IP rate-limiting. Surfaced during the ADR-0030 dockerised dev mode validation (the boot log shows the `ValidationError` immediately after the rate-limit middleware is built).
## Root cause
`apps/portal-bff/src/security/rate-limit.middleware.ts` returned:
```ts
return `ip:${req.ip ?? 'unknown'}`;
```
`req.ip` is the raw address the IP-trust chain hands Express. For IPv4 that's already the right bucket key. For IPv6, every distinct host in an attacker's allocation hashes to a different bucket — even though the same human controls all of them. An attacker on a residential IPv6 assignment (typically `/56`) thus has ~2^72 trivially-rotatable buckets per `/56`, which makes the per-IP rate limit useless against them.
`express-rate-limit` v7+ ships an `ipKeyGenerator` helper that **truncates the address to its `/56` prefix** before keying. The library v8 raises `ERR_ERL_KEY_GEN_IPV6` at boot if a custom `keyGenerator` returns `req.ip` verbatim, precisely to refuse shipping this bypass.
## Fix
| File | Change |
| --- | --- |
| `apps/portal-bff/src/security/rate-limit.middleware.ts` | Import `ipKeyGenerator` from `express-rate-limit`; wrap `req.ip` through it when keying. IPv4 addresses pass through unchanged; IPv6 addresses get truncated to their `/56`. Comment explains the rationale + that the lib's `/56` default matches a typical residential ISP customer allocation. |
| `apps/portal-bff/src/security/rate-limit.middleware.spec.ts` | New test asserting two IPv6 addresses in the same `/56` share a bucket (the bypass would have set both apart), and that distinct `/56`s remain isolated (truncation does not collapse all IPv6 traffic into one global bucket). Existing IPv4 / session / `SKIP_PATHS` tests are unchanged and still pass — `ipKeyGenerator` is a no-op for IPv4. |
The session-keyed branch (`s:${sessionID}`) is untouched: sessions key on the BFF-issued session id, not the address.
## Why the BFF kept booting despite the error
The log shows `bootstrap` reaching `AuthModule` immediately after the `ValidationError` printout. `express-rate-limit` v8's `errorHandler` defaults to logging the error and continuing rather than throwing for this specific validation, so the middleware was effectively running with the unfixed `keyGenerator` until now — i.e. the bypass was live in the dev BFF. Fixed pre-emptively, before any prod consumer.
## Related
- Per [ADR-0021](docs/decisions/0021-phase-2-security-baseline.md) — Phase-2 security baseline, rate-limit section.
- Surfaced by the ADR-0030 dockerised dev mode validation (the SPA `/api/auth/me` proxy errors visible in the same log run were a side effect of the BFF restarting on every config validator until the env was fully populated; unrelated to this fix).
## Test plan
- [x] `pnpm exec nx test portal-bff --testFile=apps/portal-bff/src/security/rate-limit.middleware.spec.ts` — 794 tests pass, including the new `/56` isolation case.
- [ ] Restart the BFF (`./infra/local/dev.sh restart portal-bff`) — `ERR_ERL_KEY_GEN_IPV6` no longer appears in the boot log.
- [ ] IPv4 traffic still rate-limits as before (existing test coverage; no behavioural change since `ipKeyGenerator` is identity for v4).
---------
Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr>
Reviewed-on: #260
Documentation index
This is the entry point to all project documentation. It is maintained automatically: any addition, rename, or removal of a .md file under docs/ must be reflected here in the same change.
Conventions
- Documentation is written in English.
- One topic per file. Group related files into a folder when there are three or more.
- Cross-reference with relative links so they keep working in GitHub, IDEs, and exported sites.
- For architectural decisions, do not add them here — they belong in decisions/ as MADR 4.0.0 ADRs.
Sections
Daily development
- development.md — repo layout, prerequisites, initial setup, daily commands, observability dev-loop (Jaeger UI, log ↔ trace correlation), dependency updates (Renovate), conventional commit cycle. Day-to-day reference for working on the project.
Architecture
- architecture.md — cross-cutting Mermaid diagrams: C4 system context, C4 containers, Nx module boundaries, CI/CD pipeline. Single-decision diagrams (auth sequence, ERD, etc.) live inline in their ADR.
Onboarding & environment
Setup guides for new contributors:
- setup/01-wsl-terminal-setup.md — modern WSL terminal (Zsh + Powerlevel10k + CLI tools)
- setup/02-dev-web-stack.md — Node via nvm, pnpm via corepack, Docker
- setup/03-angular-nx-monorepo.md — Angular + Nx monorepo bootstrap
Operations & runbooks
Empty — to be populated when we deploy.
Security, performance, accessibility
Empty — placeholders to be filled with rationale docs alongside their corresponding ADRs.
Description
Languages
TypeScript
85.3%
JavaScript
5.4%
SCSS
4.3%
HTML
3.9%
Shell
0.8%
Other
0.3%