fix(security): harden Redis auth, restrict /metrics, document Gitea port fix
Some checks failed
Some checks failed
- Add Redis password via REDIS_PASSWORD env var (--requirepass flag) - Update all REDIS_URL and REDIS_ADDR references to include password - Restrict /metrics endpoint to localhost and Docker internal networks (403 for external requests) - Document Gitea port 3000 localhost binding fix (must be applied manually on server) - Add REDIS_PASSWORD to .env.example Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -149,6 +149,10 @@ SEED_ORDERS_PER_STORE=10
|
||||
# =============================================================================
|
||||
# CELERY / REDIS TASK QUEUE
|
||||
# =============================================================================
|
||||
# Redis password (must match docker-compose.yml --requirepass flag)
|
||||
# ⚠️ CHANGE THIS IN PRODUCTION! Generate with: openssl rand -hex 16
|
||||
REDIS_PASSWORD=changeme
|
||||
|
||||
# Redis connection URL (used for Celery broker and backend)
|
||||
# Default works with: docker-compose up -d redis
|
||||
REDIS_URL=redis://localhost:6379/0
|
||||
|
||||
@@ -34,7 +34,8 @@ from datetime import UTC, datetime
|
||||
from enum import Enum
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Response
|
||||
from fastapi import APIRouter, Request, Response
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -538,12 +539,20 @@ async def readiness_check() -> dict[str, Any]:
|
||||
|
||||
|
||||
@health_router.get("/metrics")
|
||||
async def metrics_endpoint() -> Response:
|
||||
async def metrics_endpoint(request: Request) -> Response:
|
||||
"""
|
||||
Prometheus metrics endpoint.
|
||||
|
||||
Returns metrics in Prometheus text format for scraping.
|
||||
Restricted to localhost and Docker internal networks only.
|
||||
"""
|
||||
client_ip = request.client.host if request.client else None
|
||||
allowed_prefixes = ("127.", "10.", "172.16.", "172.17.", "172.18.", "172.19.",
|
||||
"172.20.", "172.21.", "172.22.", "172.23.", "172.24.",
|
||||
"172.25.", "172.26.", "172.27.", "172.28.", "172.29.",
|
||||
"172.30.", "172.31.", "192.168.", "::1")
|
||||
if not client_ip or not client_ip.startswith(allowed_prefixes):
|
||||
return JSONResponse(status_code=403, content={"detail": "Forbidden"})
|
||||
content = metrics_registry.generate_latest()
|
||||
return Response(
|
||||
content=content,
|
||||
|
||||
@@ -21,7 +21,7 @@ services:
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
restart: always
|
||||
command: redis-server --maxmemory 100mb --maxmemory-policy allkeys-lru
|
||||
command: redis-server --maxmemory 100mb --maxmemory-policy allkeys-lru --requirepass ${REDIS_PASSWORD:-changeme}
|
||||
mem_limit: 128m
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
@@ -42,7 +42,7 @@ services:
|
||||
environment:
|
||||
DATABASE_URL: postgresql://orion_user:secure_password@db:5432/orion_db
|
||||
JWT_SECRET_KEY: ${JWT_SECRET_KEY:-your-super-secret-key}
|
||||
REDIS_URL: redis://redis:6379/0
|
||||
REDIS_URL: redis://:${REDIS_PASSWORD:-changeme}@redis:6379/0
|
||||
USE_CELERY: "true"
|
||||
depends_on:
|
||||
db:
|
||||
@@ -73,7 +73,7 @@ services:
|
||||
env_file: .env
|
||||
environment:
|
||||
DATABASE_URL: postgresql://orion_user:secure_password@db:5432/orion_db
|
||||
REDIS_URL: redis://redis:6379/0
|
||||
REDIS_URL: redis://:${REDIS_PASSWORD:-changeme}@redis:6379/0
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
@@ -101,7 +101,7 @@ services:
|
||||
env_file: .env
|
||||
environment:
|
||||
DATABASE_URL: postgresql://orion_user:secure_password@db:5432/orion_db
|
||||
REDIS_URL: redis://redis:6379/0
|
||||
REDIS_URL: redis://:${REDIS_PASSWORD:-changeme}@redis:6379/0
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
@@ -123,7 +123,7 @@ services:
|
||||
ports:
|
||||
- "127.0.0.1:5555:5555"
|
||||
environment:
|
||||
REDIS_URL: redis://redis:6379/0
|
||||
REDIS_URL: redis://:${REDIS_PASSWORD:-changeme}@redis:6379/0
|
||||
FLOWER_BASIC_AUTH: ${FLOWER_USER:-admin}:${FLOWER_PASSWORD:-changeme}
|
||||
depends_on:
|
||||
redis:
|
||||
@@ -238,7 +238,7 @@ services:
|
||||
ports:
|
||||
- "127.0.0.1:9121:9121"
|
||||
environment:
|
||||
REDIS_ADDR: redis://redis:6379
|
||||
REDIS_ADDR: redis://:${REDIS_PASSWORD:-changeme}@redis:6379
|
||||
depends_on:
|
||||
redis:
|
||||
condition: service_healthy
|
||||
|
||||
@@ -1724,6 +1724,22 @@ Docker network segmentation, fail2ban configuration, and automatic security upda
|
||||
| API | `"127.0.0.1:8001:8000"` | `"8001:8000"` |
|
||||
| Flower | `"127.0.0.1:5555:5555"` | `"5555:5555"` |
|
||||
|
||||
**Gitea stack** (`~/gitea/docker-compose.yml`) also needs this fix:
|
||||
|
||||
```yaml
|
||||
# BEFORE (vulnerable):
|
||||
ports:
|
||||
- "3000:3000"
|
||||
- "2222:22"
|
||||
|
||||
# AFTER (secure):
|
||||
ports:
|
||||
- "127.0.0.1:3000:3000" # Caddy proxies git.wizard.lu
|
||||
- "2222:22" # SSH must stay public (Caddy can't proxy SSH)
|
||||
```
|
||||
|
||||
Port 2222 stays public because Caddy cannot proxy SSH — this is acceptable since SSH is designed for internet exposure. Port 3000 (Gitea web UI) must be localhost-only since Caddy reverse proxies `git.wizard.lu` to it.
|
||||
|
||||
**After deploying, verify no services are exposed:**
|
||||
|
||||
```bash
|
||||
@@ -1732,8 +1748,31 @@ 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)'
|
||||
|
||||
# Gitea web UI should be localhost only, SSH stays public
|
||||
sudo ss -tlnp | grep 3000 # should show 127.0.0.1
|
||||
sudo ss -tlnp | grep 2222 # will show 0.0.0.0 (expected for SSH)
|
||||
```
|
||||
|
||||
### 20.0b Redis Authentication (Defense-in-Depth)
|
||||
|
||||
Redis is isolated on Docker's internal network with no exposed ports, but as defense-in-depth a password is configured via the `REDIS_PASSWORD` environment variable.
|
||||
|
||||
Add to `~/apps/orion/.env`:
|
||||
|
||||
```bash
|
||||
# Generate a strong password
|
||||
openssl rand -hex 16
|
||||
# Add to .env
|
||||
REDIS_PASSWORD=<generated-password>
|
||||
```
|
||||
|
||||
The `docker-compose.yml` passes this to `redis-server --requirepass` and includes it in all `REDIS_URL` connection strings automatically.
|
||||
|
||||
### 20.0c Prometheus /metrics Endpoint Restriction
|
||||
|
||||
The `/metrics` endpoint is restricted to localhost and Docker internal networks at the application level. External requests to `https://api.wizard.lu/metrics` receive a `403 Forbidden` response. Prometheus scrapes from the Docker monitoring network (172.x.x.x) and is unaffected.
|
||||
|
||||
### 20.1 Docker Network Segmentation
|
||||
|
||||
Three isolated networks replace the default flat network:
|
||||
|
||||
Reference in New Issue
Block a user