Un secreto en git no se borra: se rota (y por qué)
La escena es universal: subes sin querer un .env con una API key, lo ves a los cinco minutos, haces git rm, un commit «remove secret» y respiras. Mal hecho: el secreto sigue ahí, intacto, en el historial. Y a estas alturas, probablemente ya lo tenga alguien más.
Por qué borrarlo no lo borra
Git no guarda «el estado actual», guarda todos los cambios. Tu commit de borrado añade una línea nueva a la historia, no la reescribe. El secreto sigue accesible con un comando trivial:
git log -p | grep -i "api_key"
git show <commit-donde-lo-metiste>
Y no es solo tu clon. Si lo empujaste a un repositorio compartido, asume que ya no lo controlas: hay forks, clones de compañeros, copias en la caché del proveedor, en los logs de CI y en los backups. Peor: bots automáticos escanean los commits públicos en tiempo real; una clave de AWS filtrada puede estar minando criptomonedas en tu cuenta en cuestión de minutos.
Lo primero, siempre: rotar
El instinto es «borro el commit y arreglado». No. Lo primero, antes que nada, es revocar y rotar la credencial en el proveedor (AWS, GitHub, Stripe, tu base de datos…). Desde el segundo en que un secreto toca un repositorio, trátalo como comprometido —aunque el repo sea privado—. Rotar es lo que corta el riesgo; lo demás es limpieza.
Limpiar el historial: útil, pero no es el remedio
Una vez rotada la credencial, puedes reescribir la historia para quitar el rastro con git filter-repo (o BFG). Pero entiende sus límites:
- Reescribe los hashes de todos los commits posteriores → rompe a quien tenga clones y obliga a force-push.
- No borra los clones, forks ni cachés que ya existen.
- Es higiene, no contención. Si te saltas la rotación, el secreto sigue siendo válido esté o no en la historia.
Dónde se esconden (más allá del historial)
- Historial de git: el clásico, borrado «en falso».
.envcommiteado: casi siempre por un.gitignoreincompleto o ungit add .a lo bruto.- Logs de CI/build: un
echo $TOKENde depuración que acaba en un log público. - Hardcodeado en el código: claves «temporales» que nadie quitó.
commit, pasa tu diff, tu .env o tu config por el Escáner de Secretos: detecta claves de AWS, tokens de GitHub/Slack, claves privadas, JWT y cadenas de alta entropía, en tu navegador (nada se sube). La forma más barata de cazar el secreto es no dejar que llegue al repo. Y si el filtrado es un JWT, mira también por qué un token se lee sin clave.Prevención que sí funciona
- ✅
.envsiempre en.gitignore; comparte un.env.examplesin valores. - ✅ Pre-commit hooks (gitleaks, trufflehog) que bloquean el commit si detectan un secreto.
- ✅ Secret scanning + push protection del proveedor (GitHub, GitLab) para frenar el push.
- ✅ Secretos en un gestor (Vault, AWS Secrets Manager, variables de entorno del runner), nunca en el árbol de ficheros.
- ✅ Y la regla de oro: rota por defecto. Ante la duda de si un secreto se filtró, rótalo; es más barato que investigarlo.
Checklist ante una fuga
- ✅ Rota/revoca ya la credencial (esto primero, siempre).
- ✅ Revisa el uso de la clave desde que se filtró (logs del proveedor).
- ✅ Opcional: reescribe el historial (
git filter-repo/BFG) tras rotar. - ✅ Cierra la puerta:
.gitignore, pre-commit hook, push protection. - ✅ Escanea antes de commitear la próxima vez.
«Ya lo borré» es la frase que precede a la factura de AWS. En git, un secreto no se borra: se rota. Interioriza esa diferencia y te ahorrarás el incidente que empieza con «pero si lo quité en el commit siguiente».