Las credenciales filtradas siguen siendo la vía de entrada número uno en incidentes reales — y casi ninguna filtración es sofisticada: es un .env commiteado a GitHub, una key pegada en un log público, o un token dentro del bundle frontend. Este artículo ordena dónde debe vivir cada secreto según su contexto y qué hacer cuando (no si) algo se filtra.
La regla fundacional: el código nunca contiene secretos
Tres capas donde los secretos NO deben aparecer:
- Código fuente: ni hardcodeados ni como constantes. El repo se replica, se clona, se hereda.
- El repositorio: ni en commits antiguos. Git recuerda todo; borrar el fichero en el último commit no borra la historia.
- El frontend: todo lo que llega al navegador es público por definición. Una API key en tu bundle JS es un regalo con lazo para scrapers — y los bots rastrean repos públicos buscando patrones de claves en minutos (GitHub escanea automáticamente y notifica a los proveedores: AWS revoca keys filtradas sin preguntar).
La separación correcta: el código lee process.env.DATABASE_URL / os.environ["..."]; los valores reales viven fuera del código, inyectados por el entorno de ejecución.
Variables de entorno: el estándar operativo
El patrón 12-factor que gobierna prácticamente todo despliegue moderno:
# .env (LOCAL, jamás en git)
DATABASE_URL=postgres://app:secreto@db:5432/app
STRIPE_KEY=sk_live_...
RESEND_API_KEY=re_...
Reglas operativas:
.enven.gitignoredesde el minuto cero — antes del primer commit, no después. Nuestro generador de .gitignore lo incluye por defecto junto al resto de artefactos sensibles..env.examplesí al repo: estructura con claves vacías o ficticias documentando QUÉ necesita la app sin revelar valores.- Un
.envpor entorno; producción usa las variables nativas de la plataforma (Vercel/Netlify/Docker secrets/K8s secrets), nunca ficheros sueltos. - Prefijos por contexto (
VITE_,NEXT_PUBLIC_,REACT_APP_) marcan lo que SÍ viaja al navegador: si ves una clave real detrás de esos prefijos, es un bug de seguridad activo.
Tipología: qué secreto admite cada ubicación
| Secreto | ¿Puede ir al frontend? | Almacén correcto |
|---|---|---|
| API key de pago (Stripe secret) | Jamás | Backend env |
| API key pública publishable | Sí (es su función) | Frontend env |
| JWT signing secret | Jamás | Backend env / KMS |
| Token de servicio CI/CD | Jamás | Secrets del CI |
| Clave privada TLS | Jamás | Servidor / cert manager |
| Firebase public config | Sí | Frontend (con reglas de seguridad reales) |
El matiz importante: las claves "públicas" (Stripe publishable, config de Firebase) están diseñadas para exponerse — su seguridad depende de restricciones del lado servidor (reglas Firestore, dominios permitidos). Exponerlas no es filtración; exponer sus hermanas privadas, sí.
Cuando ya se filtró: protocolo de emergencia
Un secreto en un commit público exige asumirlo comprometido YA, aunque lo borres al segundo — bots automáticos lo capturan en segundos. Protocolo:
- Rotar/revocar inmediatamente en el panel del proveedor. Es la única acción que importa; todo lo demás es forense.
- Revisar logs de uso del secreto: ¿hubo actividad anómala entre filtración y rotación?
- Limpiar la historia SOLO si el repo es privado y decides conservarlo (BFG Repo-Cleaner o git filter-repo) — en repos públicos, da igual: ya fue rastreado. La rotación manda.
- Añadir detección preventiva: gitleaks o trufflehog en pre-commit y en CI bloquean pushes con patrones de claves.
Y el caso especial incómodo: claves en imágenes Docker. Los ENV quedan horneados en capas visibles con docker history. Secretos de build → build args usados y borrados en la misma capa, o mount secrets de BuildKit.
Gestores de secretos: cuándo subir de nivel
Variables de entorno bastan hasta cierto punto; sus límites empujan a plataformas dedicadas:
- Rotación centralizada: cambiar una credencial en Vault/AWS Secrets Manager/Doppler se propaga a todos los consumidores sin redeploy.
- Auditoría: quién leyó qué secreto y cuándo — imposible con ficheros.
- Secrets dinámicos: credenciales de BD efímeras generadas por uso (TTL horas), reduciendo el valor de cualquier robo.
- Cifrado en reposo y acceso por rol: los secretos no duermen en texto plano en un disco compartido.
Para proyectos personales, variables de entorno bien guardadas + backup cifrado son suficientes. La escalera sube con el tamaño del equipo y la cantidad de servicios.
Higiene diaria barata
- Nunca pegues tokens en issues, PRs, Slack o mensajes de commit.
- Logs: jamás loguear headers de autorización ni bodies completos en endpoints sensibles.
.envde ejemplo actualizado cuando añades variables nuevas.- Revisión periódica:
git log -p -- '*.env*' '*/config/*'de tanto en tanto, y gitleaks sobre toda la historia (gitleaks detect --log-opts="--all").
Preguntas frecuentes
¿Mi repo privado está a salvo? Relativamente: sigue siendo un punto único de fallo (laptop robada, cuenta comprometida, colaborador que se va). Los secretos viven mejor fuera del repo aunque sea privado.
¿Cifrar el .env dentro del repo (git-crypt, SOPS)? Solución legítima madura para equipos: el repo lleva el fichero cifrado y la clave vive en KMS/nube. Complejidad extra justificada solo con varios entornos y equipo grande.
¿Qué hago con claves de terceros en el cliente (mapas, analytics)? Restringe por dominio/referrer en el panel del proveedor y presupuesta cuota — es el diseño pensado para ellas. Las que no admiten restricción no deben estar ahí.
Asegura que tu .env jamás entre al repo con nuestro generador de .gitignore, gratis y directamente en tu navegador.