Módulo 3: Exportar, normalizar y estructurar el repositorio

3. Separar credenciales de los workflows

Descripción

Al terminar esta lección vas a poder mantener las credenciales de tus workflows completamente fuera del repositorio, de forma que ningún secreto —ni siquiera cifrado— llegue nunca a Git. Vas a saber qué es la N8N_ENCRYPTION_KEY y qué papel juega, qué hace exactamente el comando n8n export:credentials y por qué su bandera --decrypted es un riesgo de seguridad que hay que tratar con guantes, la diferencia entre la referencia a una credencial (que sí viaja al repo) y su valor (que jamás), y cómo poner un .gitignore que blinda tus secretos desde el primer commit.

Esto importa más que cualquier otra cosa de este módulo, y no es exageración. Un diff feo es una molestia; una credencial filtrada es un incidente de seguridad. order-triage usa dos credenciales reales —la llave del modelo de IA y la llave del CRM—, y basta un git add . distraído para publicarlas. Si el repositorio termina en GitHub, esa llave queda expuesta a bots que rastrean secretos filtrados las veinticuatro horas. Esta es la lección que separa a quien entrega un sistema profesional de quien, sin darse cuenta, le regala al mundo el acceso al CRM de su cliente.

Conexión con el módulo: en la lección 2 exportaste los workflows. Esta exporta —con muchísimo cuidado— las credenciales, que son un mundo aparte porque son secretas. La regla que aprendas aquí condiciona todo lo que sigue: la estructura del repositorio (lección 5) reserva un lugar para el esquema de las credenciales pero nunca para sus valores, y el script de exportación (lección 7) va a estar diseñado para no tocar jamás un secreto. Piensa en esta lección como la que instala el reflejo de seguridad que las demás dan por hecho.

Una advertencia de método antes de empezar. En esta lección vas a ver cómo se usa la bandera --decrypted, porque tienes que entender qué produce para saber por qué es peligrosa. Pero verla explicada no es lo mismo que correrla a la ligera. Cuando la corras en tu propia máquina, hazlo con plena conciencia de lo que sale de ahí: secretos en texto plano. Trátalos como tratarías dinero en efectivo sobre la mesa.

Qué es una credencial, y por qué es distinta de un workflow

Empecemos por el objeto. Una credencial en n8n es el secreto que un nodo usa para autenticarse ante un servicio externo: una llave de API, un token de OAuth, un usuario y contraseña, un secreto de webhook. Es la prueba de identidad que le dice al otro servicio "soy quien digo ser, déjame entrar".

En order-triage hay dos:

  • La llave del modelo de lenguaje que usa el nodo AI Agent para clasificar los pedidos. Sin ella, el nodo no puede llamar al modelo.
  • La llave del CRM que usa el nodo HTTP Request para consultar los datos del cliente. Sin ella, el CRM responde "no autorizado".

Aquí está la diferencia fundamental con un workflow, y es la que gobierna toda la lección. Un workflow es lógica: describe qué pasa, en qué orden, con qué nodos. La lógica se puede mirar, comparar, versionar y compartir sin riesgo —de hecho, queremos compartirla, ese es el punto del repositorio—. Una credencial es un secreto: su único valor está en que nadie más la tenga. En el momento en que una credencial deja de ser secreta, deja de servir; peor, se vuelve una puerta abierta.

Piénsalo como la diferencia entre los planos de tu casa y la llave de tu puerta. Los planos los puedes fotocopiar, mandar por correo, colgar en la pared: mostrar cómo está construida tu casa no la vuelve insegura. La llave es lo contrario: su valor entero depende de que solo tú la tengas. Fotocopiar la llave y repartirla es exactamente lo que no debes hacer. El workflow son los planos. La credencial es la llave. Este módulo versiona los planos y mantiene la llave lejos del papel.

Cómo guarda n8n las credenciales: cifrado en reposo y la llave maestra

Para entender por qué "ni siquiera cifrada" va al repo, primero hay que ver cómo n8n guarda las credenciales por dentro.

Cuando creas una credencial en el editor y escribes tu llave de API, n8n no la guarda tal cual. La cifra antes de escribirla en su base de datos. Cifrar significa transformar un texto legible en un galimatías ilegible usando una clave, de forma que solo quien tenga esa clave pueda revertirlo. Es lo que se llama cifrado en reposo (encryption at rest): tus secretos están cifrados mientras "descansan" en la base de datos, así que aunque alguien robara el archivo de la base de datos, vería basura, no tus llaves.

¿Y con qué clave cifra? Con la N8N_ENCRYPTION_KEY. Esta es la llave maestra de tu instancia: una cadena de caracteres que n8n usa para cifrar y descifrar todas las credenciales. Vale la pena conocerla bien, porque es el centro de todo el asunto de seguridad:

  • Qué es: una clave secreta, única de tu instancia, con la que se cifran todas las credenciales de esa instancia.
  • De dónde sale: según la documentación oficial, n8n genera una clave aleatoria automáticamente la primera vez que arranca y la guarda en la carpeta ~/.n8n (en un archivo de configuración, bajo el nombre encryptionKey). No tienes que hacer nada para que exista. También puedes fijar la tuya propia, poniéndola en la variable de entorno N8N_ENCRYPTION_KEY antes del primer arranque; entonces n8n usa esa en vez de generar una.
  • Por qué importa tanto: la credencial cifrada y la llave maestra son las dos mitades del mismo candado. La credencial cifrada sin la llave es basura inútil. La llave sin la credencial cifrada no abre nada. Pero las dos juntas descifran todos tus secretos. Quien tenga ambas, tiene tus llaves de API en texto plano.

La analogía: la N8N_ENCRYPTION_KEY es la combinación de una caja fuerte, y las credenciales cifradas son lo que está adentro de la caja. Si alguien roba la caja pero no tiene la combinación, no puede abrirla. Pero si alguien consigue la combinación y la caja, se lleva todo. Por eso la combinación y la caja nunca viajan juntas, y por eso —lo vas a ver enseguida— ni siquiera la caja cerrada va al repositorio.

Un detalle práctico que el Módulo 4 desarrolla y conviene adelantar: cada entorno de Cumbre —dev, staging, prod— tiene su propia N8N_ENCRYPTION_KEY. Eso significa que una credencial cifrada en dev no se puede descifrar en prod, porque las llaves maestras son distintas. Es una propiedad deseada: aísla los secretos entre entornos. Por ahora quédate con la idea; en el Módulo 4 la usas de verdad.

Exportar credenciales: n8n export:credentials y la bandera peligrosa

La CLI tiene el gemelo de export:workflow para las credenciales: n8n export:credentials. Comparte casi todas las banderas que ya conoces —--all, --id, --output, --separate, --pretty, --backup—, y agrega una que no existe para los workflows y que hay que tratar con cuidado extremo: --decrypted.

BanderaQué hace
--allExporta todas las credenciales de la instancia.
--id=<id>Exporta una sola credencial por su identificador.
--output=<ruta>, -oDónde escribir el archivo o la carpeta.
--separateUn archivo por credencial. Requiere una carpeta en --output.
--prettyFormato legible.
--backupAtajo de --all --pretty --separate.
--decryptedExporta las credenciales en texto plano. Solo para credenciales.

La descripción oficial de --decrypted es de una honestidad brutal, y conviene citarla tal cual: "Exports the credentials in a plain text format. All sensitive information is visible in the files." Traducido: exporta las credenciales en texto plano; toda la información sensible queda visible en los archivos. Es decir, tus llaves de API, tal cual, legibles por cualquiera que abra el archivo.

Hay entonces dos formas de exportar credenciales, y la diferencia es enorme:

Sin --decrypted (por defecto): el archivo contiene las credenciales todavía cifradas. El valor de cada secreto sale como un bloque de caracteres ininteligibles, cifrado con la N8N_ENCRYPTION_KEY de tu instancia. Se ve así (recortado y con el bloque cifrado acortado):

[
  {
    "id": "5",
    "name": "Cumbre CRM key",
    "type": "httpHeaderAuth",
    "data": "U2FsdGVkX1+8f3a...bloque cifrado ilegible...9c4d=="
  }
]

Ese data es el galimatías. Sin la llave maestra correcta, no se puede volver texto legible. Este formato es el que se usa para migrar de una instancia a otra que comparte la misma llave, o como respaldo cifrado guardado en un lugar seguro.

Con --decrypted: el mismo archivo, pero con data en texto plano:

[
  {
    "id": "5",
    "name": "Cumbre CRM key",
    "type": "httpHeaderAuth",
    "data": {
      "name": "Authorization",
      "value": "Bearer sk-crm-live-9f2c8a1b4e7d6003"
    }
  }
]

Ahí está la llave del CRM de Cumbre, Bearer sk-crm-live-9f2c8a1b4e7d6003, a la vista de cualquiera. Este formato existe por una razón legítima: migrar credenciales a otra instancia que tiene una llave maestra distinta. Como la otra instancia no puede descifrar lo cifrado con tu llave, la única forma de llevar las credenciales es exportarlas en claro, moverlas por un canal seguro, e importarlas allá (donde se vuelven a cifrar con la llave de destino). Es una herramienta de migración, no de respaldo cotidiano, y todo lo que produce es material que nunca, bajo ninguna circunstancia, toca un repositorio.

La regla de oro: ni siquiera la versión cifrada va al repo

Aquí llegamos al corazón de la lección. Es tentador razonar así: "la exportación sin --decrypted sale cifrada, y cifrado es seguro, entonces esa sí la puedo commitear". No. Ni la cifrada. Hay tres razones, y vale la pena entender las tres porque el razonamiento se transfiere a cualquier secreto, no solo a n8n.

Razón 1: la mitad del candado en un lugar permanente. La credencial cifrada es inútil sola, cierto. Pero es la mitad del candado. La otra mitad —la N8N_ENCRYPTION_KEY— vive en tu servidor, en variables de entorno, en scripts de despliegue, en respaldos… en un montón de lugares donde podría filtrarse por su cuenta. El día que esa llave se filtre por cualquier vía, todo lo que necesita el atacante para descifrar tus secretos es la otra mitad. Y si esa otra mitad está en un repositorio de Git, ya la tiene servida. Commitear la credencial cifrada es dejar preparada la mitad del trabajo del atacante, para siempre.

Razón 2: la historia de Git es para siempre. Esto es lo que hace tan peligroso el error. Cuando commiteas un archivo y después te das cuenta y lo borras en un commit posterior, el archivo sigue en la historia. Git guarda todo: la versión donde el secreto existía queda grabada, y cualquiera que clone el repositorio la puede recuperar con un par de comandos. Borrar el archivo hoy no borra que estuvo. La única forma real de sacar algo de la historia de Git es reescribir la historia entera, un procedimiento delicado y que, si el repo ya está en GitHub y alguien lo clonó, ya llegó tarde. Con los secretos, la regla es: más vale no meterlo nunca que intentar sacarlo después.

Razón 3: como documentación, no sirve. Alguien podría defender la credencial cifrada diciendo "la guardo en el repo como respaldo". Pero es un respaldo malo: está atada a esa llave maestra específica, así que si pierdes la llave, el respaldo no vale nada; y si tienes la llave, el respaldo es un peligro. Para el propósito de "documentar qué credenciales necesita el workflow", que sí es legítimo, no hace falta el valor cifrado: basta con documentar el nombre y el tipo de la credencial, que no son secretos. Eso lo vemos en un momento.

La conclusión es una sola frase, y quiero que se te grabe: las credenciales, en cualquier forma —descifradas o cifradas—, viven fuera del repositorio. El repositorio versiona la lógica y la documentación. Los secretos van a otro lado: un gestor de contraseñas, un gestor de secretos, un respaldo cifrado offline. El Módulo 4 profundiza en el "otro lado"; esta lección se asegura de que ese otro lado no sea, por accidente, tu repo.

Lo que sí viaja: la referencia, no el valor

Si las credenciales no van al repo, ¿cómo sabe otro desarrollador qué credenciales necesita order-triage para funcionar? Aquí aparece una distinción fina y muy útil: la diferencia entre la referencia a una credencial y su valor.

Cuando abres el JSON de order-triage que exportaste en la lección 2 y buscas el nodo HTTP Request, vas a encontrar algo así:

{
  "name": "Query CRM",
  "type": "n8n-nodes-base.httpRequest",
  "credentials": {
    "httpHeaderAuth": {
      "id": "5",
      "name": "Cumbre CRM key"
    }
  }
}

Mira bien qué hay y qué no hay. Hay un puntero: "este nodo usa una credencial de tipo httpHeaderAuth, que en mi instancia tiene el id 5 y se llama Cumbre CRM key". No hay ningún secreto. No aparece la llave del CRM por ningún lado, solo su nombre y su tipo. Es como una etiqueta que dice "aquí va la llave de la puerta principal" sin ser la llave.

Esa referencia sí viaja al repositorio, y está perfectamente bien que viaje, porque no es secreta. De hecho es útil: le dice a quien lea el workflow qué credencial hace falta y de qué tipo. Cuando ese desarrollador importe el workflow en su instancia, va a tener que crear una credencial de tipo httpHeaderAuth con su propia llave del CRM y conectarla; la referencia le dice exactamente qué crear.

Entonces, ¿qué versiona el repositorio respecto de las credenciales? Tres cosas, ninguna secreta:

  1. La referencia, que ya viene incluida dentro del JSON del workflow (no haces nada extra: viaja sola).
  2. Un documento que lista qué credenciales necesita cada workflow, con su tipo y su propósito —"Cumbre CRM key, tipo Header Auth, da acceso de lectura al CRM"—. Esto es parte de la documentación de handoff de la lección 6.
  3. Un archivo .env.example con los nombres de las variables secretas pero sin sus valores, solo con marcadores. Esto lo desarrolla el Módulo 4; por ahora quédate con la idea de que el ejemplo lleva la forma, no el contenido.

El valor real —la llave— no está en ninguno de los tres. Está afuera, en el lugar seguro. El repositorio dice qué se necesita; el secreto real se provee aparte, en cada instancia.

El .gitignore que blinda los secretos desde el primer commit

Toda esta disciplina se apoya en una red de seguridad concreta: el .gitignore. Es la herramienta que hace que, aunque te distraigas con un git add ., los secretos no entren igual.

Un .gitignore es un archivo de texto, que vive en la raíz del repositorio, donde escribes —una por línea— las rutas y patrones de archivos que quieres que Git ignore por completo. Un archivo ignorado no aparece en git status, no se puede agregar con git add por accidente, y Git actúa como si no existiera. Es una lista de "no mires aquí".

Piénsalo como la lista de "no tocar" que le dejas a alguien que te cuida la casa: "todo lo de este cajón, ni lo abras". El .gitignore es ese cajón cerrado para Git. Lo que esté adentro no se sube, punto.

Cómo funciona por dentro: cada línea es un patrón. Un nombre suelto (.env) ignora ese archivo. Una carpeta con barra (credentials/) ignora todo lo que haya dentro de esa carpeta. Un asterisco (*.credentials.json) es un comodín: el * significa "cualquier cosa", así que ese patrón ignora cualquier archivo que termine en .credentials.json. Las líneas que empiezan con # son comentarios y Git las ignora a ellas también.

Este es un .gitignore sensato para un repositorio de n8n como cumbre-automations:

# --- Secretos: nunca al repositorio ---
# Variables de entorno con valores reales (llaves, tokens, contraseñas)
.env
.env.*
!.env.example

# Cualquier exportación de credenciales, cifrada o descifrada
credentials/
*.credentials.json
credentials-backup.json
*-decrypted.json

# La carpeta de datos de n8n, que contiene la encryption key y la base de datos
.n8n/

# --- Ruido del sistema y de herramientas ---
node_modules/
.DS_Store

Vamos línea por línea sobre las que importan:

  • .env y .env.* — ignora el archivo de variables de entorno con valores reales, y cualquier variante como .env.prod o .env.local. Ahí es donde vive el secreto de verdad en cada máquina.
  • !.env.example — el signo de admiración es una excepción: significa "esta sí, no la ignores". Como la línea anterior ignoró todos los .env.*, esta rescata específicamente .env.example, el archivo de ejemplo sin valores reales, que sí queremos versionar como plantilla. Es la única variante de .env que viaja.
  • credentials/, *.credentials.json, credentials-backup.json, *-decrypted.json — cualquier exportación de credenciales, sin importar cómo la hayas nombrado ni si está cifrada o en claro. Es una red ancha a propósito: prefieres ignorar de más que dejar pasar un secreto.
  • .n8n/ — la carpeta de datos de n8n, que contiene nada menos que la N8N_ENCRYPTION_KEY y la base de datos con las credenciales cifradas. Si esta carpeta se colara al repo, subirías la llave maestra y la caja fuerte de una sola vez. Ignorarla es obligatorio.

Ejemplo trabajado: el orden correcto, y por qué el orden es todo

El detalle que hace o rompe esta protección es el orden. El .gitignore tiene que existir antes de que hagas el primer git add que podría atrapar un secreto. Veámoslo como una secuencia, sobre cumbre-automations.

Paso 1 — Crea el .gitignore primero, antes que nada. Apenas inicializas el repositorio, incluso antes de exportar una sola credencial, escribe el .gitignore de arriba en la raíz. Este es el primer archivo que existe, no el último.

Paso 2 — Ahora exporta las credenciales, a un lugar que el .gitignore ya cubre. Si quieres un respaldo cifrado (no para el repo, para un lugar seguro), exporta así:

docker exec -u node -it n8n n8n export:credentials --all --output=credentials-backup.json

Fíjate: sin --decrypted, así sale cifrado. Y el nombre credentials-backup.json ya está en la lista de ignorados, así que aunque caiga dentro de la carpeta del repo, Git no lo va a ver. Ese archivo lo mueves a tu respaldo seguro y lo borras del directorio de trabajo.

Paso 3 — Verifica que Git no lo ve. Antes de commitear nada, corre:

git status

Qué esperar: en la lista de archivos que Git ve, credentials-backup.json no aparece. Tampoco .env ni la carpeta .n8n/ si existieran. Si el .gitignore está bien puesto, esos archivos son invisibles para Git. Si aparecen, detente: el .gitignore no está bien, y estás a un git add de un accidente. Revisa los patrones antes de continuar.

Paso 4 — Recién ahora, commitea. Con la certeza de que los secretos son invisibles, agregas los archivos que sí van —los workflows normalizados, el README, el .env.example— y commiteas con tranquilidad.

El orden importa porque el .gitignore no borra del pasado: solo evita el futuro. Si hicieras git add . antes de crear el .gitignore, y en ese momento hubiera un credentials-backup.json en la carpeta, Git ya lo habría capturado, y agregar el .gitignore después no lo sacaría de la historia. Por eso el .gitignore es el primer ciudadano del repositorio, no un agregado tardío.

Si el accidente ya pasó: rota, no solo borres

Supongamos lo peor: ya commiteaste una credencial, incluso ya la subiste a GitHub. ¿Qué haces? La respuesta correcta sorprende a mucha gente:

Lo primero no es borrar el archivo de Git. Lo primero es rotar la credencial.

Rotar una credencial significa invalidar la vieja y generar una nueva en el servicio que la emitió: entras al panel del CRM, revocas la llave sk-crm-live-9f2c8a1b..., y creas una nueva. En el instante en que la revocas, la que se filtró deja de servir: aunque un bot ya la haya copiado del repo, no abre nada. Después, con calma, actualizas la credencial en n8n con la llave nueva, y luego limpias la historia de Git.

¿Por qué en ese orden? Porque una vez que un secreto estuvo público, aunque sea un minuto, tienes que asumir que alguien lo copió. Borrarlo de Git cierra la puerta, pero si alguien ya entró, cerrar la puerta no lo saca. Rotar la credencial cambia la cerradura entera: ya no importa quién tenga la llave vieja. Limpiar Git es necesario, pero es el segundo paso, no el primero. La seguridad no se trata de esconder el secreto filtrado; se trata de que deje de ser válido.

Errores comunes

Commitear la credencial cifrada creyendo que cifrado es seguro (conceptual, y grave). Qué pasa: alguien exporta credenciales sin --decrypted, ve el bloque ilegible, concluye "esto está cifrado, lo subo tranquilo" y lo commitea. Por qué pasa: "cifrado" activa la intuición de "a salvo", que es correcta a medias: el cifrado protege si y solo si la llave maestra nunca se filtra, y la llave vive en muchos lugares donde podría filtrarse. Cómo detectarlo: si en algún momento razonas "está cifrado, entonces lo puedo subir", esa frase es la señal de alarma. Cómo corregirlo: aplica la regla sin excepciones —credenciales en cualquier forma, fuera del repo— y confía en el .gitignore para que ni por accidente entren. La lógica va al repo; el secreto, nunca.

Poner el .gitignore después del primer git add . (práctico). Qué pasa: alguien inicializa el repo, corre git add . para "agregar todo", commitea, y después se acuerda del .gitignore. Pero en ese add . se coló un .env o una exportación de credenciales, que ya quedó en la historia. Agregar el .gitignore ahora no la saca. Por qué pasa: git add . es cómodo y captura todo lo que hay, incluidos los secretos que todavía no ignoraste. Cómo detectarlo: revisa tu primer commit con git show --stat HEAD y busca cualquier .env, credentials, .n8n o archivo con "secret"/"key" en el nombre. Cómo corregirlo: el .gitignore va primero, siempre, antes del primer add. Y si ya se coló un secreto, primero rota la credencial, después limpia la historia. Prevenir es infinitamente más barato que reparar.

Confundir la referencia de la credencial con el secreto (conceptual). Qué pasa: alguien ve en el JSON del workflow el bloque "credentials": { "httpHeaderAuth": { "id": "5", "name": "Cumbre CRM key" } } y se asusta pensando que ahí está la llave, o al revés, borra ese bloque creyendo que protege algo. Por qué pasa: la palabra "credentials" aparece, y es fácil suponer que el secreto está ahí. Cómo detectarlo: mira si el bloque contiene un valor que parezca una llave (sk-..., Bearer ..., una contraseña) o solo un id y un name. Si es lo segundo, es una referencia, no un secreto. Cómo corregirlo: deja la referencia en paz —es útil y no es secreta—; el secreto real vive en la credencial de la instancia, no en el JSON del workflow. Confundir las dos cosas lleva a subir secretos por miedo mal dirigido o a romper el workflow borrando su puntero.

Usar --decrypted para el respaldo de todos los días (práctico y peligroso). Qué pasa: alguien lee que --decrypted "exporta las credenciales" y lo adopta como su comando de respaldo habitual, generando archivos con secretos en texto plano por toda su carpeta de trabajo. Por qué pasa: la palabra "exporta" suena a "respalda", pero --decrypted es una herramienta de migración entre instancias con llaves distintas, no de respaldo. Cómo detectarlo: si tienes archivos .json con llaves de API legibles en tu disco, --decrypted anda suelto. Cómo corregirlo: para respaldar, exporta sin --decrypted (sale cifrado) y guárdalo en un lugar seguro fuera del repo; reserva --decrypted para el momento puntual de migrar credenciales a otra instancia, moviéndolas por un canal seguro y borrándolas apenas termines.

Ejercicios

Ejercicio 1 — Clasifica qué viaja y qué no. Para cada uno de estos seis elementos, di si debe ir al repositorio (cumbre-automations) o no, y por qué en una frase: (a) el JSON del workflow order-triage con su bloque credentials de referencias; (b) el archivo credentials-backup.json exportado sin --decrypted; (c) el archivo .env con la llave real del CRM; (d) el archivo .env.example con CRM_API_KEY= y nada después; (e) la carpeta .n8n/; (f) un documento docs/order-triage.md que dice "requiere una credencial Header Auth para el CRM".

Ver solución

(a) Sí va. Es la lógica del workflow. El bloque credentials que contiene solo id y name es una referencia, no un secreto.

(b) No va. Es una exportación de credenciales; aunque esté cifrada, es la mitad de un candado y la historia de Git es para siempre. Guárdalo fuera del repo, en un lugar seguro. El .gitignore ya lo cubre.

(c) Jamás va. Contiene la llave real del CRM en texto plano. Es el secreto puro. El .gitignore lo ignora con .env.

(d) Sí va. Es la plantilla: tiene el nombre de la variable pero no su valor. Documenta qué hace falta sin filtrar nada. Es la excepción que rescata !.env.example en el .gitignore.

(e) No va. Contiene la N8N_ENCRYPTION_KEY y la base de datos con las credenciales cifradas —la llave maestra y la caja fuerte juntas—. Ignorarla es obligatorio.

(f) Sí va. Es documentación: dice qué credencial se necesita y de qué tipo, sin revelar el valor. Es exactamente lo que otro desarrollador necesita para reconstruir el sistema.

Por qué funciona: si acertaste los seis, ya internalizaste la línea divisoria que gobierna la seguridad del repo. Lógica y documentación (a, d, f) viajan. Secretos y sus contenedores (b, c, e) se quedan afuera. La única que confunde a casi todos es la (b): "está cifrada, ¿por qué no?". Porque cifrado no es lo mismo que seguro para siempre.

Ejercicio 2 — Escribe el .gitignore y verifica. En un repositorio de prueba, crea el .gitignore de esta lección. Después, crea a mano tres archivos vacíos para simular el peligro: .env, credentials-backup.json y .env.example. Corre git status y anota cuáles aparecen y cuáles no. Explica el resultado.

Ver solución

git status debería mostrar solo .env.example (y el propio .gitignore). Los otros dos, .env y credentials-backup.json, no aparecen: están ignorados.

La razón: .env cae bajo la regla .env; credentials-backup.json cae bajo credentials-backup.json (y también bajo *.credentials.json si lo hubieras nombrado así). Pero .env.example, aunque la regla .env.* lo ignoraría, es rescatado por la excepción !.env.example, así que Git sí lo ve y lo puedes versionar como plantilla.

Por qué funciona: ver con tus propios ojos que git status no muestra los secretos —aun estando ahí, en la carpeta— es lo que convierte el .gitignore de un concepto abstracto en una red de seguridad en la que confías. Y descubrir que .env.example sí aparece te enseña el mecanismo de la excepción con !, que es el detalle que más gente escribe mal.

Ejercicio 3 — El incidente. Un compañero te escribe angustiado: "Subí sin querer a GitHub un .env con la llave de la API del CRM del cliente. Ya borré el archivo y volví a commitear. ¿Estamos bien?" Responde en tres o cuatro frases: qué está mal en su solución, qué debe hacer primero, y en qué orden.

Ver solución

No están bien todavía, y el problema es que borrar el archivo no es suficiente. La llave estuvo pública, así que hay que asumir que ya alguien la copió —los bots que rastrean secretos en GitHub son automáticos y rápidos—. Además, borrar el archivo en un commit nuevo no lo saca de la historia: sigue recuperable en el commit anterior.

Lo primero que debe hacer es rotar la credencial: entrar al panel del CRM, revocar esa llave y generar una nueva, y actualizarla en n8n. En el momento en que la revoca, la llave filtrada deja de servir, sin importar quién la haya copiado. Solo después limpia la historia de Git (o, según el caso, considera el repo comprometido). El orden es: rotar primero, limpiar después. La seguridad no consiste en esconder el secreto filtrado, sino en que deje de ser válido.

Por qué funciona: este ejercicio corrige la intuición más peligrosa que existe con los secretos filtrados —"lo borré, ya está"—. Entender que la única reparación real es la rotación te prepara para responder bien el día que te pase a ti o a alguien de tu equipo, que con suficiente tiempo, le pasa a casi todos.

Resumen y siguiente paso

En esta lección viste por qué las credenciales son un mundo aparte de los workflows: un workflow es lógica, que queremos compartir; una credencial es un secreto, cuyo valor entero depende de que nadie más la tenga. Entendiste cómo n8n las guarda cifradas en reposo con la N8N_ENCRYPTION_KEY —la llave maestra que genera solo en el primer arranque y guarda en ~/.n8n—, y que la credencial cifrada y la llave maestra son las dos mitades de un mismo candado. Conociste n8n export:credentials y su bandera --decrypted, que exporta secretos en texto plano ("all sensitive information is visible in the files") y existe solo para migrar entre instancias con llaves distintas. Grabaste la regla de oro: ni siquiera la versión cifrada va al repositorio, por la mitad-del-candado, porque la historia de Git es para siempre, y porque como documentación no sirve. Distinguiste la referencia a una credencial (un puntero con id y name, que sí viaja) de su valor (el secreto, que jamás). Y pusiste el .gitignore que blinda todo —antes del primer commit, no después—, más el reflejo correcto ante un accidente: rotar la credencial primero, limpiar Git después.

Antes de avanzar deberías poder: explicar por qué una credencial cifrada tampoco va al repo; distinguir en un JSON la referencia de una credencial de su valor; escribir de memoria las líneas del .gitignore que protegen .env y las exportaciones de credenciales; y decir cuál es el primer paso ante un secreto filtrado.

La lección 4 vuelve al terreno de los workflows, ya sin el peso de la seguridad encima. Ahora que exportas workflows limpios y mantienes los secretos afuera, hay un problema estético que en realidad es de ingeniería: el JSON exportado trae campos que cambian solos —pinData, identificadores de versión, marcas de tiempo— y ensucian cada diff. Vas a escribir un pequeño script de normalización, con jq o Node, que quita ese ruido y ordena las claves de forma estable, para convertir un diff ilegible en un cambio que de verdad se puede revisar.

Recursos