From n00b to ZeroCool / El lado oscuro

Secrets que no deben vivir en Git: llaves, tokens y credenciales sin drama

Cómo manejar secrets sin filtrarlos en Git: .env, CI/CD, rotación, escaneo y qué hacer si ya se expusieron. Enfoque defensivo.

Lo que vale la pena leer aquí

Viernes, 6:40 pm. La laptop ya trae el ventilador a tope, tú nomás querías cerrar el deploy y largarte. Y en el chat alguien suelta: “¿Quién dejó el API key de producción en un commit de hace dos meses?”

Viernes, 6:40 pm. La laptop ya trae el ventilador a tope, tú nomás querías cerrar el deploy y largarte. Y en el chat alguien suelta: “¿Quién dejó el API key de producción en un commit de hace dos meses?”

Se siente como cuando te cae un 404 en production justo cuando el jefe pide “un cambio rápido”. Alguien intenta el clásico: “pero el repo es privado”.

Neta: Git es buenísimo para código y pésimo para secretos. Si se te cuela una llave, no es un bug simpático. Es un incidente. Y casi siempre te das cuenta tarde.

Qué te vas a llevar

  • Qué sí cuenta como secret (y qué solo es config normal).
  • Cómo sacar secretos de Git sin romper tu workflow: local, CI/CD y production.
  • Cómo reducir leaks con .gitignore, hooks y scanning.
  • Qué hacer si ya se filtró un token (sin pánico, pero en corto).
  • Tradeoffs reales: .env vs secret managers vs variables del runner.

El lado oscuro del repo: cómo se filtran “sin querer”

Casi nunca es malicia. Es talacha y deadline:

  • El compa nuevo sube .env “para que sí corra en tu máquina”.
  • Alguien mete la key directo al código con un “ahorita lo quito”. Y se va a dormir.
  • Un script de onboarding trae credenciales hardcodeadas porque “así es más fácil”.
  • En CI se imprime un echo $TOKEN para debug… y el log se queda guardado meses.

Y en muchas chambas pasa el combo mortal: accesos compartidos + prisa + “nomás tantito”. El día que se va un contractor o se pierde una laptop, te acuerdas por qué eso era mala idea.

Qué sí es un secret

  • API keys (Stripe, Twilio, SendGrid, OpenAI, etc.)
  • Tokens de acceso (GitHub, GitLab, npm, Docker registry)
  • Passwords (DB, Redis, SMTP)
  • Private keys (JWT signing, SSH, TLS)
  • Connection strings completas

Qué no es un secret (pero tampoco lo grites)

  • URLs públicas, nombres de bucket, IDs de proyecto (a veces son sensibles, pero no secretos)
  • Flags de feature sin impacto
  • Config general y defaults

Regla práctica: si con eso alguien puede actuar como tu sistema (cobrar, borrar, leer datos, hacer deploy), eso no va en Git.

Setup defensivo que sí aguanta el jale

1) Separa “config” de “secrets” sin inventarte magia

Ten dos mundos claros:

  • Config versionable: archivos de ejemplo y defaults (config.example.json, appsettings.json sin credenciales, config.yaml).
  • Secrets no versionables: .env, variables de entorno, secret manager.

Ejemplo simple (Node):

# .env (NO se commitea)
DATABASE_URL=postgres://user:pass@host:5432/db
STRIPE_SECRET_KEY=sk_live_...
JWT_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----..."

Y el “molde” para el equipo:

# .env.example (SÍ se commitea)
DATABASE_URL=
STRIPE_SECRET_KEY=
JWT_PRIVATE_KEY=

Tradeoff honesto: .env es cómodo, sobre todo en proyectos chicos o freelance mal cotizado donde necesitas que “nomás funcione”. En equipos más grandes, se vuelve un “¿quién tiene el bueno?” y aparecen variants raras. Ahí conviene un secret manager o mínimo un password manager compartido (1Password/Bitwarden) para repartir secretos sin Slack.

2) Cierra la puerta obvia con .gitignore (y confirma que sí sirve)

Agrega/valida patrones típicos:

# envs
.env
.env.*
*.env

# keys
*.pem
*.key
id_rsa
id_ed25519

# cloud creds
*.p12
*.pfx

# local tooling
.terraform/
terraform.tfstate*

La trampa que duele: si el archivo ya estaba trackeado, .gitignore no lo des-trackea.

Para sacarlo del índice sin borrarlo de tu máquina:

git rm --cached .env
git commit -m "Stop tracking .env"

Cuando alguien diga “pero ya está en .gitignore”, la pregunta correcta es: “¿sigue trackeado?”

3) Secrets en runtime: local, CI y production

Local

  • .env + loader (por ejemplo dotenv en Node, python-dotenv en Python).
  • O un script make/npm run que exporte variables.

CI/CD

  • Usa el vault de tu proveedor (GitHub Actions Secrets, GitLab CI Variables, CircleCI Contexts).
  • Evita que los secrets salgan en logs.

Ejemplo conceptual (GitHub Actions) sin exponer valores:

- name: Run tests
  env:
    DATABASE_URL: ${{ secrets.DATABASE_URL }}
    STRIPE_SECRET_KEY: ${{ secrets.STRIPE_SECRET_KEY }}
  run: npm test

Production

  • Ideal: Secret manager (AWS Secrets Manager/SSM, GCP Secret Manager, Azure Key Vault) + IAM.
  • Alternativa decente para equipos chicos: variables en runtime (ECS, Cloud Run, Kubernetes Secret) con permisos mínimos.

Tradeoff real: el secret manager cuesta lana y setup, sí. Pero te compra auditoría, permisos finos y rotación. A mano es rápido… hasta que tu knowledge se vuelve “pregúntale a Chuy, él sabe dónde está la clave”.

Secrets que no deben vivir en Git: llaves, tokens y credenciales sin drama - visual explicativa 1
Visual de apoyo: Qué te vas a llevar

4) Pre-commit hooks: el guardia que te evita el oso

La mejor filtración es la que no pasa. Un hook te frena antes de que el commit exista.

Herramientas comunes:

  • gitleaks (rápido, reglas claras)
  • trufflehog (muy buen detector, puede ser más pesado)

Ejemplo defensivo con gitleaks como hook (conceptual):

# instala gitleaks (según tu OS)
# luego crea un hook local
cat > .git/hooks/pre-commit <<'EOF'
#!/bin/sh
gitleaks protect --staged --redact
EOF
chmod +x .git/hooks/pre-commit

Realidad: los hooks locales no se comparten fácil. Solución práctica: usa un manager tipo pre-commit o, mínimo, ponlo en onboarding y refuérzalo en CI para que no dependa de “buena fe”.

5) Scanning en CI: asume que alguien se va a equivocar

Aunque tengas hook, corre escaneo en el pipeline:

  • En PRs: bloquea merge si detecta secrets.
  • En main: monitorea por si algo se coló.

Tip de supervivencia: ajusta reglas/allowlist para bajar falsos positivos (keys fake de ejemplo, strings de test). Si el scanner le estorba a la banda, lo van a bypass y te quedas sin red.

6) Permisos mínimos + rotación: no todo token debe ser “Dios”

  • Token de deploy: que solo haga deploy, no admin.
  • Servicios externos: separa sandbox vs prod.
  • DB: usuario de app con permisos mínimos (no root, no postgres).

Política que sí jala:

  • Rotación programada cada X días para secretos críticos.
  • Rotación inmediata cuando: alguien sale del equipo, laptop perdida/robada, repo expuesto, sospecha de leak.

Señal útil: si rotar “rompe todo”, tu deploy depende de magia y deuda técnica. Mejor descubrirlo en un simulacro que cuando production te cobre la factura.

7) Si ya se filtró: contén, rota y luego limpia

Si ya viste el secreto en Git (o te llegó el correo de “compromised key”):

  1. Contén: asume que ya fue visto. No esperes confirmación.
  2. Rota o revoca el secreto afectado (API key/token/password). Genera uno nuevo.
  3. Despliega para que el sistema use el nuevo secreto.
  4. Audita: revisa logs del proveedor, uso raro, IPs, timestamps.
  5. Limpia el repo: quitarlo del historial baja exposición futura, pero no reemplaza la rotación.

Limpiar historial reescribe commits y rompe forks; se hace con plan y comunicación. Regla defensiva: primero rotas, luego arreglas el pasado.

Screenshots sugeridos

  • Pantalla de “Secrets” en GitHub Actions agregando DATABASE_URL.
  • Job de CI corriendo un escaneo y marcando un hallazgo (valores redactados).
  • .gitignore en el repo y git status mostrando que .env no se trackea.
  • Panel del proveedor (Stripe/Twilio/SendGrid) donde se rota/revoca una API key.

Errores comunes (y cómo salir del hoyo)

“El repo es privado, no pasa nada”

Problema: privado no es seguro; solo es menos público. Igual hay accesos, logs, backups, permisos mal puestos.
Solución: trabaja como si fuera público: pre-commit + CI scanning + rotación.

“Lo subí y luego lo borré, ya quedó”

Problema: Git guarda historial. Borrar en el último commit no borra el pasado.
Solución: rota el secreto y, si aplica, planea limpieza de historial.

“Marcó falso positivo y lo desactivé”

Problema: desactivar el guard te deja en pelotas.
Solución: ajusta reglas/allowlist y mantén el control activo.

“Un .env por persona y ya”

Problema: drift. Cada quien corre distinto y el bug solo existe en la máquina de alguien.
Solución: .env.example al día + doc mínima + validación al arrancar.

Ejemplo de validación (Node):

const required = ["DATABASE_URL", "STRIPE_SECRET_KEY"];
for (const k of required) {
  if (!process.env[k]) throw new Error(`Missing env var: ${k}`);
}

“Uso el mismo token para todo”

Problema: un leak = compromiso total.
Solución: segmenta por entorno (dev/stage/prod) y por uso (deploy vs runtime).

Secrets que no deben vivir en Git: llaves, tokens y credenciales sin drama - visual explicativa 2
Visual de apoyo: El lado oscuro del repo: cómo se filtran “sin querer”

Checklist final (modo: antes de hacer merge)

  • No hay .env, .pem, .key, id_rsa ni terraform.tfstate trackeados.
  • Existe .env.example (o equivalente) con nombres de variables.
  • Pre-commit de secrets activo (o por lo menos requerido en el workflow).
  • Escaneo de secrets en CI bloquea PRs cuando detecta leaks.
  • Secrets están en el store del CI/CD o en un secret manager, no en el repo.
  • Tokens con permisos mínimos (no admin por default).
  • Plan de rotación definido para secretos críticos.
  • Si hubo leak: rotación hecha + deploy ok + auditoría revisada.

FAQ

1) ¿Un .env es “seguro”?

Más seguro que Git, sí. Pero no es escudo total: no te salva de malware, backups chuecos o laptops compartidas. Úsalo con .gitignore, disco cifrado y buen control de accesos.

2) ¿Qué conviene más: variables de entorno o un secret manager?

Variables de entorno: simples, rápidas, buen fit para proyectos chicos. Secret manager: IAM, auditoría, rotación y menos exposición. Si ya estás en nube y necesitas orden, vale la pena.

3) ¿Cómo comparto secretos con el equipo?

No por Slack. Usa un password manager con roles y revocación (Bitwarden/1Password). Para infra, IAM + secret manager. Y documenta el “cómo pedir acceso”, no el “pásame la clave”.

4) ¿Cómo evito que se impriman secrets en logs?

No loguees process.env, headers completos ni connection strings. En CI activa redacción (redact) y revisa scripts de debug. Si necesitas comprobar, loguea “existe/no existe” o longitud.

5) ¿Si limpio el historial de Git ya quedé?

No. Si un secret estuvo en Git, trátalo como comprometido. Limpiar reduce exposición futura, pero lo real es revocar/rotar.

Siguiente episodio

Threat modeling sin juntas eternas: detectar riesgos de verdad en tu app antes de que production te pida rollback a medianoche. Con ejemplos de producto, no solo cajitas y flechas.