fix(security): close exposed PostgreSQL and Redis ports (BSI/CERT-Bund report)
Some checks failed
CI / ruff (push) Successful in 12s
CI / validate (push) Has been cancelled
CI / dependency-scanning (push) Has been cancelled
CI / docs (push) Has been cancelled
CI / deploy (push) Has been cancelled
CI / pytest (push) Has been cancelled

Docker bypasses UFW iptables, so bare port mappings like "5432:5432"
exposed the database to the public internet. Removed port mappings for
PostgreSQL and Redis (they only need Docker-internal networking), and
bound the API port to 127.0.0.1 since only Caddy needs to reach it.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-04 22:31:07 +01:00
parent 3c7e4458af
commit a7392de9f6
2 changed files with 35 additions and 11 deletions

View File

@@ -9,8 +9,6 @@ services:
POSTGRES_PASSWORD: secure_password POSTGRES_PASSWORD: secure_password
volumes: volumes:
- postgres_data:/var/lib/postgresql/data - postgres_data:/var/lib/postgresql/data
ports:
- "5432:5432"
mem_limit: 256m mem_limit: 256m
healthcheck: healthcheck:
test: ["CMD-SHELL", "pg_isready -U orion_user -d orion_db"] test: ["CMD-SHELL", "pg_isready -U orion_user -d orion_db"]
@@ -24,8 +22,6 @@ services:
image: redis:7-alpine image: redis:7-alpine
restart: always restart: always
command: redis-server --maxmemory 100mb --maxmemory-policy allkeys-lru command: redis-server --maxmemory 100mb --maxmemory-policy allkeys-lru
ports:
- "6380:6379" # Use 6380 to avoid conflict with host Redis
mem_limit: 128m mem_limit: 128m
healthcheck: healthcheck:
test: ["CMD", "redis-cli", "ping"] test: ["CMD", "redis-cli", "ping"]
@@ -41,7 +37,7 @@ services:
profiles: profiles:
- full # Only start with: docker compose --profile full up -d - full # Only start with: docker compose --profile full up -d
ports: ports:
- "8001:8000" # Use 8001 to avoid conflict with local dev server - "127.0.0.1:8001:8000" # Localhost only — Caddy reverse proxies to this
env_file: .env env_file: .env
environment: environment:
DATABASE_URL: postgresql://orion_user:secure_password@db:5432/orion_db DATABASE_URL: postgresql://orion_user:secure_password@db:5432/orion_db

View File

@@ -1706,6 +1706,34 @@ curl -s http://localhost:9090/api/v1/rules | python3 -m json.tool | grep -i redi
Docker network segmentation, fail2ban configuration, and automatic security updates. Docker network segmentation, fail2ban configuration, and automatic security updates.
### 20.0 Docker Port Binding (Critical — Docker Bypasses UFW)
!!! danger "Docker bypasses UFW firewall rules"
Docker manipulates iptables directly, bypassing UFW entirely. A port mapping like `"5432:5432"` exposes PostgreSQL to the **public internet** even if UFW only allows ports 22, 80, and 443. This was flagged by the German Federal Office for Information Security (BSI/CERT-Bund) in March 2026.
**Rules for port mappings in `docker-compose.yml`:**
1. **No port mapping** for services that only talk to other containers (PostgreSQL, Redis) — they communicate via Docker's internal network using service names (`db:5432`, `redis:6379`)
2. **Bind to `127.0.0.1`** for services that need host access but not internet access (API via Caddy, Flower, Prometheus, Grafana, etc.)
3. **Never use bare port mappings** like `"5432:5432"` or `"6380:6379"` — these bind to `0.0.0.0` (all interfaces)
| Service | Correct | Wrong |
|---|---|---|
| PostgreSQL | *(no ports section)* | `"5432:5432"` |
| Redis | *(no ports section)* | `"6380:6379"` |
| API | `"127.0.0.1:8001:8000"` | `"8001:8000"` |
| Flower | `"127.0.0.1:5555:5555"` | `"5555:5555"` |
**After deploying, verify no services are exposed:**
```bash
# Should return nothing for 5432 and 6379
sudo ss -tlnp | grep -E '0.0.0.0:(5432|6379|6380)'
# Should show 127.0.0.1 only for app services
sudo ss -tlnp | grep -E '(8001|5555|9090|3001)'
```
### 20.1 Docker Network Segmentation ### 20.1 Docker Network Segmentation
Three isolated networks replace the default flat network: Three isolated networks replace the default flat network:
@@ -2340,12 +2368,12 @@ After Google Wallet is verified working:
| Service | Internal Port | External Port | Domain (via Caddy) | | Service | Internal Port | External Port | Domain (via Caddy) |
|---|---|---|---| |---|---|---|---|
| Orion API | 8000 | 8001 | `api.wizard.lu` | | Orion API | 8000 | 127.0.0.1:8001 | `api.wizard.lu` |
| Main Platform | 8000 | 8001 | `wizard.lu` | | Main Platform | 8000 | 127.0.0.1:8001 | `wizard.lu` |
| OMS Platform | 8000 | 8001 | `omsflow.lu` | | OMS Platform | 8000 | 127.0.0.1:8001 | `omsflow.lu` |
| Loyalty+ Platform | 8000 | 8001 | `rewardflow.lu` | | Loyalty+ Platform | 8000 | 127.0.0.1:8001 | `rewardflow.lu` |
| PostgreSQL | 5432 | 5432 | (internal only) | | PostgreSQL | 5432 | none (Docker internal) | (internal only) |
| Redis | 6379 | 6380 | (internal only) | | Redis | 6379 | none (Docker internal) | (internal only) |
| Flower | 5555 | 5555 | `flower.wizard.lu` | | Flower | 5555 | 5555 | `flower.wizard.lu` |
| Gitea | 3000 | 3000 | `git.wizard.lu` | | Gitea | 3000 | 3000 | `git.wizard.lu` |
| Prometheus | 9090 | 9090 (localhost) | (internal only) | | Prometheus | 9090 | 9090 (localhost) | (internal only) |