| Hallazgo | Severidad | Línea original | Corrección aplicada | Estado |
|---|---|---|---|---|
|
Credenciales hardcoded en ENV
DATABASE_URL y SECRET_KEY bakeados en la imagen → filtrable con docker history
|
Critical | ENV DATABASE_URL=...admin:supersecreto... | Eliminadas del Dockerfile. Inyectadas en runtime via env_file: .env + secrets BuildKit |
✓ Resuelto |
|
Proceso corriendo como root
Sin instrucción USER → PID 1 es root, escalada trivial si hay RCE
|
Critical | CMD python main.py (sin USER) | Añadido RUN groupadd -r appgroup && useradd -r -g appgroup appuser + USER appuser |
✓ Resuelto |
|
Herramientas dev en imagen prod
curl, wget, git, vim, nano, build-essential en runtime → superficie de ataque masiva
|
Critical | apt-get install -y curl wget git vim nano build-essential | Movido todo al stage builder. Runtime final solo lleva libpq5 para la conexión DB. | ✓ Resuelto |
|
Imagen base :latest sin pinear
FROM python:3.11 sin tag específico → builds no reproducibles, actualizaciones silenciosas
|
High | FROM python:3.11 | FROM python:3.11.9-slim-bookworm — versión exacta + variante slim |
✓ Resuelto |
|
COPY . . antes de instalar deps
Cualquier cambio de código invalida el caché de pip → builds 12 min siempre
|
High | COPY . .\nRUN pip install... | Reordenado: COPY requirements.txt → RUN pip install → COPY src/ separado | ✓ Resuelto |
|
Sin HEALTHCHECK definido
Docker Compose y K8s no pueden detectar contenedor unhealthy ni hacer rolling deploy seguro
|
Medium | (ausente) | Añadido en Dockerfile y en compose: curl -f http://localhost:8000/health |
✓ Resuelto |
|
Cache apt no limpiada en same layer
/var/lib/apt/lists/* queda en la capa, aumenta imagen ~50 MB sin beneficio
|
Medium | apt-get update && apt-get install (sin cleanup) | Añadido && rm -rf /var/lib/apt/lists/* en el mismo RUN del builder |
✓ Resuelto |
|
Sin .dockerignore
COPY . . envía .git, __pycache__, .env, tests/ al contexto → lento + secretos filtrados
|
Low | (sin .dockerignore) | Creado .dockerignore con .git, __pycache__, .env*, tests/, *.pyc, .pytest_cache | ✓ Resuelto |
|
No hay limitación de recursos en compose
Un proceso runaway puede consumir toda la RAM del host de staging
|
Low | (sin deploy.resources) | Añadido deploy.resources.limits: cpu 0.5, memory 512M para API |
✓ Resuelto |
FROM python:3.11 WORKDIR /app COPY . . # ← invalida cache siempre RUN pip install -r requirements.txt RUN apt-get update && apt-get install -y \ curl wget git vim nano \ build-essential libpq-dev # ↑ herramientas dev en prod ENV DATABASE_URL=postgresql://admin:supersecreto123@db:5432/nutriflow ENV SECRET_KEY=myjwtsecretkey1234567890 # ↑↑ CRITICAL: secretos en imagen EXPOSE 8000 CMD python main.py # ↑ sin USER → corre como root # sin HEALTHCHECK
# ═══ STAGE 1: builder ════════════════ FROM python:3.11.9-slim-bookworm AS builder WORKDIR /app # Instalar deps de compilación (solo en builder) RUN apt-get update && apt-get install -y --no-install-recommends \ build-essential libpq-dev \ && rm -rf /var/lib/apt/lists/* # Deps primero → cache estable COPY requirements.txt . RUN pip install --no-cache-dir --prefix=/install \ -r requirements.txt # ═══ STAGE 2: runtime ════════════════ FROM python:3.11.9-slim-bookworm WORKDIR /app # Solo librerías runtime necesarias RUN apt-get update && apt-get install -y --no-install-recommends \ libpq5 curl \ && rm -rf /var/lib/apt/lists/* # Usuario no-root RUN groupadd -r appgroup && useradd -r -g appgroup appuser # Copiar solo artefactos del builder COPY --from=builder /install /usr/local COPY --chown=appuser:appgroup ./src . USER appuser HEALTHCHECK --interval=30s --timeout=10s --retries=3 \ CMD curl -f http://localhost:8000/health || exit 1 EXPOSE 8000 CMD ["python", "-m", "uvicorn", "main:app", \ "--host", "0.0.0.0", "--port", "8000"]
version: '3.9' services: api: build: context: . target: runtime image: nutriflow-api:staging restart: unless-stopped env_file: .env # ← secretos en archivo, nunca inline ports: - "127.0.0.1:8000:8000" # ← solo localhost, no 0.0.0.0 networks: - frontend - backend depends_on: db: condition: service_healthy # ← espera health real redis: condition: service_healthy healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8000/health"] interval: 30s timeout: 10s retries: 3 start_period: 15s deploy: resources: limits: cpus: '0.5' memory: 512M logging: driver: json-file options: max-size: "10m" max-file: "3" db: image: postgres:15.6-alpine # ← versión pineada + alpine restart: unless-stopped env_file: .env volumes: - postgres_data:/var/lib/postgresql/data # ← named volume networks: - backend healthcheck: test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"] interval: 10s timeout: 5s retries: 5 deploy: resources: limits: cpus: '1.0' memory: 1G redis: image: redis:7.2-alpine restart: unless-stopped command: redis-server --maxmemory 256mb --maxmemory-policy allkeys-lru volumes: - redis_data:/data networks: - backend healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 10s timeout: 3s retries: 3 networks: frontend: # ← separación frontend/backend backend: internal: true # ← backend no accessible externally volumes: postgres_data: redis_data:
# Control de versiones .git .gitignore # Variables de entorno (NUNCA al contexto) .env .env.* !.env.example # Python cache __pycache__ *.pyc *.pyo *.pyd .Python .pytest_cache *.egg-info dist/ build/ # Tests y docs (no van a prod) tests/ docs/ *.md !README.md # IDE y OS .vscode/ .idea/ .DS_Store *.swp # Docker (no recursivo) Dockerfile* docker-compose*
# ═══ NutriFlow — Variables requeridas ══════ # Copiar a .env y rellenar valores reales. # NUNCA commitear .env con valores reales. # Base de datos POSTGRES_USER=nutriflow_user POSTGRES_PASSWORD=# CAMBIAR: mínimo 32 chars POSTGRES_DB=nutriflow_staging DATABASE_URL=postgresql://nutriflow_user:PASSWORD@db:5432/nutriflow_staging # Aplicación SECRET_KEY=# CAMBIAR: python -c "import secrets; print(secrets.token_hex(32))" ENVIRONMENT=staging DEBUG=false ALLOWED_ORIGINS=https://staging.nutriflow.app # Redis REDIS_URL=redis://redis:6379/0 # Logging LOG_LEVEL=INFO