No filtrar al cliente: el secreto vive en el servidor
Por qué los secretos de un Worker viven solo en el servidor del edge y jamás en el navegador: que `env` no existe en el cliente, el error de exponer un secreto en los static assets, el patrón del endpoint que guarda la llave, y las fugas por logs y errores.
Has aprendido a guardar secretos cifrados, a centralizarlos y a separarlos por entorno. Todo ese cuidado se evapora en el instante en que un secreto acaba en algo que el navegador descarga. Es el único pecado verdaderamente irreversible de la configuración: una var mal puesta se corrige, pero una credencial publicada hay que rotarla y asumir que alguien la vio. La buena noticia es que en Workers el secreto vive, por diseño, solo en el servidor del edge; el navegador nunca ve env. La fuga, cuando ocurre, siempre es porque alguien copió el valor a mano hacia el lado equivocado de la frontera.
- Entender que
envy sus secretos existen solo en el servidor del Worker, nunca en el cliente. - Reconocer el error de exponer un secreto en los static assets o en el bundle de cliente.
- Aplicar el patrón del endpoint de servidor que guarda la llave y devuelve solo el resultado.
- Cerrar las vías de fuga por registros y por cuerpos de error.
env no existe en el navegador
Un Worker se ejecuta en los servidores del edge de Cloudflare, no en la máquina del visitante. El objeto env —con sus vars, sus secretos y sus bindings— lo entrega el runtime al handler fetch del lado del servidor, y no se serializa ni se envía a ninguna parte. Desde el navegador no hay forma de leer env: no es una cuestión de permisos que se puedan saltar, es que ese objeto sencillamente no existe en el cliente.
De ahí una conclusión liberadora y exigente a la vez: un secreto de Worker no puede filtrarse por sí solo. La plataforma no lo expone. Cada fuga real que verás en tu carrera tiene la misma forma —un desarrollador tomó el valor de env y lo copió, con sus propias manos, a algo que el cliente descarga—. Blindar tus secretos, entonces, no consiste en configurar mejor la plataforma, sino en no cruzar nunca esa frontera con el valor en la mano.
Es un cambio de perspectiva importante respecto a un servidor tradicional, donde uno se preocupa de firewalls y puertos abiertos. En el edge, la plataforma ya te da el aislamiento del runtime gratis; tu responsabilidad se estrecha a una sola cosa, pero absoluta: no ser tú quien, con una línea de código, anule ese aislamiento entregando el secreto al cliente.
Merece subrayarse porque invierte una intuición: en el edge, el riesgo no viene de fuera hacia dentro, como en el modelo del perímetro, sino de dentro hacia fuera. No temes que alguien entre a robar el secreto del runtime; temes que el secreto salga del runtime en tu equipaje. Toda esta lección es, en el fondo, sobre vigilar la puerta de salida.
Como la plataforma no filtra por su cuenta, vale la pena tener a la vista el mapa completo de las cuatro vías por las que, en cambio, tú sí puedes filtrar un secreto sin querer:
El bundle de cliente
Una constante con la clave, o una var con prefijo público, que el bundler hornea en el JavaScript que descarga el navegador.
Un asset público
El secreto escrito en un archivo servido desde la carpeta de assets: HTML, un JSON de configuración, cualquier cosa con URL.
Los registros
Un console.log del secreto que viaja a wrangler tail y a Workers Logs, un almacén con muchos más lectores que tu base de datos.
Un cuerpo de error
El error crudo de un tercero, con su cabecera de autorización dentro, reenviado tal cual al cliente para depurar.
Verás que las cuatro comparten una raíz: en todas, el valor de env fue copiado a un sitio de menor confianza que el runtime del Worker. Ninguna es un ataque sofisticado; todas son un descuido. Por eso la defensa no es criptográfica sino de hábito: reconocer, al escribir cada línea, si estás moviendo un secreto hacia el cliente, hacia un archivo público o hacia un registro, y detenerte antes de hacerlo.
Si en algún momento no estás seguro de si un secreto llegó a viajar al cliente —un despliegue dudoso, un log que quizá lo imprimió, un asset que tal vez lo contuvo—, trátalo como filtrado. Rotar un secreto por precaución cuesta minutos; convivir con la duda de si una credencial está expuesta cuesta la tranquilidad y, tarde o temprano, un incidente. La rotación barata es la que se hace por si acaso, no la que llega tras confirmar el desastre.
El error clásico: secretos en los static assets
La vía de fuga más común nace de una arquitectura muy habitual: un Worker que sirve static assets junto a su lógica. Todo lo que vive en tu carpeta de assets —el HTML, el JavaScript del cliente, los archivos públicos— es descargable por cualquiera que sepa la URL. Es su propósito. Y ahí es donde un secreto se pierde:
// codigo de CLIENTE (se descarga al navegador): FUGA
const STRIPE_KEY = "sk_live_51H..."; // horneado en el bundle publico
await fetch("https://api.stripe.com/v1/charges", {
method: "POST",
headers: { authorization: `Bearer ${STRIPE_KEY}` },
});
Da igual que el valor original viniera de un secreto bien guardado: en el momento en que lo escribes en código que se empaqueta para el cliente, lo estás publicando. Peor aún, las herramientas de build de los frameworks incrustan variables de entorno en el bundle durante la compilación, y solo las que llevan un prefijo público —PUBLIC_, VITE_ y similares— están pensadas para el cliente. Un secreto nunca debe llevar ese prefijo ni ser referenciado desde código de cliente, porque el bundler lo horneará en un archivo que se sirve al mundo.
El prefijo público de tu framework es para valores que ya son públicos —una URL, una clave publicable de solo lectura—, no un permiso para exponer secretos. Antes de dar a cualquier variable un prefijo PUBLIC_, hazte la pregunta literal: ¿me importaría que esto apareciera impreso en el HTML de mi página? Si la respuesta es sí, no lleva prefijo y no se toca desde el cliente. El prefijo no protege nada: solo declara que el valor está autorizado a ser visto.
El riesgo se agrava cuando el mismo Worker sirve assets y hace SSR, que es justo la arquitectura recomendada en 2026. Como el render de servidor y los archivos estáticos salen del mismo proyecto, es fácil perder de vista qué código corre dónde. La regla mental que lo ordena: lo que se ejecuta dentro del handler fetch es servidor y puede leer env; lo que se envía en la respuesta —HTML, JavaScript de cliente, assets— es cliente y jamás debe contener el valor.
El caso más traicionero es el de una plantilla de servidor que interpola un valor directamente en el HTML: el render ocurre en el servidor, sí, pero su producto se descarga en el cliente. Que el código que escribe el secreto corra en el servidor no basta; lo que importa es dónde termina el resultado, y el resultado de una plantilla siempre termina en el navegador.
El patrón: pide un resultado, no la llave
La solución al problema de que una parte del cliente necesita algo que solo el servidor puede tener es siempre la misma: interpones una ruta de tu propio Worker. El cliente no recibe la llave; recibe el resultado de usarla.
// Worker (SERVIDOR): el secreto no sale de aqui
export default {
async fetch(request, env) {
const cobro = await fetch("https://api.stripe.com/v1/charges", {
method: "POST",
headers: { authorization: `Bearer ${env.STRIPE_KEY}` },
}).then((r) => r.json());
return Response.json({ estado: cobro.status }); // solo el resultado
},
} satisfies ExportedHandler<Env>;
El navegador llama a una ruta de tu Worker, el Worker lee env.STRIPE_KEY en el servidor, habla con el tercero y devuelve solo lo que el cliente necesita saber. La llave nunca cruza la frontera; lo que viaja es la respuesta ya cocinada.
Este rodeo —cliente pide, servidor guarda y traduce— es el mismo que sostiene cualquier arquitectura sana en el edge, y reaparece idéntico en los frameworks full-stack. Cuando en Astro o similares marcas un componente para hidratarse en el cliente, sus props se serializan dentro del HTML; pasar un secreto como prop a ese componente lo publica con tus propias manos, exactamente igual que escribirlo en el bundle. La regla es universal: lo que cruza hacia el cliente son datos ya cocinados y aptos para ver, nunca el material que los autorizó.
Hay una prueba mental que zanja casi todos los casos dudosos: imagina que un usuario abre las herramientas de desarrollo del navegador y mira la pestaña de red y el código fuente. ¿Vería el valor? Si tu diseño depende de que no se le ocurra mirar, el diseño está roto. La seguridad no puede apoyarse en la desatención de quien mira: el secreto tiene que ser, literalmente, inaccesible desde el cliente, no solo poco visible.
Ese endpoint intermedio, además, no es solo un escudo: es el lugar natural donde añadir lo que el cliente no debe controlar. La autenticación, los límites de uso, la validación de la petición y el registro de auditoría viven en esa ruta de servidor, entre el navegador y el tercero. Al no entregar la llave, no solo evitas la fuga: recuperas el control sobre cómo, cuándo y cuánto se usa la credencial.
Ya escribas un Worker crudo, uses Hono o despliegues un framework full-stack, la frontera es la misma: el código de servidor lee env, el código de cliente nunca. Los frameworks añaden comodidades —prefijos públicos, módulos de solo servidor, endpoints— pero ninguno puede impedir que tú, a mano, cruces un secreto al otro lado. La herramienta ayuda; la disciplina de no entregar la llave al cliente es tuya y no se delega.
Otras vías: registros y errores
No toda fuga va al navegador. Dos caminos más discretos filtran secretos hacia sitios que casi nadie vigila. El primero son los registros. Un console.log en un Worker no desaparece: viaja a wrangler tail y a Workers Logs, un almacén con más lectores y menos protección que tu base de datos.
// FUGA silenciosa hacia wrangler tail y Workers Logs
console.log("conectando con", env.STRIPE_KEY);
// Mejor: registra el hecho, nunca el material sensible
console.log("conectando con el proveedor de pagos");
El segundo son los cuerpos de error. Cuando un servicio de terceros falla, es tentador devolver al cliente el error completo para depurar; pero ese error a veces arrastra la cabecera de autorización o el cuerpo de la petición con la clave dentro. Nunca reenvíes al cliente un error crudo de un servicio protegido: registra lo necesario en el servidor y devuelve al cliente un mensaje que no revele material.
Cuando depures en producción con wrangler tail, registra identificadores y hechos, no valores: el id de la petición, el nombre del proveedor, el código de estado que devolvió. Un log útil dice qué pasó y dónde mirar; un log peligroso pega la clave para ahorrarte una reproducción. Si necesitas correlacionar sin exponer, registra un hash corto, nunca el secreto entero.
Reúne las cuatro vías —bundle, asset, log y error— y verás que ninguna requiere malicia: todas nacen de la comodidad de tener el valor a mano y usarlo donde toca mirar. La disciplina, por eso, no consiste en desconfiar de agentes externos, sino de la propia prisa: casi siempre quien filtra el secreto es el yo apurado de un viernes por la tarde.
flowchart LR CLI[navegador] --> RT[ruta del worker en el servidor] RT --> SEC[lee env secreto] SEC --> TP[llama al tercero] TP --> RES[devuelve solo el resultado] RES --> CLI SEC -.fuga si el valor acaba en un asset o un log.-> CLI style SEC fill:#f38ba8,color:#11111b style RES fill:#a6e3a1,color:#11111b
Recoge aquí todo lo del nivel en un solo principio, porque es el que de verdad protege a tus usuarios. La seguridad de un secreto no vive en su cifrado ni en el almacén que lo guarda: vive en una frontera, la que separa el servidor del cliente, y en la disciplina de no cruzarla nunca con el valor en la mano. Cloudflare hace su parte perfectamente —env es del servidor, se inyecta en el runtime, no se serializa, no viaja—, pero esa garantía termina exactamente donde empieza tu código, porque tú eres el único que puede tomar el valor y copiarlo al otro lado. Por eso las fugas nunca son fallos de la plataforma: son props que pasaste a un componente de cliente, constantes que horneaste en un bundle, logs que imprimiste bajo presión, errores que reenviaste sin limpiar. La postura mental correcta no es preguntar si está bien guardado tu secreto, sino preguntar si existe algún camino, por retorcido que sea, por el que ese valor llegue a algo que el usuario descargue, y recorrer ese árbol de posibilidades hasta poder responder que no. Y cuando falles —porque algún día un secreto se te escapará—, interioriza la asimetría que gobierna este juego: proteger un secreto exige acertar en todos y cada uno de los caminos, mientras que comprometerlo solo exige que se filtre por uno. Esa asimetría es despiadada y define la única respuesta correcta ante una filtración: no discutas si alguien llegó a verlo, asume que sí, rótalo de inmediato y trata la credencial vieja como quemada. Un secreto que pudo descargarse ya no es un secreto, por muchas veces que te digas que seguramente nadie miró.
- Pasa deliberadamente un valor de
enva código de cliente o a un asset público, construye, y busca el valor en lo que sirve el navegador para verlo filtrado. - Corrige moviendo el uso del secreto a una ruta del Worker y haciendo que el cliente llame a esa ruta y reciba solo el resultado.
- Añade un
console.logque imprima un secreto, obsérvalo enwrangler tail, y reescríbelo para que registre el hecho sin el valor. - Revisa el manejo de errores de una llamada a un tercero: asegúrate de que ningún cuerpo de error que devuelves al cliente puede arrastrar la credencial. Documenta tu política de rotación ante una fuga.
- Añade a tu proyecto una comprobación automática —un escáner de secretos en pre-commit o en CI— que falle si detecta algo con forma de clave en el código o los assets, para que la máquina cace lo que a ti se te escape.