Secretos en despliegue: sin filtraciones al cliente
El riesgo central de las variables de entorno en producción: que un secreto acabe en el navegador. Por qué las props de una isla hidratada se serializan y son una vía de fuga, la diferencia entre inyectar el secreto en el build y en el runtime, y dónde vive cada credencial en los adapters de Node, Vercel, Netlify y Cloudflare, con sus almacenes cifrados y su alcance por entorno.
El único pecado capital: filtrar un secreto al cliente
En despliegue solo hay un error verdaderamente caro con las variables de entorno, y es que un secreto termine en el bundle que viaja al navegador. Todo lo demás se arregla; una credencial publicada hay que rotarla y asumir que alguien la vio. Las vías de fuga evidentes ya las cierran los mecanismos que estudiaste: una variable sin PUBLIC_ no se incrusta, un campo access: 'secret' no se exporta desde astro:env/client. Pero hay una vía sutil que ninguno de esos muros detiene, porque no pasa por ellos: las props de una isla.
---import Panel from '../components/Panel.jsx';import { API_SECRET } from 'astro:env/server';---<!-- FUGA: el secreto se serializa en el HTML para hidratar la isla --><Panel client:load token={API_SECRET} />Cuando marcas un componente de framework con client:load o cualquier directiva de hidratación, Astro tiene que enviar sus props al navegador para que el componente pueda revivir allí. Esas props se serializan dentro del HTML. Da igual que el valor viniera de astro:env/server y fuese un secreto legítimo del servidor: en el momento en que lo pasas como prop a una isla, lo estás copiando al cliente con tus propias manos. El secreto salió limpio del módulo correcto y se filtró en la línea siguiente.
Build-time frente a runtime: dos entornos, un mismo secreto
Hay una distinción que decide en qué máquina debe existir cada variable. Las públicas de cliente se incrustan durante el build: la máquina que compila necesita tenerlas, porque su valor queda horneado en los artefactos. Los secretos leídos en ejecución viven en el otro extremo: la máquina que sirve las peticiones es la que debe conocerlos, y a menudo la que compila ni siquiera debería verlos.
# El build incrusta las publicas: el CI necesita PUBLIC_*PUBLIC_API_URL=https://api.produccion.com astro build# El servidor en produccion inyecta los secretos en su propio entornoAPI_SECRET=... node ./dist/server/entry.mjsConfundir los dos entornos causa fallos desconcertantes. Un secreto presente en tu máquina pero ausente en el servidor de despliegue produce un sitio que compila sin quejarse y falla en la primera petición real. Una variable pública que olvidaste en el entorno del CI genera un build con un valor vacío incrustado que ninguna configuración del runtime podrá ya corregir, porque su hueco se cerró al compilar. La pregunta ¿esta variable la necesita quien compila o quien sirve? debe tener respuesta antes de desplegar.
Esta separación tiene una consecuencia práctica en tu integración continua. La máquina que compila necesita las variables públicas para hornearlas, pero no debería tener acceso a los secretos de producción si su único trabajo es construir el artefacto estático. Dar a un paso de build más credenciales de las que su tarea exige es ampliar la superficie de ataque sin ganar nada: cada sitio que toca un secreto es un sitio más que hay que proteger y auditar.
Dónde viven los secretos en cada plataforma
Un secreto nunca se escribe en tu repositorio: se guarda en el almacén que ofrece la plataforma de despliegue, y el adapter se encarga de que getSecret y las importaciones de servidor lo encuentren allí. El sitio concreto cambia según dónde despliegues.
Estas diferencias explican, retroactivamente, por qué getSecret recibe su valor a través del adapter en lugar de leer siempre process.env. En Node el secreto está en el proceso; en Cloudflare no hay proceso persistente, sino un objeto de entorno que la plataforma pasa a cada invocación del Worker. Escribes el mismo getSecret('API_SECRET') en ambos casos, y es el adapter quien traduce esa intención al mecanismo concreto de la plataforma. Tu código de lectura permanece portable justamente porque el conocimiento sobre dónde viven los secretos está encapsulado en el adapter, no esparcido por tus endpoints.
Esa misma indirección del adapter opera también en tu máquina, no solo en el servidor de despliegue. En desarrollo, los secretos viven en archivos .env que nunca se versionan, y cada adapter tiene su forma de leerlos en local para que astro dev se parezca al despliegue. El servidor Node lee el .env durante el desarrollo; el adapter de Cloudflare, que en producción recibe los secretos por el objeto de entorno del Worker, los toma en local de un archivo aparte que su herramienta reconoce. Conocer el mecanismo de tu adapter evita que lo que funciona en tu portátil se rompa al desplegar por leer de un sitio distinto.
Vías de fuga menos obvias: registros y props
Hay una vía de fuga que no pasa por el navegador y que los muros anteriores no cubren: los registros. Un secreto que imprimes por consola para depurar, o que acaba dentro del objeto de un error que tu telemetría captura, se copia a un almacén de logs que suele tener menos protección y más lectores que tu base de datos. Un secreto en un log es un secreto filtrado a un lugar que casi nadie vigila.
// FUGA silenciosa: el secreto viaja al almacen de logsconsole.log('conectando con', API_SECRET);// Mejor: registra que ocurrio, nunca el valor sensibleconsole.log('conectando con el servicio de pagos');La regla es fácil de enunciar y fácil de olvidar bajo presión: los valores sensibles no se imprimen, no se serializan en mensajes de error y no se incluyen en los datos que mandas a tu telemetría. Cuando depures un fallo de autenticación, registra el hecho —la firma no coincidió— y no el material —la clave era tal—. El primer dato ayuda a diagnosticar; el segundo solo añade un sitio más del que tendrás que rotar el secreto en cuanto alguien lo lea.
La otra vía menos obvia ya asomó al principio —las props de una isla—, y su corrección merece un patrón propio, porque es la solución canónica a una parte del cliente necesita algo que solo el servidor puede tener. En vez de entregar el secreto a la isla, interpones un endpoint tuyo: la isla le pide el dato, el endpoint —código de servidor— lee el secreto, habla con el tercero y devuelve solo el resultado. El secreto nunca sale del servidor; lo que viaja al cliente es una respuesta ya cocinada.
// src/pages/api/precio.ts -> el endpoint guarda el secretoimport { getSecret } from 'astro:env/server';export const GET = async () => { const clave = getSecret('PRICING_KEY'); const datos = await fetch('https://tercero.example/precio', { headers: { authorization: `Bearer ${clave}` }, }).then((r) => r.json()); return Response.json({ precio: datos.precio }); // solo el resultado, no la clave};La isla, en el navegador, hace un fetch a /api/precio y recibe el precio sin enterarse jamás de la credencial que lo autorizó. Este rodeo —cliente pide, servidor guarda y traduce— es el mismo que sostiene cualquier arquitectura sana: el material sensible vive de un solo lado de la frontera y solo la cruza el resultado, nunca la llave. Interiorizarlo te ahorra la tentación recurrente de pasarle el token a la isla para que sea más simple, que es simple justo hasta el día en que alguien abre las herramientas del navegador.