From 01612753cf303f7648123a72497d83ef7012a903 Mon Sep 17 00:00:00 2001 From: Dani Herrero Date: Tue, 9 Jun 2026 22:42:06 +0200 Subject: [PATCH] =?UTF-8?q?docs:=20fix=20network=20topology=20=E2=80=94=20?= =?UTF-8?q?nginx=20is=20the=20only=20public=20door,=20gateway=20is=20priva?= =?UTF-8?q?te?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The architecture diagrams described "Client → Gateway (public) → API (private)", omitting nginx and wrongly painting the gateway as the public entrypoint. docs/SECURITY.md even had the (public)/(private) labels reversed. The real topology (source of truth: production compose) is: Browser → Nginx (front) → Gateway → API → PostgreSQL, where only the `front` container is exposed and gateway/api/postgres all live on `internal-network` (`internal: true`) with no inbound from the Internet. - Rewrite the three architecture diagrams in Mermaid (renders natively on GitHub) in README.md, docs/README_eng.md and docs/SECURITY.md, fixing the reversed public/private labels and inserting nginx as the only public door. - Align prose in README.md, docs/README_eng.md and apps/gateway/AGENTS.md: the gateway is private (internal-network), behind nginx — not "the only service exposed to the Internet". - compose.yaml: move gateway to internal-network only (drop edge-network and its published port); put front on edge-network + internal-network so it can reach the gateway. Verified with `docker compose config`. Co-Authored-By: Claude Opus 4.8 --- README.md | 45 +++++++++++++++++++++++++++++------------- apps/gateway/AGENTS.md | 16 ++++++++------- compose.yaml | 4 +--- docs/README_eng.md | 45 +++++++++++++++++++++++++++++------------- docs/SECURITY.md | 32 ++++++++++++++++++++++++------ 5 files changed, 98 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index c0df0e3..5a1d17a 100644 --- a/README.md +++ b/README.md @@ -17,18 +17,19 @@ Monorepo Nx listo para producción que incluye autenticación JWT con rotación ### 🎯 **Stack Tecnológico** - **Frontend**: Angular 21 con standalone components, Signals API y control flow nativo (`@if` / `@for` / `@switch`) -- **Gateway**: Express 5 + `http-proxy-middleware` — único servicio expuesto a Internet, gestiona tokens y CORS -- **API**: Express 5 + Sequelize, vive en red privada, expone CRUDs y endpoints `/internal/*` para el gateway +- **Nginx (front)**: sirve la SPA y es la puerta pública; hace reverse-proxy de `/api/*` al gateway (mismo origen) +- **Gateway**: Express 5 + `http-proxy-middleware` — privado (`internal-network`), detrás de nginx; gestiona tokens y CORS +- **API**: Express 5 + Sequelize, privado (`internal-network`), expone CRUDs y endpoints `/internal/*` para el gateway - **Base de datos**: PostgreSQL 16 - **Monorepo**: Nx 22 para gestión eficiente - **Build System**: esbuild (backend) + Vite (frontend) -- **Containerización**: Docker + Docker Compose con redes `edge-network` e `internal-network` +- **Containerización**: Docker + Docker Compose; sólo `front` (nginx) se expone, el resto vive en `internal-network` (`internal: true`) - **UI**: Bootstrap 5 + NgBootstrap - **i18n**: Transloco (Español/Valenciano/Inglés) ### 🔐 **Autenticación & Seguridad** — ver [`docs/SECURITY.md`](docs/SECURITY.md) -- Arquitectura de microservicios: **gateway** público + **api** privado. El api nunca habla con el cliente directamente; el gateway firma un JWT interno EdDSA antes de proxiar +- Arquitectura de microservicios: **nginx** (front) como puerta pública → **gateway** (auth) → **api** privado. El api nunca habla con el cliente directamente; el gateway firma un JWT interno EdDSA antes de proxiar - JWT del cliente con **dos secretos separados** (`JWT_ACCESS_SECRET`, `JWT_REFRESH_SECRET`), claims `typ` y `jti` - **Rotación de refresh con detección de reuso**: tabla `refresh_token_family` revoca la familia completa si una cookie ya rotada se vuelve a presentar - **JWT interno Ed25519** entre gateway y api: el gateway tiene la clave privada (firma), el api sólo la pública (verifica). Privilegio separado: un api comprometido no puede emitir tokens @@ -47,14 +48,30 @@ Monorepo Nx listo para producción que incluye autenticación JWT con rotación ### 🏗️ **Arquitectura** -``` -┌──────────┐ cookie+Authorization ┌──────────┐ X-Internal-Auth (EdDSA) ┌─────┐ -│ Cliente │ ─────────────────────────▶│ Gateway │ ──────────────────────────▶ │ API │ -└──────────┘ └──────────┘ └─────┘ - (público) (privada) - :3100 :3200 +```mermaid +flowchart LR + Browser(["🌐 Navegador"]) + + subgraph priv ["🔒 internal-network · internal: true (sin salida a Internet)"] + direction LR + Nginx["Nginx · front
SPA Angular + proxy /api/*"] + Gateway["Gateway · :3100
auth JWT cliente
firma JWT interno EdDSA"] + API["API · :3200
Express 5 + Sequelize"] + DB[("PostgreSQL 16")] + + Nginx -->|"proxy_pass /api/"| Gateway + Gateway -->|"X-Internal-Auth · EdDSA"| API + API --> DB + end + + Browser ==>|"HTTPS · cookie + Authorization
única puerta pública"| Nginx + + classDef public fill:#1f6feb,stroke:#0b3d91,color:#fff; + class Nginx public; ``` +- **Nginx (contenedor `front`)** es la **única puerta pública**: sirve la SPA y hace reverse-proxy de `/api/*` al gateway (mismo origen → las cookies viajan sin CORS). El navegador nunca habla con el gateway directamente. +- **Gateway**, **API** y **PostgreSQL** viven todos en `internal-network` (`internal: true`), sin entrada desde Internet. El gateway autentica el JWT del cliente, firma el JWT interno EdDSA y proxia al api privado. - Patrón Controller-Service-Repository en el api - DTOs compartidos entre frontend y backend en `libs/rest-dto` - Contrato interno gateway↔api en `libs/internal-auth` (Ed25519 + scopes) @@ -95,7 +112,7 @@ npm run dev ### Acceso a la Aplicación - **Frontend**: http://localhost:4200 -- **Gateway (público)**: http://localhost:3100/api/v1/ +- **Gateway (API del cliente)**: http://localhost:3100/api/v1/ (en dev el front la consume vía proxy de Vite; en docker, tras nginx) - **API (privado)**: http://localhost:3200 (sólo accesible vía gateway en docker) - **Base de datos**: localhost:5432 @@ -170,7 +187,7 @@ nx-fullstack-starter/ │ │ │ ├── libs/auth/ # Módulo de autenticación (service, guards) │ │ │ └── services/ # Servicios de negocio │ │ └── src/assets/i18n/ # Archivos de traducción -│ ├── gateway/ # Servicio público (Express + http-proxy-middleware) +│ ├── gateway/ # Servicio de auth + proxy, privado tras nginx (Express + http-proxy-middleware) │ │ └── src/ │ │ ├── controllers/ # auth.controller (login/logout) │ │ ├── middleware/ # hasPermission, refresh rotation @@ -245,7 +262,7 @@ Especialista en diseño de esquemas PostgreSQL y MongoDB, migraciones sin downti #### 🔧 Backend Developer -Especialista en Express + Sequelize siguiendo arquitectura de 4 capas: Routes → Controllers → Services → Models. Trabaja sobre `apps/api` (lógica de negocio) y `apps/gateway` (auth público, proxy). +Especialista en Express + Sequelize siguiendo arquitectura de 4 capas: Routes → Controllers → Services → Models. Trabaja sobre `apps/api` (lógica de negocio) y `apps/gateway` (auth de cliente, proxy). - Patrones `AbstractCrudService` / `AbstractCrudController` para minimizar boilerplate - Todas las respuestas HTTP a través de `HttpResponser` (nunca `res.json()` directo) @@ -419,7 +436,7 @@ npm run build docker compose --env-file .env up -d ``` -> Sólo `gateway` y `front` exponen puertos al exterior. `api` y `postgresdb` viven en `internal-network` con `internal: true`. +> Sólo `front` (nginx) se expone al exterior. `gateway`, `api` y `postgresdb` viven en `internal-network` con `internal: true` y no son accesibles desde Internet. ### 🌍 **Internacionalización** diff --git a/apps/gateway/AGENTS.md b/apps/gateway/AGENTS.md index 93e2f1d..bf89628 100644 --- a/apps/gateway/AGENTS.md +++ b/apps/gateway/AGENTS.md @@ -3,8 +3,10 @@ > Layer-specific rules for the gateway. See the root `AGENTS.md` for global > conventions and path aliases. -The gateway is the **only service exposed to the public**. It owns the public-facing -auth surface, then proxies authorized requests to the internal API. +The gateway owns the **client-facing auth surface**, but it is **not exposed to the +Internet**: it lives on `internal-network` (`internal: true`) and sits behind Nginx (the +`front` container reverse-proxies `/api/*` to it). It then proxies authorized requests to +the internal API on the same private network. ## Responsibilities @@ -21,11 +23,11 @@ auth surface, then proxies authorized requests to the internal API. ## Request flow (must stay intact) ``` -client ──access token──▶ gateway - │ hasPermission() → res.locals.user - │ signUserContext() → internal EdDSA JWT - ▼ - API /v1/* (verifies internal token, not the public one) +browser ──/api/*──▶ nginx ──proxy_pass──▶ gateway + │ hasPermission() → res.locals.user + │ signUserContext() → internal EdDSA JWT + ▼ + API /v1/* (verifies internal token, not the public one) ``` ## Hard rules diff --git a/compose.yaml b/compose.yaml index 768459d..ac45cb3 100644 --- a/compose.yaml +++ b/compose.yaml @@ -64,10 +64,7 @@ services: - INTERNAL_JWT_PRIVATE_KEY=${INTERNAL_JWT_PRIVATE_KEY} - CORS_ORIGIN=${CORS_ORIGIN} - SERVICE_FQDN_GATEWAY=${SERVICE_FQDN_GATEWAY} - ports: - - '${GATEWAY_PORT:-3100}' networks: - - edge-network - internal-network front: @@ -84,6 +81,7 @@ services: - '${FRONT_PORT:-80}' networks: - edge-network + - internal-network volumes: starter-postgresdb: diff --git a/docs/README_eng.md b/docs/README_eng.md index 86ffea3..3c30dd7 100644 --- a/docs/README_eng.md +++ b/docs/README_eng.md @@ -17,18 +17,19 @@ Production-ready Nx monorepo including JWT authentication with refresh rotation, ### 🎯 **Tech Stack** - **Frontend**: Angular 21 with standalone components, Signals API and native control flow (`@if` / `@for` / `@switch`) -- **Gateway**: Express 5 + `http-proxy-middleware` — the only service exposed to the Internet, handles tokens and CORS -- **API**: Express 5 + Sequelize, lives in a private network, exposes CRUDs and `/internal/*` endpoints used by the gateway +- **Nginx (front)**: serves the SPA and is the public door; reverse-proxies `/api/*` to the gateway (same origin) +- **Gateway**: Express 5 + `http-proxy-middleware` — private (`internal-network`), behind nginx; handles tokens and CORS +- **API**: Express 5 + Sequelize, private (`internal-network`), exposes CRUDs and `/internal/*` endpoints used by the gateway - **Database**: PostgreSQL 16 - **Monorepo**: Nx 22 for efficient management - **Build System**: esbuild (backend) + Vite (frontend) -- **Containerisation**: Docker + Docker Compose with split `edge-network` and `internal-network` +- **Containerisation**: Docker + Docker Compose; only `front` (nginx) is exposed, everything else lives on `internal-network` (`internal: true`) - **UI**: Bootstrap 5 + NgBootstrap - **i18n**: Transloco (Spanish / Valencian / English) ### 🔐 **Authentication & Security** — see [`SECURITY.md`](SECURITY.md) -- Microservices architecture: public **gateway** + private **api**. The api never talks directly to clients; the gateway signs a short-lived EdDSA internal JWT before proxying. +- Microservices architecture: **nginx** (front) as the public door → **gateway** (auth) → private **api**. The api never talks directly to clients; the gateway signs a short-lived EdDSA internal JWT before proxying. - Client JWTs with **two separate secrets** (`JWT_ACCESS_SECRET`, `JWT_REFRESH_SECRET`), plus `typ` and `jti` claims. - **Refresh rotation with reuse detection**: the `refresh_token_family` table revokes the whole family when an already-rotated cookie is replayed. - **Internal Ed25519 JWT** between gateway and api: the gateway holds the private key (signs), the api only the public key (verifies). Privilege separation: a compromised api cannot mint tokens. @@ -47,14 +48,30 @@ Production-ready Nx monorepo including JWT authentication with refresh rotation, ### 🏗️ **Architecture** -``` -┌──────────┐ cookie+Authorization ┌──────────┐ X-Internal-Auth (EdDSA) ┌─────┐ -│ Client │ ─────────────────────────▶│ Gateway │ ──────────────────────────▶ │ API │ -└──────────┘ └──────────┘ └─────┘ - (public) (private) - :3100 :3200 +```mermaid +flowchart LR + Browser(["🌐 Browser"]) + + subgraph priv ["🔒 internal-network · internal: true (no Internet access)"] + direction LR + Nginx["Nginx · front
Angular SPA + proxy /api/*"] + Gateway["Gateway · :3100
client JWT auth
signs internal EdDSA JWT"] + API["API · :3200
Express 5 + Sequelize"] + DB[("PostgreSQL 16")] + + Nginx -->|"proxy_pass /api/"| Gateway + Gateway -->|"X-Internal-Auth · EdDSA"| API + API --> DB + end + + Browser ==>|"HTTPS · cookie + Authorization
the only public door"| Nginx + + classDef public fill:#1f6feb,stroke:#0b3d91,color:#fff; + class Nginx public; ``` +- **Nginx (the `front` container)** is the **only public door**: it serves the SPA and reverse-proxies `/api/*` to the gateway (same origin → cookies travel without CORS). The browser never talks to the gateway directly. +- **Gateway**, **API** and **PostgreSQL** all live on `internal-network` (`internal: true`), with no inbound from the Internet. The gateway authenticates the client JWT, signs the internal EdDSA JWT and proxies to the private api. - Controller-Service-Repository pattern inside the api - Shared DTOs between frontend and backend in `libs/rest-dto` - Internal gateway↔api contract in `libs/internal-auth` (Ed25519 + scopes) @@ -94,7 +111,7 @@ npm run dev ### Application Access - **Frontend**: http://localhost:4200 -- **Gateway (public)**: http://localhost:3100/api/v1/ +- **Gateway (client API)**: http://localhost:3100/api/v1/ (in dev the front consumes it via the Vite proxy; under docker, behind nginx) - **API (private)**: http://localhost:3200 (only reachable via the gateway under docker) - **Database**: localhost:5432 @@ -169,7 +186,7 @@ nx-fullstack-starter/ │ │ │ ├── libs/auth/ # Authentication module (service, guards) │ │ │ └── services/ # Business services │ │ └── src/assets/i18n/ # Translation files -│ ├── gateway/ # Public service (Express + http-proxy-middleware) +│ ├── gateway/ # Auth + proxy service, private behind nginx (Express + http-proxy-middleware) │ │ └── src/ │ │ ├── controllers/ # auth.controller (login/logout) │ │ ├── middleware/ # hasPermission, refresh rotation @@ -244,7 +261,7 @@ Expert in PostgreSQL and MongoDB schema design, zero-downtime migrations, indexi #### 🔧 Backend Developer -Expert in Express + Sequelize following a 4-layer architecture: Routes → Controllers → Services → Models. Works across `apps/api` (business logic) and `apps/gateway` (public auth, proxy). +Expert in Express + Sequelize following a 4-layer architecture: Routes → Controllers → Services → Models. Works across `apps/api` (business logic) and `apps/gateway` (client auth, proxy). - `AbstractCrudService` / `AbstractCrudController` patterns to minimise boilerplate - All HTTP responses through `HttpResponser` (never bare `res.json()`) @@ -418,7 +435,7 @@ npm run build docker compose --env-file .env up -d ``` -> Only `gateway` and `front` expose ports to the host. `api` and `postgresdb` live on `internal-network` with `internal: true`. +> Only `front` (nginx) is exposed. `gateway`, `api` and `postgresdb` live on `internal-network` with `internal: true` and are not reachable from the Internet. ### 🌍 **Internationalisation** diff --git a/docs/SECURITY.md b/docs/SECURITY.md index cfe3412..a6f7c97 100644 --- a/docs/SECURITY.md +++ b/docs/SECURITY.md @@ -5,14 +5,34 @@ pasos manuales obligatorios antes de desplegar. ## Modelo de confianza -``` -┌──────────┐ cookie+Authorization ┌──────────┐ X-Internal-Auth (EdDSA) ┌─────┐ -│ Cliente │ ───────────────────────────▶│ Gateway │ ───────────────────────────▶ │ API │ -└──────────┘ └──────────┘ └─────┘ - (privada) (pública) +```mermaid +flowchart LR + Client(["🌐 Cliente"]) + + subgraph priv ["🔒 internal-network · internal: true"] + direction LR + Nginx["Nginx · front
SPA + proxy /api/*"] + Gateway["Gateway
JWT_ACCESS_SECRET / JWT_REFRESH_SECRET (HS256)
INTERNAL_JWT_PRIVATE_KEY (Ed25519, firma)"] + API["API
INTERNAL_JWT_PUBLIC_KEY (Ed25519, sólo verifica)"] + DB[("PostgreSQL")] + + Nginx -->|"proxy_pass /api/"| Gateway + Gateway -->|"X-Internal-Auth · EdDSA"| API + API --> DB + end + + Client ==>|"cookie + Authorization
única puerta pública"| Nginx + + classDef public fill:#1f6feb,stroke:#0b3d91,color:#fff; + class Nginx public; ``` -- **Gateway** es el único servicio expuesto a Internet. Posee: +- **Nginx** (contenedor `front`) es la puerta pública: sirve la SPA y hace + reverse-proxy de `/api/*` al gateway (mismo origen, para que las cookies + viajen sin CORS). El cliente nunca contacta al gateway directamente. +- **Gateway** vive en `internal-network` (privado, sin entrada desde Internet) — + es el servicio que firma los tokens de cara al cliente y proxia hacia el api + privado. Posee: - `JWT_ACCESS_SECRET` y `JWT_REFRESH_SECRET` (HS256) para los tokens del cliente. - `INTERNAL_JWT_PRIVATE_KEY` (Ed25519) para firmar las llamadas que