Gestión de secretos:
por qué las claves no van en el repositorio y qué hacer en su lugar
DevSecOps | unfantasmaenelsistema.com
Casi todo el mundo lo ha hecho alguna vez: una clave de API pegada en un fichero de configuración, una contraseña de base de datos en un `docker-compose.yml`, un token en un script de despliegue que «solo era para probar». Y luego ese código se sube a un repositorio. En cuanto ese commit existe, el secreto deja de ser secreto: queda en el historial de Git, se replica en cada clon, en cada fork y en cada mirror, y sigue ahí aunque borres la línea en el commit siguiente.
La gestión de secretos es la disciplina que resuelve ese problema para todo lo que no es una persona delante de un teclado: servidores, pipelines de CI/CD, contenedores y servicios que necesitan autenticarse entre sí sin que la credencial acabe escrita en texto claro en un sitio del que ya no se puede sacar. Este artículo recorre por qué el problema es tan persistente, cómo se detecta una fuga y qué herramientas y patrones se usan hoy para gestionarlo bien.
Qué aprenderás en este artículo
Por qué un secreto que entra en Git queda comprometido para siempre, aunque lo borres · Cómo se detectan secretos filtrados: gitleaks, trufflehog y la protección en el push · Qué es un gestor de secretos y cómo se comparan Vault, los de nube y sops+age · Secretos dinámicos y de vida corta: la diferencia entre rotar y no tener que rotar · Cómo se inyecta un secreto en tiempo de ejecución y por dónde se fuga después · Rotación, revocación y el caso concreto de contenedores y CI/CD
1. Por qué un secreto en el repositorio ya no es un secreto
Git no guarda ficheros, guarda una cadena de instantáneas enlazadas. Cuando añades una clave a un fichero y haces commit, ese contenido queda referenciado por el hash de ese commit para siempre. Borrarlo en un commit posterior solo añade una instantánea nueva sin la línea; la anterior, con el secreto, sigue siendo accesible con `git log`, `git show` o cualquier herramienta que recorra el historial.
Y aunque reescribieras el historial con `git filter-repo` o similar, ya es tarde en la práctica: si el repositorio se ha empujado a un servidor compartido, cualquiera con acceso ha podido clonarlo. En un repositorio público, los bots que rastrean GitHub en busca de credenciales encuentran una clave nueva en cuestión de segundos, no de horas. El commit puede haber quedado también en forks, en mirrors internos, en cachés de CI, en la copia local de cada compañero y en los sistemas de indexado de seguridad de la propia plataforma.
La regla práctica: si se filtró, se rota
Un secreto que ha estado en un repositorio (aunque sea privado, aunque el commit ya no exista) se considera comprometido y hay que revocarlo y emitir uno nuevo · Limpiar el historial es el segundo paso, no el primero: primero se corta la validez de la credencial expuesta, porque la limpieza nunca llega a todas las copias · «Es un repo privado» no es una mitigación: reduce la exposición, no la elimina, y las cuentas con acceso cambian con el tiempo
2. Detección: encontrar los secretos antes (y después) de que salgan
La primera capa de defensa es no dejar que el secreto llegue al repositorio, y la segunda es detectarlo cuanto antes si llega. Las herramientas de escaneo buscan patrones conocidos (formatos de clave de AWS, tokens de GitHub, claves privadas PEM, cadenas de conexión) y también entropía alta, que delata cadenas aleatorias que parecen credenciales.
| Herramienta / control | Dónde actúa | Qué aporta |
|---|---|---|
| gitleaks | Pre-commit, CI, auditoría del historial completo | Rápido, reglas personalizables, ideal como hook y como paso de pipeline |
| trufflehog | CI, auditoría, escaneo de historial y de repos remotos | Verifica activamente si la credencial encontrada sigue siendo válida |
| git-secrets | Pre-commit | Sencillo, se centra en patrones de proveedores de nube |
| Push protection de la plataforma | En el servidor, al recibir el push | Rechaza el push si detecta un secreto conocido; última red antes de publicar |
| Escaneo del proveedor (secret scanning) | Continuo, del lado del servidor | Avisa (y a veces revoca vía el emisor) cuando aparece un patrón conocido |
Lo eficaz es combinar niveles: un hook de `pre-commit` que corre gitleaks en local para el descuido cotidiano, el mismo escaneo como paso obligatorio de CI para lo que se salte el hook, y la protección en el push de la plataforma como último filtro. Ninguno sustituye a los demás.
# Escanear todo el historial de un repositorio en busca de secretos gitleaks detect --source . --report-format sarif --report-path gitleaks.sarif # Como hook de pre-commit (.pre-commit-config.yaml) # - repo: https://github.com/gitleaks/gitleaks # rev: v8.18.0 # hooks: # - id: gitleaks # Comprobar si un token encontrado sigue vivo, sin exponerlo en claro trufflehog git file://. --only-verified
3. Dónde deberían vivir los secretos: los gestores
Un gestor de secretos es un servicio cuya única función es guardar credenciales cifradas, controlar quién puede leer cada una, y registrar cada acceso. La aplicación no lleva la contraseña: lleva una identidad con la que pide la contraseña al gestor en el momento de arrancar o de usarla.
3.1 Familias principales
- HashiCorp Vault (y su fork OpenBao): gestor completo y agnóstico de plataforma. Almacén cifrado, políticas por ruta, auditoría, y motores que generan credenciales dinámicas para bases de datos, nube y PKI. Es el más potente y el que más operación requiere
- Gestores de la nube (AWS Secrets Manager, GCP Secret Manager, Azure Key Vault): integrados con el IAM del proveedor, rotación gestionada para sus propios servicios, cero infraestructura que mantener. A cambio, atan el diseño a esa nube
- sops + age (o KMS): no es un servicio, es un formato. Cifra los valores de un fichero YAML/JSON dejando las claves legibles, de modo que el fichero cifrado sí puede vivir en Git. Encaja de forma natural en flujos GitOps, donde el repositorio es la fuente de verdad
- Kubernetes: los `Secret` nativos son solo base64 (no cifrado) salvo que actives cifrado en reposo en etcd; lo habitual es combinarlos con un operador (External Secrets Operator, Sealed Secrets) que sincroniza desde un gestor real
| Criterio | Vault / OpenBao | Gestor de nube | sops + age |
|---|---|---|---|
| Infraestructura propia | Sí, hay que operarlo | No | No (solo un binario) |
| Secretos dinámicos / vida corta | Sí, es su punto fuerte | Parcial, según servicio | No, son estáticos |
| El fichero cifrado puede ir en Git | No | No | Sí, ese es el objetivo |
| Auditoría de accesos | Detallada | Vía logs del proveedor | No (es cifrado en reposo) |
| Encaje con GitOps | Con operador | Con operador | Nativo |
| Coste de entrada | Alto | Bajo | Muy bajo |
Elegir sin sobredimensionar
Un equipo pequeño en una sola nube casi siempre está bien con el gestor de esa nube más sops para lo que va en Git · Vault empieza a compensar cuando necesitas credenciales dinámicas, varias nubes o un PKI interno, y tienes quien lo opere · Lo que no es una opción a medio plazo: un `.env` en el servidor sin cifrar y sin control de acceso
4. Secretos dinámicos y de vida corta
La mejor credencial filtrada es la que ya ha caducado. Un secreto dinámico se genera bajo demanda, con permisos mínimos y una validez de minutos u horas, y se destruye al expirar. Si se filtra en un log, cuando alguien lo lee ya no sirve.
El ejemplo clásico es el acceso a base de datos: en lugar de una contraseña fija compartida por todas las instancias del servicio, el gestor crea un usuario temporal en la base de datos cada vez que un servicio arranca, con un TTL corto, y lo elimina al vencer. Lo mismo aplica a credenciales de nube (roles asumidos por minutos), a certificados de cliente emitidos por un PKI interno, o a tokens de acceso a APIs internas.
Rotar frente a no tener que rotar
La rotación programada de un secreto estático reduce la ventana de exposición pero sigue habiendo una credencial de larga vida entre rotación y rotación · El secreto dinámico elimina el problema de raíz: no hay nada que rotar porque nada dura lo suficiente para que importe
5. Inyección en tiempo de ejecución y sus fugas
Una vez que el secreto está en un gestor, hay que hacérselo llegar al proceso que lo usa. Cada mecanismo tiene un perfil de fuga distinto.
| Mecanismo | Cómo llega el secreto | Por dónde se fuga |
|---|---|---|
| Variables de entorno | El orquestador las inyecta al arrancar el proceso | `/proc/<pid>/environ`, volcados de crash, logs de arranque, subprocesos que heredan el entorno, paneles de error |
| Fichero montado (tmpfs) | El gestor escribe el secreto en un fichero que el proceso lee | Permisos del fichero, backups que incluyen el volumen, el fichero si queda en disco persistente |
| Llamada a la API del gestor | El proceso pide el secreto y lo mantiene solo en memoria | Volcados de memoria del proceso, trazas de depuración; es la opción con menor superficie |
| Sidecar / agente | Un proceso auxiliar renueva y expone el secreto localmente | Superficie del propio sidecar y del canal local (socket, fichero) |
Las variables de entorno son cómodas y ruidosas
Cualquier librería que registre la configuración al arrancar, cualquier gestor de errores que adjunte el entorno al informe, y cualquier proceso hijo, ven esas variables · Si se usan, que sea con secretos de vida corta y nunca en la salida de logs; para credenciales sensibles de larga vida, preferir fichero en tmpfs o lectura directa de la API
6. Rotación, revocación y contenedores
Un programa de secretos maduro define, para cada tipo de credencial, un periodo máximo de vida y un procedimiento de revocación inmediata para cuando algo se tuerce. La revocación tiene que ser independiente de la rotación: si sospechas de una fuga a las tres de la madrugada, necesitas poder invalidar esa credencial concreta sin esperar al ciclo normal ni desplegar nada.
6.1 En imágenes de contenedor
- Un secreto copiado en una capa de la imagen queda en esa capa aunque una instrucción posterior lo borre: `docker history` y el desempaquetado de capas lo recuperan. Nunca va en el `Dockerfile` ni en un `COPY`
- Los secretos de build (para descargar dependencias privadas, por ejemplo) se pasan con `RUN –mount=type=secret`, que los expone solo durante ese paso y no los persiste en la imagen
- Los secretos de ejecución se inyectan al arrancar el contenedor desde el orquestador o el gestor, no se hornean en la imagen
6.2 En CI/CD
- Los secretos del pipeline se guardan en el almacén de secretos del propio CI o se leen de un gestor externo mediante identidad federada (OIDC), que evita tener un token de larga vida guardado en el CI
- Enmascarado de secretos en los logs activado, y cuidado con los `echo`, los `set -x` y los pasos que imprimen el entorno al depurar
- Los pull requests desde forks no deberían tener acceso a los secretos del repositorio: es un vector clásico de exfiltración
# .sops.yaml: reglas de cifrado por ruta y por clave age
creation_rules:
- path_regex: secrets/prod/.*\.yaml$
key_groups:
- age:
- age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
- path_regex: secrets/dev/.*\.yaml$
age: age1lggyhqq0h2r3z0d0jd9q0zj0z0w0z0z0z0z0z0z0z0z0z0z0z0sq0x2r0
# Editar un secreto cifrado en el sitio (sops descifra en memoria)
sops secrets/prod/database.yaml
# Descifrar en el arranque del servicio, sin escribir a disco
sops --decrypt secrets/prod/database.yaml | app --config -Uso responsable y marco legal
Las técnicas de detección y auditoría de este artículo son para tus propios repositorios, tu infraestructura y tus pipelines, o para sistemas sobre los que tengas autorización escrita · Rastrear repositorios ajenos en busca de credenciales, o usar una credencial filtrada de terceros, constituye acceso no autorizado: en España, arts. 197 bis, 197 ter y 264 del Código Penal · Si encuentras un secreto expuesto de otra organización, el camino es la divulgación responsable, no la comprobación de si funciona
Conclusión
El error de fondo casi nunca es criptográfico: es de proceso. El secreto acaba en un sitio del que no se puede sacar (un commit, una capa de imagen, un log) porque en el momento de escribirlo era lo más rápido. La gestión de secretos consiste en hacer que la opción correcta —pedírselo a un gestor en tiempo de ejecución— sea también la más cómoda, y en poner redes (escaneo en el commit, en el CI y en el push) para el día que alguien tenga prisa.
Si tienes que quedarte con tres ideas: los secretos no viven en el código sino en un gestor; cuanto más corta sea su vida, menos importa que se filtren; y una credencial que ha pasado por un repositorio se rota primero y se investiga después.
Referencias y herramientas
gitleaks.io y github.com/trufflesecurity/trufflehog — escaneo de secretos · developer.hashicorp.com/vault y openbao.org — gestor de secretos completo · getsops.io y github.com/FiloSottile/age — cifrado de secretos para GitOps · external-secrets.io — sincronización de gestores externos hacia Kubernetes · OWASP Secrets Management Cheat Sheet — guía de referencia concisa
unfantasmaenelsistema.com | Seguridad, sistemas y todo lo que no debería funcionar (pero funciona)

