<link rel="stylesheet" href="/assets/fonts/jetbrains-mono/jetbrains-mono.css" />
All posts

Contraseñas en el historial de git: cómo las eliminé (y por qué no basta)

Estaba haciendo una revisión general de mi proyecto, buscando cosas olvidadas. Encontré una que habría preferido no encontrar: un archivo de configuración de despliegue con la clave que firma los tokens de inicio de sesión, la contraseña del administrador y la contraseña para el envío de emails. En texto plano, en el historial de git, en tres ramas.

Cómo pudo pasar

El archivo estaba en el .gitignore. El problema es que llegó ahí después de haberse hecho commit una primera vez, y .gitignore solo ignora los archivos que todavía no están en seguimiento: un archivo que ya está en seguimiento sigue estándolo, cambio tras cambio, aunque su nombre aparezca en la lista.

El repositorio era privado, pero privado no significa seguro: lo ven los colaboradores, los servicios de CI, las copias de seguridad y cada ordenador en el que se ha clonado.

Eliminarlo de todo el historial

Borrar el archivo con un nuevo commit no sirve: sigue en todos los commits anteriores. Hay que reescribir el historial de cada rama. Usé git filter-repo sobre un clon mirror, después hice un push forzado y comprobé que ningún commit lo contenía:

# 1. Stop tracking the file (it stays on disk)
git rm --cached .env.deploy
git commit -m "Stop tracking .env.deploy"

# 2. Rewrite the history of EVERY branch on a mirror clone
git clone --mirror git@github.com:me/my-repo.git
cd my-repo.git
git filter-repo --path .env.deploy --invert-paths
git push --force --mirror

# 3. Verify: no commit on any branch may still contain it
git log --all --oneline -- .env.deploy

Después de una reescritura así, cualquiera que tenga una copia local debe realinearla con el nuevo remoto: los commits antiguos no deben volver a publicarse.

Por qué no basta

Aquí está la lección más importante. Incluso después de reescribir, los commits antiguos pueden seguir siendo accesibles: GitHub, por ejemplo, mantiene referencias internas a las pull requests que un push forzado no puede borrar. Y no se sabe quién descargó el repositorio en el pasado.

Eliminar un secreto del historial sirve para no empeorar la situación; lo único que la resuelve es cambiarlo. En la práctica:

  • Nueva clave de firma de tokens: invalida todas las sesiones existentes, así que todos tendrán que volver a iniciar sesión. Es el precio a pagar.
  • Nueva contraseña de administrador.
  • Revocar la contraseña de aplicación del correo y generar una nueva.
  • Actualizar las variables en todos los entornos donde se usan.

La segunda vez, el mismo patrón

Unas semanas después encontré el mismo problema de otra forma: un script de instalación del servidor, muy cómodo, que escribía las credenciales directamente en el archivo de configuración. Es el patrón que se repite: el script de conveniencia que guarda los secretos para no tener que configurar nada. Lo reescribí para que genere las claves en el momento y pida las contraseñas a quien lo ejecuta:

# Before: secrets written into a script tracked by git
# JWT_SECRET=hardcoded-value...

# After: generated or asked at runtime, never committed
JWT_SECRET="$(openssl rand -base64 48)"
read -rsp "SMTP password: " SMTP_PASS; echo
umask 077   # the .env file is readable by its owner only
printf 'JWT_SECRET=%s\nSMTP_PASS=%s\n' "$JWT_SECRET" "$SMTP_PASS" > .env

Los hábitos que evitan que vuelva a pasar

  1. Un .env.example en el repositorio, con los nombres de las variables y sin valores; el .env real nunca entra en el repositorio.
  2. Un escáner de secretos antes de cada commit, como gitleaks en un hook de pre-commit: bloquea el commit antes de que se produzca el daño.
  3. La protección de push que ofrecen GitHub y GitLab, que rechaza los push con secretos reconocibles.
  4. Nunca secretos en los scripts: generarlos en tiempo de ejecución o leerlos del entorno.
  5. Una revisión periódica del historial: un secreto que encuentras tú es un incidente pequeño; uno que encuentran otros, no.

En resumen

Un secreto en un repositorio debe tratarse como comprometido en el momento en que lo encuentras. Reescribir el historial es necesario, pero no suficiente: la verdadera solución es cambiar cada credencial expuesta. Después conviene hacer que el problema no pueda repetirse, con escáneres y reglas claras. He reunido más prácticas de seguridad en esta guía.

💬 Notas de los lectores

0 notas

Escribe una nota

Comparte tu opinión, una sugerencia o un cumplido

Últimas notas

Aún no hay notas. ¡Sé el primero en comentar!