Secretos filtrados en el historial de Git: encontrar, rotar, prevenir
Borrar una clave de API commiteada no la borra del historial de Git. Cómo encontrar secretos filtrados, rotarlos primero y evitar la próxima fuga.
· 7 min de lectura · Lina Source LLC
Alguien commitea un archivo .env, o pega una clave de API real en un módulo de configuración para probar algo rápido. Un revisor se da cuenta, la clave se borra en el commit siguiente y todo el mundo sigue a lo suyo. La clave sigue ahí. Git guarda todas las versiones de todos los archivos, y cualquiera que pueda clonar el repositorio puede leer el commit que la añadió.
Las credenciales hardcodeadas se registran como CWE-798. Esta guía cubre qué hacer cuando una ya ha aterrizado en el historial: encontrar todos los secretos, rotarlos antes que nada, decidir si merece la pena reescribir el historial y poner las barreras que eviten la próxima.
Por qué no basta con borrar la línea
Un commit que elimina un secreto añade una instantánea nueva sin él. La instantánea anterior, con el secreto, sigue estando referenciada por el historial de la rama. git log -p lo muestra, un git checkout del commit antiguo lo restaura, y todos los clones y forks existentes ya tienen una copia. Hacer squash de la rama antes de fusionar tampoco ayuda si los commits originales llegaron a subirse: el remoto puede seguir teniéndolos, y quien los haya descargado los tiene en local.
En un repositorio público, da por hecho que el secreto fue visto. Hay scrapers automáticos vigilando los pushes públicos en busca de patrones de credenciales, y la ventana entre un push y el primer abuso puede ser muy corta. En un repositorio privado la exposición es menor, pero no nula: todos los empleados, contratistas, sistemas de CI e integraciones con acceso de lectura la tienen.
Paso 1: rota primero
La rotación es el único paso que elimina realmente el riesgo. Reescribir el historial esconde el secreto de los clones futuros; no hace nada con las copias que ya existen. Así que revoca y reemplaza la credencial antes de tocar el repositorio.
Planifica la rotación para que no provoque una caída. Muchos proveedores permiten tener dos claves válidas a la vez: crea la nueva, despliégala en todos los sitios donde se usaba la antigua, confirma que el tráfico se ha movido y entonces revoca la antigua. Si el proveedor solo admite una clave, asume una interrupción breve; unos minutos de caída son mejor trato que dejar activa una credencial que sabes filtrada mientras coordinas un cambio perfecto.
- Genera una clave nueva en la consola del proveedor y despliégala por tu vía de configuración habitual.
- Revoca la clave antigua. No te limites a dejar de usarla; una clave válida sin uso sigue siendo una clave válida.
- Revisa los registros de auditoría del proveedor en busca de actividad con la clave antigua desde la fecha del commit: llamadas a la API, usuarios nuevos, permisos cambiados, facturación inesperada.
- Si el secreto era una contraseña de base de datos o una clave de firma, piensa qué podría haber desbloqueado. Un secreto de firma de JWT filtrado significa que se pudo falsificar cualquier token, así que invalida las sesiones existentes.
- Anota qué quedó expuesto, durante cuánto tiempo y qué cambiaste. Lo querrás tener si un cliente o un auditor pregunta.
Paso 2: encuentra todo lo que se filtró
Donde hay un secreto commiteado suele haber otros. Busca en el historial completo, no solo en el árbol actual, e incluye todas las ramas y etiquetas.
# Commits que añadieron o eliminaron una cadena en cualquier punto del historial
git log -p -S "sk_live_" --all
# Búsqueda con regex en los diffs de todos los commits
git log -p -G "AKIA[0-9A-Z]{16}" --all
# Archivos que existieron alguna vez en una ruta sospechosa
git log --all --oneline -- .env config/production.json
# Los escáneres dedicados comprueban cientos de formatos de credenciales conocidos
gitleaks git -v .
trufflehog git file://. --only-verifiedgitleaks y trufflehog son escáneres de código abierto hechos para esta tarea. Conocen los formatos de las claves de los proveedores habituales, y trufflehog puede comprobar si una credencial encontrada sigue activa. Las versiones antiguas de gitleaks usan gitleaks detect --source . en lugar del subcomando git. Pasa un escáner una vez por todo el historial y después mantenlo en marcha sobre los commits nuevos. Espera algunos falsos positivos, como fixtures de test y claves de ejemplo en la documentación. Revisa cada uno y después registra los falsos positivos confirmados en la allowlist o el archivo de baseline del escáner, para que la siguiente ejecución solo muestre hallazgos nuevos.
No te olvides de los sitios que rodean al repositorio: logs de CI que hicieron echo de variables de entorno, comentarios en issues y pull requests, páginas del wiki, capas de imágenes Docker construidas desde el repositorio, y gists o pastes compartidos mientras se depuraba.
Paso 3: decide si reescribir el historial
Una vez rotado, el secreto ya no sirve de nada, así que reescribir el historial es limpieza y no contención. Aun así merece la pena cuando el repositorio es público, cuando el secreto revela algo más allá de sí mismo (un hostname interno, el nombre de un cliente) o cuando lo exige el cumplimiento normativo. Tiene costes reales: cada colaborador debe volver a clonar, los pull requests abiertos se ven afectados y los hashes de commit cambian. Todo lo que referencie un commit por su hash, como notas de versión, registros de despliegue, enlaces en issues o dependencias fijadas en otros repositorios, apuntará a commits que ya no existen en la rama. Anuncia la reescritura con antelación, elige un momento tranquilo y fusiona o cierra antes los pull requests abiertos.
git filter-repo es la herramienta que recomienda el proyecto Git para esto, en lugar del antiguo git filter-branch. Trabaja sobre un clon nuevo y elimina el remoto origin como medida de seguridad, así que tendrás que volver a añadirlo antes de hacer push.
# Parte de un clon nuevo
git clone [email protected]:acme/api.git api-clean
cd api-clean
# Opción A: eliminar un archivo de todos los commits
git filter-repo --invert-paths --path config/production.env
# Opción B: reemplazar las cadenas secretas allí donde aparezcan
# replacements.txt contiene una regla por línea, por ejemplo:
# PASTE_THE_OLD_KEY_HERE==>REDACTED
git filter-repo --replace-text ../replacements.txt
# filter-repo elimina origin; vuelve a añadirlo y haz force-push
git remote add origin [email protected]:acme/api.git
git push --force --all
git push --force --tagsLo que reescribir no puede hacer
- No puede llegar a los clones, forks, cachés de CI ni copias de seguridad que ya existen. Quien hiciera fetch antes de la reescritura se queda con los commits antiguos.
- En GitHub, las referencias creadas para los pull requests son de solo lectura y pueden mantener accesibles los commits antiguos. Eliminar las vistas cacheadas y esas referencias requiere contactar con el soporte de GitHub.
- Los colaboradores que hagan pull o merge desde un clon antiguo pueden volver a subir el historial viejo. Pide a todo el mundo que vuelva a clonar, y protege la rama mientras lo hacen.
- No elimina el secreto de ningún otro sitio al que se copiara: logs, tickets, mensajes de chat o artefactos construidos.
Por eso importa el orden. Rota y después limpia. Un historial reescrito con una clave viva es una falsa sensación de seguridad. Después del push, pasa tu escáner sobre un clon nuevo para confirmar que el secreto ha desaparecido de verdad de todas las ramas y etiquetas.
Paso 4: evita el siguiente
El objetivo es que commitear un secreto sea difícil y que detectarlo sea rápido. Ningún control consigue las dos cosas, así que combínalos en capas.
Mantén los secretos fuera del código
Lee las credenciales de variables de entorno o de un gestor de secretos en tiempo de ejecución, y valida al arrancar que están presentes las que necesitas. Commitea un .env.example con valores de ejemplo y pon .env en .gitignore desde el primer commit. Para producción, un gestor de secretos como AWS Secrets Manager, Google Secret Manager, HashiCorp Vault o la configuración de entorno cifrada de tu plataforma te da control de acceso, registros de auditoría y una rotación más sencilla.
Bloquea los secretos antes de que se commiteen
Un hook de pre-commit detecta el error en la máquina de quien programa, antes de que llegue siquiera al remoto. El framework pre-commit convierte esto en unas pocas líneas de configuración que cualquier colaborador puede instalar.
# .pre-commit-config.yaml
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: vX.Y.Z # fija la última etiqueta de versión
hooks:
- id: gitleaks
# Instalar una vez por clon
# pip install pre-commit
# pre-commit installLos hooks son locales y se pueden saltar, así que ejecuta el mismo escáner en CI en cada push como red de seguridad. En GitHub, activa el secret scanning y la push protection si tu plan lo permite; la push protection rechaza en el servidor los pushes que contengan tokens de proveedores reconocidos.
Reduce el daño de las fugas que se te escapan
- Limita cada clave a los permisos mínimos y, si el proveedor lo permite, a IPs o referrers concretos.
- Prefiere credenciales de vida corta, como la federación OIDC desde CI hacia tu proveedor de nube, frente a claves estáticas de larga duración.
- Usa claves distintas por entorno, para que una clave de test filtrada no pueda tocar producción.
- Mantén un runbook de rotación para cada secreto crítico, de modo que rotar bajo presión sea rutina y no improvisación.
Dónde encaja la revisión de código
Los escáneres reconocen bien los formatos conocidos; son más débiles con el contexto, como una contraseña montada a partir de dos constantes o una credencial por defecto que viaja en un fallback de configuración. Una pasada de revisión sobre el código detecta esos casos. CodeAuditAgent marca las credenciales hardcodeadas como CWE-798 con la evidencia citada cuando audita la rama por defecto de un repositorio público o un fragmento pegado. Lee el código actual, no el historial de commits, así que mantén también un escáner de historial para los commits pasados.
La versión corta: un secreto commiteado es un secreto filtrado. Rótalo hoy, limpia el historial si compensa la molestia, y haz que la próxima fuga falle en el hook de pre-commit en lugar de en una página pública.