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 files | Docker Secrets | External Secret Manager |
|---|---|---|---|
| Complejidad | Baja | Media | Alta |
| Seguridad | Básica (archivo en disco) | Buena (tmpfs, no en disco) | Excelente (encriptado, auditable) |
| Rotación | Manual (editar archivo, restart) | Manual (recrear secret, redeploy) | Automática (API) |
| Auditoría | Ninguna | Básica (quién accede al archivo) | Completa (quién, cuándo, qué) |
| Multi-entorno | .env.dev, .env.prod, etc. | Diferente secret por entorno | Namespaces/paths por entorno |
| Costo | $0 | $0 | $10-100+/mes |
| Cuándo usarlo | Desarrollo local | Staging, producción simple | Producción con compliance |
| Ejemplos | — | Docker Swarm Secrets | AWS 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
- Docker Compose Secrets — Referencia oficial
- OpenAI API Key Safety — Best practices de OpenAI
- git-secrets — Herramienta para prevenir commits de secrets
- truffleHog — Scanner de secrets en repos Git
- 12 Factor App — Config — Principio de config en env vars
- GitHub Actions Encrypted Secrets — Secrets en CI/CD
- Docker BuildKit Secrets — Secrets seguros en build-time
- BFG Repo-Cleaner — Limpiar secrets del historial de Git