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:
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user