Módulo 2: Local & Container Deployment

7. Secrets y Seguridad Local

Descripción

En esta cápsula vas a aprender a manejar API keys, credenciales y datos sensibles en tu Docker Compose de forma segura. Al terminar, tus secrets nunca estarán en tu código, nunca en Git, y tendrás un flujo claro para gestionarlos en diferentes entornos.

Contexto: Las apps AI manejan API keys costosas (OpenAI, Anthropic) y potencialmente datos sensibles de usuarios. Un leak de tu API key de OpenAI puede costarte miles de dólares en minutos. Esta cápsula te protege de eso.


El Problema: API Keys Everywhere

Lo que NO debes hacer

# ❌ NUNCA hardcodees API keys en código
client = OpenAI(api_key="sk-proj-abc123def456...")

# ❌ NUNCA pongas keys en Dockerfiles
ENV OPENAI_API_KEY=sk-proj-abc123def456...

# ❌ NUNCA commitees .env con keys reales
# Si lo haces, la key está en el historial de Git PARA SIEMPRE

Lo que SÍ debes hacer

# ✅ Lee de variables de entorno
import os
client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])

# ✅ Falla rápido si la key no existe
api_key = os.environ.get("OPENAI_API_KEY")
if not api_key:
    raise RuntimeError("OPENAI_API_KEY environment variable is required")

Nivel 1: .env Files (Básico)

# .env — Archivo local, NUNCA en Git
OPENAI_API_KEY=sk-proj-your-real-key
ANTHROPIC_API_KEY=sk-ant-your-real-key
REDIS_PASSWORD=your-redis-password
# .env.example — Template, SÍ en Git
OPENAI_API_KEY=sk-proj-replace-me
ANTHROPIC_API_KEY=sk-ant-replace-me
REDIS_PASSWORD=change-this
# .gitignore
.env
.env.production
.env.staging
!.env.example
# docker-compose.yml
services:
  api:
    env_file:
      - .env  # Carga todas las variables del archivo

Nivel 2: Docker Secrets (Compose)

Para producción, Docker Compose soporta secrets como archivos montados:

services:
  api:
    secrets:
      - openai_key
    environment:
      - OPENAI_API_KEY_FILE=/run/secrets/openai_key

secrets:
  openai_key:
    file: ./secrets/openai_key.txt
# api/config.py — Leer secret de archivo
import os

def read_secret(name: str) -> str:
    """Lee un secret desde archivo Docker o variable de entorno."""
    file_path = os.environ.get(f"{name}_FILE")
    if file_path and os.path.exists(file_path):
        with open(file_path) as f:
            return f.read().strip()
    return os.environ.get(name, "")

OPENAI_API_KEY = read_secret("OPENAI_API_KEY")

Estructura de archivos para Docker Secrets

project/
├── secrets/                  # NUNCA en Git
│   ├── openai_key.txt        # Solo contiene: sk-proj-...
│   ├── anthropic_key.txt
│   └── redis_password.txt
├── .gitignore                # Incluye secrets/
└── docker-compose.yml
# .gitignore
secrets/
!secrets/.gitkeep

Ventaja sobre .env: Los secrets se montan como archivos en /run/secrets/, que es un filesystem en memoria (tmpfs). Nunca tocan el disco del container.


Comparación: .env vs Docker Secrets vs Secret Managers

Aspecto.env filesDocker SecretsExternal Secret Manager
ComplejidadBajaMediaAlta
SeguridadBásica (archivo en disco)Buena (tmpfs, no en disco)Excelente (encriptado, auditable)
RotaciónManual (editar archivo, restart)Manual (recrear secret, redeploy)Automática (API)
AuditoríaNingunaBásica (quién accede al archivo)Completa (quién, cuándo, qué)
Multi-entorno.env.dev, .env.prod, etc.Diferente secret por entornoNamespaces/paths por entorno
Costo$0$0$10-100+/mes
Cuándo usarloDesarrollo localStaging, producción simpleProducción con compliance
EjemplosDocker Swarm SecretsAWS Secrets Manager, HashiCorp Vault

Recomendación por etapa

Desarrollo local     → .env files (simple, rápido)
Staging              → Docker Secrets + .env fallback
Producción simple    → Docker Secrets
Producción enterprise → External Secret Manager (AWS Secrets Manager, Vault)

Nivel 3: Validación de Secrets al Arrancar

# api/config.py — Validar que los secrets existen antes de aceptar tráfico
from pydantic_settings import BaseSettings
from pydantic import field_validator

class Settings(BaseSettings):
    openai_api_key: str
    redis_url: str = "redis://cache:6379"
    
    @field_validator("openai_api_key")
    @classmethod
    def validate_api_key(cls, v):
        if not v:
            raise ValueError("OPENAI_API_KEY is required")
        if v.startswith("sk-proj-replace") or v == "sk-your-key-here":
            raise ValueError("OPENAI_API_KEY contains placeholder value")
        if not v.startswith("sk-"):
            raise ValueError("OPENAI_API_KEY format invalid (should start with sk-)")
        return v

settings = Settings()  # Falla al importar si la key es inválida

Validadores avanzados con Pydantic

import re
from pydantic_settings import BaseSettings
from pydantic import field_validator, model_validator

class Settings(BaseSettings):
    openai_api_key: str = ""
    anthropic_api_key: str = ""
    redis_url: str = "redis://cache:6379"
    redis_password: str = ""
    environment: str = "development"
    log_level: str = "debug"
    cache_ttl: int = 3600
    model_name: str = "gpt-4o-mini"
    max_tokens: int = 500

    @field_validator("openai_api_key")
    @classmethod
    def validate_openai_key(cls, v):
        if not v:
            raise ValueError("OPENAI_API_KEY is required")

        placeholders = ["replace", "your-key", "xxx", "change-this", "TODO"]
        if any(p in v.lower() for p in placeholders):
            raise ValueError("OPENAI_API_KEY contains a placeholder value — set a real key")

        if not re.match(r"^sk-(proj-)?[a-zA-Z0-9_-]{20,}$", v):
            raise ValueError(
                "OPENAI_API_KEY format looks wrong. "
                "Expected: sk-proj-... or sk-... with 20+ characters"
            )
        return v

    @field_validator("anthropic_api_key")
    @classmethod
    def validate_anthropic_key(cls, v):
        if not v:
            return v
        if not v.startswith("sk-ant-"):
            raise ValueError("ANTHROPIC_API_KEY should start with 'sk-ant-'")
        if len(v) < 30:
            raise ValueError("ANTHROPIC_API_KEY looks too short")
        return v

    @field_validator("redis_url")
    @classmethod
    def validate_redis_url(cls, v):
        if not v.startswith(("redis://", "rediss://")):
            raise ValueError("REDIS_URL must start with redis:// or rediss://")
        return v

    @field_validator("cache_ttl")
    @classmethod
    def validate_cache_ttl(cls, v):
        if v < 0:
            raise ValueError("CACHE_TTL cannot be negative")
        if v > 86400:
            raise ValueError("CACHE_TTL too high (max 24 hours = 86400)")
        return v

    @field_validator("max_tokens")
    @classmethod
    def validate_max_tokens(cls, v):
        if v < 1 or v > 128000:
            raise ValueError("MAX_TOKENS must be between 1 and 128000")
        return v

    @field_validator("log_level")
    @classmethod
    def validate_log_level(cls, v):
        valid = {"debug", "info", "warning", "error", "critical"}
        if v.lower() not in valid:
            raise ValueError(f"LOG_LEVEL must be one of: {valid}")
        return v.lower()

    @model_validator(mode="after")
    def validate_production_settings(self):
        if self.environment == "production":
            if self.log_level == "debug":
                raise ValueError("LOG_LEVEL should not be 'debug' in production")
            if not self.redis_password:
                raise ValueError("REDIS_PASSWORD is required in production")
        return self

    @property
    def is_dev(self) -> bool:
        return self.environment == "development"

    @property
    def detected_provider(self) -> str:
        if self.openai_api_key:
            return "openai"
        if self.anthropic_api_key:
            return "anthropic"
        return "none"

    class Config:
        env_file = ".env"
# Uso en main.py
from config import Settings

try:
    settings = Settings()
    print(f"✅ Config valid | Provider: {settings.detected_provider} | Env: {settings.environment}")
except Exception as e:
    print(f"❌ Config error: {e}")
    exit(1)

Secret Rotation Workflow

Rotar API keys es algo que vas a hacer regularmente: cuando un key se compromete, cuando un empleado sale del equipo, o simplemente como buena práctica de seguridad.

Flujo de rotación sin downtime

1. Generar nueva key en el dashboard del proveedor
2. Agregar la nueva key al entorno (sin quitar la vieja)
3. Verificar que la nueva key funciona
4. Actualizar containers con la nueva key
5. Revocar la key vieja
6. Verificar que todo sigue funcionando

Implementación paso a paso

# Paso 1: Genera la nueva key
# Ve a https://platform.openai.com/api-keys → Create new key
# Nueva key: sk-proj-NEW_KEY_123...

# Paso 2: Actualiza .env con la nueva key
# (Guarda la vieja como backup temporal)
cp .env .env.backup
# Edita .env:
# OPENAI_API_KEY=sk-proj-NEW_KEY_123...

# Paso 3: Verifica la nueva key antes de deployar
curl https://api.openai.com/v1/models \
  -H "Authorization: Bearer sk-proj-NEW_KEY_123..." \
  -s | python -m json.tool | head -5
# Debe retornar lista de modelos, no 401

# Paso 4: Actualiza los containers
docker compose up -d
# Compose detecta el cambio en .env y recrea los containers

# Paso 5: Verifica que la app funciona con la nueva key
curl http://localhost:8000/health
# {"status":"healthy",...}

curl -X POST http://localhost:8000/ask \
  -H "Content-Type: application/json" \
  -d '{"prompt":"ping"}'
# Debe responder normalmente

# Paso 6: Revoca la key vieja en el dashboard
# OpenAI Dashboard → API Keys → Revoke old key

# Paso 7: Limpia
rm .env.backup

Script de rotación automatizado

#!/bin/bash
# rotate-key.sh — Rota una API key con verificación

KEY_NAME="${1:-OPENAI_API_KEY}"
NEW_VALUE="$2"

if [ -z "$NEW_VALUE" ]; then
    echo "Uso: ./rotate-key.sh KEY_NAME NEW_VALUE"
    echo "Ejemplo: ./rotate-key.sh OPENAI_API_KEY sk-proj-new-key..."
    exit 1
fi

echo "=== Rotación de $KEY_NAME ==="

# Backup
cp .env .env.backup-$(date +%Y%m%d-%H%M%S)
echo "✅ Backup creado"

# Actualizar .env
if grep -q "^${KEY_NAME}=" .env; then
    sed -i.bak "s|^${KEY_NAME}=.*|${KEY_NAME}=${NEW_VALUE}|" .env
    rm -f .env.bak
else
    echo "${KEY_NAME}=${NEW_VALUE}" >> .env
fi
echo "✅ .env actualizado"

# Recrear containers
docker compose up -d
echo "✅ Containers recreados"

# Esperar a que pasen los health checks
echo "⏳ Esperando health checks..."
sleep 10

# Verificar
HEALTH=$(curl -s http://localhost:8000/health)
STATUS=$(echo "$HEALTH" | python -c "import sys,json; print(json.load(sys.stdin)['status'])" 2>/dev/null)

if [ "$STATUS" = "healthy" ]; then
    echo "✅ Rotación exitosa — app healthy"
else
    echo "❌ App no healthy después de rotación"
    echo "Health response: $HEALTH"
    echo "Considera restaurar el backup"
fi

Auditing Secrets en Docker Images

Un error común: meter secrets en la imagen Docker durante el build. Aunque luego los borres, las capas de Docker conservan todo.

Verificar que tu imagen no contiene secrets

# Ver el historial de capas de la imagen
docker history module-02-api --no-trunc

# Buscar keywords sospechosas
docker history module-02-api --no-trunc | grep -i "key\|secret\|password\|token"

# Si aparece algo como:
# ENV OPENAI_API_KEY=sk-proj-...
# → Tu imagen tiene un secret embebido. PELIGRO.

Errores comunes que embeben secrets

# ❌ MAL: ENV con key real
ENV OPENAI_API_KEY=sk-proj-abc123

# ❌ MAL: COPY del .env al image
COPY .env /app/.env

# ❌ MAL: ARG con default que es un secret
ARG OPENAI_API_KEY=sk-proj-abc123
RUN echo "Key is $OPENAI_API_KEY" > /tmp/debug.log
# ✅ BIEN: No incluir secrets en build-time
# Los secrets se pasan como env vars en runtime (docker-compose.yml)
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

.dockerignore para prevenir leaks

# .dockerignore
.env
.env.*
!.env.example
secrets/
*.pem
*.key
.git/
__pycache__/

Sin .dockerignore, el COPY . . en tu Dockerfile copia TODO, incluyendo .env con tus keys reales. El .dockerignore es tu primera línea de defensa.

Multi-stage builds para seguridad extra

# Build stage — puede necesitar secrets para tests, pero no llegan a la imagen final
FROM python:3.11-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Production stage — imagen limpia, sin residuos del build
FROM python:3.11-slim
WORKDIR /app

RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*

COPY --from=builder /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages
COPY --from=builder /usr/local/bin /usr/local/bin
COPY . .

EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

Escaneo automatizado de secrets en imágenes

# Usar trivy para escanear secrets en imágenes
# (instala: brew install aquasecurity/trivy/trivy)
trivy image --scanners secret module-02-api

# Usar dockle para auditar el Dockerfile
# (instala: brew install goodwithtech/r/dockle)
dockle module-02-api

Secrets en CI/CD

Cuando tu proyecto vive en GitHub y usas GitHub Actions para CI/CD, necesitas un flujo diferente para los secrets.

GitHub Actions Secrets

# .github/workflows/deploy.yml
name: Deploy AI App

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Build and test
        env:
          OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
          REDIS_PASSWORD: ${{ secrets.REDIS_PASSWORD }}
        run: |
          docker compose build
          docker compose up -d
          sleep 10
          curl -f http://localhost:8000/health
          docker compose down

Configurar secrets en GitHub

1. Ve a tu repo → Settings → Secrets and variables → Actions
2. Click "New repository secret"
3. Name: OPENAI_API_KEY
4. Value: sk-proj-tu-key-real
5. Click "Add secret"

Environment Secrets (por entorno)

# .github/workflows/deploy.yml
jobs:
  deploy-staging:
    runs-on: ubuntu-latest
    environment: staging  # Usa secrets del environment "staging"
    steps:
      - uses: actions/checkout@v4
      - name: Deploy to staging
        env:
          OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
        run: ./deploy.sh staging

  deploy-production:
    runs-on: ubuntu-latest
    environment: production  # Usa secrets del environment "production"
    needs: deploy-staging
    steps:
      - uses: actions/checkout@v4
      - name: Deploy to production
        env:
          OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
        run: ./deploy.sh production

Reglas de seguridad en CI/CD

# ❌ NUNCA imprimas secrets en logs de CI
echo $OPENAI_API_KEY  # GitHub los enmascara, pero no confíes en eso

# ❌ NUNCA pases secrets como argumentos de build
docker build --build-arg OPENAI_API_KEY=$OPENAI_API_KEY .
# Los build args quedan en docker history

# ✅ Pasa secrets solo como env vars en runtime
docker run -e OPENAI_API_KEY=$OPENAI_API_KEY my-app

# ✅ Usa Docker BuildKit para secrets en build (si necesitas)
DOCKER_BUILDKIT=1 docker build --secret id=openai_key,env=OPENAI_API_KEY .
# En el Dockerfile, con BuildKit secrets:
RUN --mount=type=secret,id=openai_key \
    OPENAI_API_KEY=$(cat /run/secrets/openai_key) \
    python -c "import openai; print('Key valid')"
# El secret NO queda en la capa — solo disponible durante el RUN

Checklist de Seguridad

Antes de commitear o deployar:
- [ ] .env no está trackeado por Git (verificar con `git status`)
- [ ] .env.example existe y tiene placeholders (no keys reales)
- [ ] .gitignore incluye .env y archivos de secrets
- [ ] .dockerignore incluye .env y secrets/
- [ ] El código lee keys de variables de entorno (no hardcodeadas)
- [ ] La app falla rápido si una key falta (no al primer request)
- [ ] Las API keys no aparecen en logs (no hacer print/log de keys)
- [ ] Docker images no contienen secrets (verificar con docker history)
- [ ] CI/CD usa GitHub Secrets, no secrets en el repo
- [ ] Pydantic valida formato y detecta placeholders

Qué Hacer si Leakeaste un Secret

# 1. INMEDIATAMENTE: Rota la key
# Ve a OpenAI Dashboard → API Keys → Revoke → Create new key

# 2. Actualiza la key en tus entornos
# .env, CI/CD secrets, managed platform env vars

# 3. Limpia el historial de Git (si commiteaste)
git filter-branch --force --index-filter \
  'git rm --cached --ignore-unmatch .env' HEAD

# 4. Fuerza push (CUIDADO: destructivo)
git push origin --force --all

# 5. Verifica que la key vieja no funciona
curl https://api.openai.com/v1/models \
  -H "Authorization: Bearer sk-old-leaked-key"
# Debe retornar 401 Unauthorized

Alternativa moderna a git filter-branch:

# BFG Repo-Cleaner (más rápido y seguro)
# Instalar: brew install bfg

# Eliminar un archivo del historial
bfg --delete-files .env

# Eliminar texto específico (la key) del historial
echo "sk-proj-abc123def456" > passwords.txt
bfg --replace-text passwords.txt

# Limpiar
git reflog expire --expire=now --all
git gc --prune=now --aggressive

Ejercicios Prácticos

Ejercicio 1: Setup seguro desde cero

Configura un proyecto con .env, .env.example, .gitignore, y validación de secrets.

Ver solución
# 1. Crear .env con tus keys reales
echo "OPENAI_API_KEY=sk-proj-tu-key-real" > .env

# 2. Crear .env.example con placeholders
echo "OPENAI_API_KEY=sk-proj-replace-me" > .env.example

# 3. Asegurar .gitignore
echo ".env" >> .gitignore

# 4. Verificar que .env NO está trackeado
git status
# .env NO debe aparecer en "Changes to be committed" o "Untracked files"
# .env.example SÍ debe aparecer

# 5. Agregar validación en config.py (ver Nivel 3 arriba)

Ejercicio 2: Verifica que tu Docker image no contiene secrets

docker history module-02-api --no-trunc | grep -i key
# No debe aparecer ninguna API key
Ver solución

Si aparece una key, tu Dockerfile tiene un ENV o ARG con la key. Solución: usar environment en Compose (runtime) en lugar de ENV en Dockerfile (build time).

# Verificación completa:

# 1. Revisar historial de la imagen
docker history module-02-api --no-trunc | grep -i "key\|secret\|password\|token"

# 2. Verificar que .dockerignore existe y excluye .env
cat .dockerignore
# Debe incluir: .env, .env.*, secrets/

# 3. Inspeccionar el filesystem de la imagen
docker run --rm module-02-api ls -la /app/
# NO debe aparecer .env ni secrets/

# 4. Inspeccionar env vars de la imagen (no del container)
docker inspect module-02-api --format='{{range .Config.Env}}{{println .}}{{end}}'
# Solo debe mostrar vars del sistema (PATH, PYTHON_VERSION, etc.)
# NO debe mostrar OPENAI_API_KEY

Ejercicio 3: Implementa la función read_secret

Implementa una función que lee secrets desde archivos Docker secrets O variables de entorno.

Ver solución
def read_secret(name: str, default: str = "") -> str:
    file_var = f"{name}_FILE"
    file_path = os.environ.get(file_var)
    
    if file_path:
        try:
            with open(file_path) as f:
                return f.read().strip()
        except FileNotFoundError:
            pass
    
    return os.environ.get(name, default)

# Uso:
OPENAI_API_KEY = read_secret("OPENAI_API_KEY")

Ejercicio 4: Implementa rotación de key con verificación

Escribe un script que rota la OPENAI_API_KEY: actualiza .env, recrea los containers, y verifica que la app sigue healthy.

Ver solución
#!/bin/bash
# rotate-openai-key.sh
set -e

NEW_KEY="$1"
if [ -z "$NEW_KEY" ]; then
    echo "Uso: ./rotate-openai-key.sh sk-proj-nueva-key"
    exit 1
fi

echo "1. Verificando nueva key con OpenAI API..."
STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
  https://api.openai.com/v1/models \
  -H "Authorization: Bearer $NEW_KEY")

if [ "$STATUS" != "200" ]; then
    echo "❌ La nueva key no es válida (HTTP $STATUS)"
    exit 1
fi
echo "✅ Key válida"

echo "2. Backup de .env..."
cp .env ".env.backup-$(date +%s)"

echo "3. Actualizando .env..."
sed -i.bak "s|^OPENAI_API_KEY=.*|OPENAI_API_KEY=$NEW_KEY|" .env
rm -f .env.bak

echo "4. Recreando containers..."
docker compose up -d

echo "5. Esperando health checks..."
for i in $(seq 1 12); do
    sleep 5
    HEALTH=$(curl -s http://localhost:8000/health 2>/dev/null || echo '{"status":"waiting"}')
    STATUS=$(echo "$HEALTH" | python3 -c "import sys,json; print(json.load(sys.stdin).get('status','unknown'))" 2>/dev/null)
    
    if [ "$STATUS" = "healthy" ]; then
        echo "✅ App healthy con nueva key"
        echo "6. Rotación completa — revoca la key vieja en el dashboard de OpenAI"
        exit 0
    fi
    echo "  Intento $i/12: status=$STATUS"
done

echo "❌ App no alcanzó estado healthy después de 60s"
echo "Restaura .env desde el backup si es necesario"
exit 1

Ejercicio 5: Audita tu imagen Docker completa

Crea un script que audita una imagen Docker para verificar que no contiene secrets embebidos.

Ver solución
#!/bin/bash
# audit-image.sh
IMAGE="${1:-module-02-api}"
echo "=== Auditoría de seguridad: $IMAGE ==="
ISSUES=0

echo -e "\n--- 1. Buscando secrets en capas del historial ---"
FOUND=$(docker history "$IMAGE" --no-trunc 2>/dev/null | grep -ic "key\|secret\|password\|token\|sk-proj\|sk-ant")
if [ "$FOUND" -gt 0 ]; then
    echo "⚠️  Encontradas $FOUND referencias sospechosas en historial"
    docker history "$IMAGE" --no-trunc | grep -i "key\|secret\|password\|token"
    ISSUES=$((ISSUES + 1))
else
    echo "✅ Sin secrets en historial"
fi

echo -e "\n--- 2. Verificando archivos sensibles en la imagen ---"
for FILE in .env .env.production secrets; do
    EXISTS=$(docker run --rm "$IMAGE" ls -la "/app/$FILE" 2>/dev/null)
    if [ -n "$EXISTS" ]; then
        echo "⚠️  Archivo sensible encontrado: /app/$FILE"
        ISSUES=$((ISSUES + 1))
    fi
done
if [ "$ISSUES" -eq 0 ]; then
    echo "✅ Sin archivos sensibles"
fi

echo -e "\n--- 3. Verificando env vars embebidas ---"
ENV_SECRETS=$(docker inspect "$IMAGE" --format='{{range .Config.Env}}{{println .}}{{end}}' 2>/dev/null | grep -ic "key\|secret\|password")
if [ "$ENV_SECRETS" -gt 0 ]; then
    echo "⚠️  ENV vars sospechosas en la imagen"
    docker inspect "$IMAGE" --format='{{range .Config.Env}}{{println .}}{{end}}' | grep -i "key\|secret\|password"
    ISSUES=$((ISSUES + 1))
else
    echo "✅ Sin secrets en ENV vars de la imagen"
fi

echo -e "\n=== Resultado: $ISSUES issues encontrados ==="
[ "$ISSUES" -eq 0 ] && echo "✅ Imagen limpia" || echo "❌ Revisar issues arriba"

Troubleshooting

"ValidationError: OPENAI_API_KEY is required" al arrancar

Tu .env no existe o no tiene la variable. Verifica:

# ¿Existe el archivo?
ls -la .env

# ¿Tiene la variable?
grep OPENAI_API_KEY .env

# ¿Docker Compose la ve?
docker compose config | grep OPENAI

"La key funciona con curl pero no en el container"

El container puede estar usando un .env viejo o una variable cacheada:

# Ver qué valor tiene el container
docker compose exec api env | grep OPENAI

# Si es diferente a tu .env actual, recrear:
docker compose down
docker compose up -d

"docker history muestra mi API key"

Tu Dockerfile tiene ENV OPENAI_API_KEY=... o ARG con la key. Elimínalo, usa env vars de runtime via Compose, y rebuild con --no-cache:

docker compose build --no-cache api
# Verifica que la key ya no aparece:
docker history module-02-api --no-trunc | grep -i key

IMPORTANTE: Si la imagen con el secret fue pusheada a un registry, elimínala del registry también.

"Secrets no se actualizan después de cambiar .env"

Docker Compose no detecta automáticamente cambios en .env para containers ya corriendo:

# Forzar recreación
docker compose up -d --force-recreate
# o
docker compose down && docker compose up -d

"GitHub Actions no encuentra el secret"

Verifica que el nombre del secret en el workflow coincide exactamente con el nombre en Settings:

# En el workflow:
env:
  OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
  # El nombre después de "secrets." debe coincidir EXACTAMENTE
  # con el nombre en Settings → Secrets

Los secrets de GitHub son case-sensitive y no permiten espacios.


Resumen

  • Nunca hardcodees API keys en código o Dockerfiles.
  • Usa .env files para desarrollo, Docker secrets para producción.
  • Valida secrets al arrancar — falla rápido si faltan.
  • .gitignore debe incluir .env; .env.example debe existir con placeholders.
  • Pydantic Settings valida formato, detecta placeholders, y verifica reglas por entorno.
  • Si leakeas un secret: rota inmediatamente, luego limpia el historial.
  • No loguees secrets — cuidado con print(os.environ) en debugging.
  • Audita tus imágenes con docker history — los secrets en build-time quedan en las capas.
  • En CI/CD, usa GitHub Secrets por entorno — nunca secrets en el código del repo.

Recursos Adicionales

  1. Docker Compose Secrets — Referencia oficial
  2. OpenAI API Key Safety — Best practices de OpenAI
  3. git-secrets — Herramienta para prevenir commits de secrets
  4. truffleHog — Scanner de secrets en repos Git
  5. 12 Factor App — Config — Principio de config en env vars
  6. GitHub Actions Encrypted Secrets — Secrets en CI/CD
  7. Docker BuildKit Secrets — Secrets seguros en build-time
  8. BFG Repo-Cleaner — Limpiar secrets del historial de Git