“Pegar la llave de tu casa a la puerta principal con cinta porque tenías prisa.”
El código de cliente no tiene secretos
Todo lo que ejecuta tu navegador, tu usuario puede leerlo. El bundle de JavaScript se descarga a su máquina; minificarlo no es cifrarlo. Cualquier cosa que pongas en código de front end (una clave en una variable, un token en una config, un secreto en una variable de entorno expuesta al cliente) es visible para cualquiera que abra las herramientas de desarrollador. No existe lo "oculto" en el navegador.
Esto confunde a la gente porque los frameworks tienen una convención que parece una función de seguridad pero no lo es. Un prefijo como NEXT_PUBLIC_ (o VITE_, o REACT_APP_) no protege un valor; hace lo contrario. Marca la variable como una que se incrustará en el bundle de cliente y se enviará al navegador. El prefijo significa "esto es público". Una herramienta de IA que lo usa para "arreglar" una variable indefinida está publicando tu secreto en silencio.
Dos tipos de claves, y la confusión fatal
La mayoría de los servicios que importan te dan dos tipos de credencial, y todo el juego consiste en no confundirlos:
- Claves publishable / anon / de cliente: diseñadas para ser públicas. La clave publishable de Stripe, la clave anon de Supabase. Seguras en el navegador porque, por sí solas, no pueden hacer nada privilegiado (dependen de reglas de servidor como RLS para ser seguras).
- Claves secret / service / de servidor: poder total. La clave secreta de Stripe, una clave de OpenAI, una clave service-role de base de datos. Estas pueden cobrar tarjetas, gastar tu presupuesto de modelo o leer cada fila. No deben tocar nunca el navegador, un repo público ni un prompt.
¿Ya se filtró? Rótala: no se puede des-filtrar.
Si un secreto ha estado en código de cliente, un repo público, un log o un chat, está quemado. Borrar la línea no ayuda: sigue en el historial de git, en la caché de alguien, en la base de datos de un scraper. La única solución real es rotar, revocando la clave antigua en el proveedor y emitiendo una nueva, y luego guardar la nueva correctamente. Esconder una clave filtrada no es una remediación; rotarla, sí.
# .gitignore: keep secrets out of the repo in the first place .env .env.local .env.*.local # Already committed once? It's in history. Rotate the key at the provider, # then scan history so you know what else escaped: # npx gitleaks detect --source . --redact
Dónde deben vivir las claves
El patrón que aguanta: los secretos viven en el servidor, y el navegador le pide al servidor que los use en su nombre. En lugar de que el navegador llame a una API de terceros con una clave secreta, llama a tu propio endpoint, y tu servidor, que el usuario no puede leer, añade la clave y hace la llamada. La clave nunca cruza la frontera hacia código que el usuario controla.
- Variables de entorno de servidor o un gestor de secretos: nunca un archivo en el repo, nunca un prefijo expuesto al cliente.
- Una ruta de servidor fina que hace de proxy hacia APIs de terceros, para que la clave secreta se quede en el servidor (la preferencia de SDEN por backends reales frente a stacks solo-cliente es en parte esto).
- Nunca en el bundle del navegador, nunca en git, nunca pegada en un prompt de IA o un documento compartido.
Una vez que has interiorizado "el navegador es público", la mayoría de las decisiones sobre secretos se responden solas: si filtrar un valor haría daño, va en el servidor, y punto.
Una línea por cada uno
- Todo lo que está en el navegador es legible; un prefijo NEXT_PUBLIC_/VITE_ publica un valor, no lo protege.
- Las claves publishable son seguras en el cliente por diseño; las claves secret/service (Stripe, OpenAI, service-role) nunca lo son.
- Un secreto filtrado no se puede des-filtrar: rótalo en el proveedor; borrar la línea lo deja en el historial de git y en los scrapers.
- Las claves pertenecen al servidor: entorno/gestor de secretos más un proxy fino, nunca en el bundle, en git o en un prompt.
Adónde ir ahora