Isolate backend and db from host: two Docker networks

- backend-net (internal: true): db ↔ backend ↔ frontend reverse proxy
- frontend-net: frontend only; single host port binding (80 prod / 5173 dev)
- Remove ports: from db (5432) and backend (8000) — unreachable from host
- Security auditor: hard rule to never add host ports to db or backend

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
curo1305
2026-04-14 00:06:38 +02:00
parent 03fcc6e117
commit d423bea134
5 changed files with 53 additions and 15 deletions
+1
View File
@@ -85,3 +85,4 @@ Key management: private key (`JWT_PRIVATE_KEY`) signs tokens and must never be e
- After any code change, verify the pre-commit hook still passes
- **Never mount `/var/run/docker.sock` directly into the backend container** — Docker socket access must always go through `tecnativa/docker-socket-proxy` on an internal-only network with a minimal API whitelist. Raw socket access inside any app container is equivalent to root on the host.
- **Never spawn `--privileged` containers** or containers with added capabilities for app workloads
- **Expose the bare minimum of ports to the host** — only the frontend binds a host port (80 prod / 5173 dev). The database and backend must never have `ports:` in any compose file; they are reachable only via internal Docker networks. If a new service is added, default to no host port binding unless there is an explicit reason.
+17 -10
View File
@@ -22,19 +22,24 @@ A fullstack SaaS web application built with FastAPI, React, and PostgreSQL.
- All input sanitized before reaching the DB (null-byte rejection, length caps, format validation)
- 3 separate Docker containers: `db` (PostgreSQL), `backend` (FastAPI), `frontend` (nginx)
- All containers run as non-root users (UID 1001 for backend and frontend, UID 70 for db)
- Network-isolated: only the frontend exposes a host port (80/5173); db and backend are unreachable from outside Docker
- Dev environment seeds a test user automatically on startup (`test@example.com` / `Test123!`)
- Password policy: min 8 chars, upper + lowercase, digit, special character, no common words
- Pre-commit security hook (`scripts/security_check.py`) runs inside Docker on every commit
## Containers
| Container | Image | Port | User (UID:GID) | Description |
|---|---|---|---|---|
| `db` | postgres:16-alpine | 5432 | 70:70 (postgres) | PostgreSQL database |
| `backend` | custom (python:3.12-slim) | 8000 | 1001:1001 (appuser) | FastAPI management API |
| `frontend` | custom (nginxinc/nginx-unprivileged:alpine) | 80 | 1001:1001 (appuser) | React UI served by nginx (internal port 8080) |
| Container | Image | Host port | Network | User (UID:GID) | Description |
|---|---|---|---|---|---|
| `db` | postgres:16-alpine | none | backend-net | 70:70 | PostgreSQL database |
| `backend` | custom (python:3.12-slim) | none | backend-net | 1001:1001 | FastAPI management API |
| `frontend` | custom (nginxinc/nginx-unprivileged:alpine) | 80 (prod) / 5173 (dev) | backend-net + frontend-net | 1001:1001 | React UI + nginx reverse proxy |
The frontend nginx container proxies `/api/*` to the backend container internally — no CORS headers needed in production.
**Networks:**
- `backend-net` (`internal: true`) — db, backend, and frontend reverse proxy communicate here; no host routing
- `frontend-net` — frontend only; this is where the single host port (80/5173) is bound
The frontend nginx proxies `/api/*` to `backend:8000` via `backend-net`. No backend or database port is ever exposed to the host.
## Installation
@@ -47,12 +52,13 @@ The frontend nginx container proxies `/api/*` to the backend container internall
```bash
git clone <repo>
cd destroying_sap
cp .env.example backend/.env # edit SECRET_KEY at minimum
cp .env.example backend/.env
python scripts/generate_jwt_keys.py # paste output into backend/.env
docker compose up --build -d
```
- Frontend: http://localhost
- API docs: http://localhost:8000/docs
- API docs: not directly accessible from host (backend port not exposed); access via `docker compose exec backend` or add a dev-only port mapping
### Development (hot reload)
@@ -61,7 +67,7 @@ docker compose -f docker-compose.yml -f docker-compose.dev.yml up --build
```
- Frontend (Vite): http://localhost:5173
- Backend (uvicorn --reload): http://localhost:8000
- Backend: reachable by frontend via Docker network only (not exposed to host)
### Local (no Docker)
@@ -95,7 +101,8 @@ Copy `.env.example` to `backend/.env` and adjust:
| Variable | Default | Description |
|---|---|---|
| `DATABASE_URL` | `postgresql+asyncpg://postgres:password@localhost:5432/destroying_sap` | Async PostgreSQL URL |
| `SECRET_KEY` | `change-me-in-production` | JWT signing key |
| `JWT_PRIVATE_KEY` | — | RS256 private key PEM (generate with `scripts/generate_jwt_keys.py`) |
| `JWT_PUBLIC_KEY` | — | RS256 public key PEM (generate with `scripts/generate_jwt_keys.py`) |
| `CORS_ORIGINS` | `["http://localhost:5173"]` | Allowed frontend origins |
## Development
+1 -1
View File
@@ -37,7 +37,7 @@ Design decision: each installable app (billing, PDF, email, etc.) runs in its ow
## Infrastructure
- [ ] **Docker port hardening** expose only port 80 externally; backend (8000) and db (5432) must not be reachable from outside the Docker network. Prepare for deployment behind Traefik or nginx proxy manager (SSL termination, reverse proxy, no direct container exposure).
- [x] **Docker port hardening** — only port 80 (prod) / 5173 (dev) exposed on the host via `frontend-net`; backend and db have no host port bindings and sit on `internal: true` `backend-net`
## Infrastructure (existing)
+20
View File
@@ -0,0 +1,20 @@
# 2026-04-14 — Docker network isolation: backend and db ports removed from host
**Timestamp:** 2026-04-14T00:00:00
## Summary
Replaced flat single-network Docker setup with two explicit networks. Only the frontend exposes a host port. The database and backend are unreachable from outside the Docker network.
## Network architecture
- `backend-net` (`internal: true`) — db, backend, and frontend reverse proxy; no gateway, no host routing
- `frontend-net` — frontend only; binds port 80 (prod) or 5173 (dev) to the host
## Files Modified
- `docker-compose.yml` — removed `ports:` from `db` and `backend`; added `networks:` to all services; defined `backend-net` (internal) and `frontend-net`
- `docker-compose.dev.yml` — no network changes needed (inherits from base); kept `5173:5173` on frontend
- `.claude/agents/security-auditor.md` — added hard rule: only frontend exposes host ports; db and backend must never have `ports:` in any compose file
- `TODO.md` — marked Docker port hardening as done
- `README.md` — updated Containers table with network column; updated Installation section; removed stale SECRET_KEY env var; noted backend API docs are not directly accessible from host
+14 -4
View File
@@ -9,8 +9,6 @@ services:
POSTGRES_USER: ${POSTGRES_USER:-postgres}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-password}
POSTGRES_DB: ${POSTGRES_DB:-destroying_sap}
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
@@ -18,6 +16,8 @@ services:
interval: 5s
timeout: 5s
retries: 10
networks:
- backend-net
# ── Backend (management) ────────────────────────────────────────────────────
backend:
@@ -30,11 +30,11 @@ services:
env_file: ./backend/.env
environment:
DATABASE_URL: postgresql+asyncpg://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-password}@db:5432/${POSTGRES_DB:-destroying_sap}
ports:
- "8000:8000"
depends_on:
db:
condition: service_healthy
networks:
- backend-net
# ── Frontend (UI) ────────────────────────────────────────────────────────────
frontend:
@@ -48,6 +48,16 @@ services:
- "80:8080"
depends_on:
- backend
networks:
- backend-net
- frontend-net
volumes:
postgres_data:
networks:
# Internal-only: db ↔ backend ↔ frontend reverse proxy. No host routing.
backend-net:
internal: true
# External-facing: only the frontend binds a host port through this network.
frontend-net: