Cuando una aplicación empieza a utilizar APIs, una de las primeras preocupaciones suele ser dónde guardar la clave de acceso al proveedor. La respuesta parece sencilla: nunca dentro de la app.
Mover esa clave a un servidor intermedio resuelve una parte importante del problema, pero no toda. En ese momento aparece una nueva pregunta: si la aplicación necesita acceder a ese servidor, ¿qué impide que otra persona descubra su dirección y empiece a utilizarlo también?
Ahí comienza realmente el trabajo de securización.
En este artículo veremos cómo proteger de forma razonable una arquitectura formada por una app móvil, un servicio intermedio y una API de inteligencia artificial, intentando mantener un equilibrio entre seguridad, simplicidad, privacidad y mantenimiento.
La idea no es construir una fortaleza innecesariamente compleja, sino aplicar pequeñas capas de protección que reduzcan de forma significativa el riesgo de abuso sin perjudicar a los usuarios legítimos ni convertir el proyecto en algo difícil de mantener.
Capítulo 1. ¿Por qué necesitábamos Cloudflare?
En una aplicación móvil es muy habitual necesitar acceder a servicios externos mediante una API. Estos servicios pueden proporcionar inteligencia artificial, información meteorológica, traducciones, datos financieros o prácticamente cualquier otra funcionalidad disponible a través de Internet.
Normalmente, para utilizar uno de estos servicios necesitamos algún tipo de credencial, como una API key. Esa clave identifica nuestra aplicación o nuestra cuenta ante el proveedor del servicio.
El problema aparece cuando desarrollamos una aplicación Android. Podríamos pensar que basta con guardar la clave dentro del código:
String API_KEY = "nuestra_clave";
o almacenarla en algún fichero de configuración incluido en la aplicación, pero hay un inconveniente importante: una aplicación Android instalada en el teléfono del usuario no es un lugar seguro para guardar secretos.
Aunque compilemos la aplicación, ofusquemos el código o intentemos ocultar la clave, alguien suficientemente interesado puede analizar el APK y recuperarla. Y si obtiene esa clave, podría utilizarla desde fuera de nuestra aplicación.
Nuestro esquema inicial sería algo parecido a:
La clave viaja desde la propia aplicación y, por tanto, tiene que estar disponible en algún lugar dentro del APK o del dispositivo. Eso es precisamente lo que queríamos evitar.
La solución que buscábamos: Necesitábamos introducir un intermediario:
│
│ petición normal
▼
Servidor intermedio
│
│ API key protegida
▼
Servicio externo
La aplicación ya no necesita conocer la clave. Solo sabe que tiene que llamar a una dirección nuestra. Ese servidor recibe la petición, utiliza internamente la credencial necesaria para acceder al proveedor y devuelve el resultado a la aplicación.
Hasta aquí, la solución es bastante tradicional: podríamos contratar un servidor, instalar un sistema operativo, configurar un servidor web, mantenerlo actualizado, protegerlo, monitorizarlo, etc. Pero para una tarea tan pequeña aquello sería desproporcionado.
Nuestro servidor solamente tenía que hacer algo parecido a esto:
↓
Comprobarla
↓
Añadir una credencial secreta
↓
Llamar al servicio externo
↓
Devolver la respuesta
No necesitábamos una máquina completa funcionando las 24 horas. Y aquí es donde entra Cloudflare Workers.
¿Qué es Cloudflare?
Cloudflare es una empresa que ofrece una gran infraestructura distribuida por Internet. Entre sus numerosos servicios dispone de Cloudflare Workers.
Un Worker es, simplificando bastante, un pequeño programa que podemos ejecutar en la infraestructura de Cloudflare sin administrar nosotros un servidor.
- Servidor
- Sistema operativo
- Apache/Nginx
- Actualizaciones
- Puertos
- Certificados HTTPS
- Máquina virtual
escribimos simplemente una función. Por ejemplo:
async fetch(request) {
return new Response("Hola");
}
};
Cloudflare se encarga de proporcionar el entorno necesario para ejecutarla cuando alguien accede a su dirección. Este modelo suele denominarse serverless.
No significa que no existan servidores. Evidentemente los hay. Lo que significa es que nosotros no tenemos que administrarlos.
Para nuestro proyecto es justamente lo que necesitábamos.
Nuestra primera entrada en Cloudflare
Accedemos a Cloudflare y creamos una cuenta gratuita en https://dash.cloudflare.com/sign-up. Yo utilizaré una dirección de correo electronico y un password aunque también puedes acceder a través de tu cuenta de Google, Apple o Github.
Después de crear una cuenta gratuita llegamos al panel principal de Cloudflare.
- dominios;
- almacenamiento;
- servicios de IA;
- redes;
- seguridad;
- aplicaciones;
- Workers.
Pero nosotros no queremos configurar un dominio ni montar una web. Queremos ejecutar un pequeño programa accesible desde Internet. Por eso seleccionamos:
Enviar algo nuevo → Crear aplicación
Esta decisión es importante porque define lo que vamos a construir. No estamos creando una página web tradicional. Estamos creando lo que podríamos imaginar como una pequeña API propia. La idea final será esta:
Aplicación
Electro Ahorro
│
│ HTTPS
▼
┌──────────────────────┐
│ Cloudflare Worker │
│ │
│ nuestro código │
│ nuestros controles │
│ nuestros secretos │
└──────────┬───────────┘
│
│ HTTPS
▼
Servicio externo
El Worker se convierte así en una especie de puerta de entrada controlada por nosotros. Y eso tiene una ventaja adicional que al principio puede pasar desapercibida. La aplicación ya no necesita saber exactamente qué hay detrás.
Hoy podría ser:
Aplicación → Worker → Servicio A
y mañana:
Aplicación → Worker → Servicio B
La dirección que utiliza Android podría seguir siendo exactamente la misma.
Esto desacopla nuestra aplicación del proveedor externo y nos da mucha más libertad para cambiar la infraestructura posteriormente.
Capítulo 2. Crear nuestro primer Worker: ¿qué estamos haciendo realmente?
Una vez dentro de Cloudflare y después de elegir Crear aplicación, llegamos a una pantalla con varias posibilidades para empezar el proyecto. Entre ellas aparecían opciones para conectar repositorios como GitHub o GitLab, utilizar plantillas y una opción mucho más sencilla:
¡Comienza con Hola Mundo!
La razón es que en este momento no necesitamos ninguna infraestructura especial.
No vamos a desplegar un proyecto existente desde GitHub, ni necesitábamos una plantilla compleja. Queremos empezar con el Worker más pequeño posible, comprobar cómo funciona y modificarlo nosotros poco a poco.
¿Qué significa realmente “Hola Mundo”?
En programación, un Hello World suele ser el programa más sencillo que puede ejecutarse en una plataforma. Su objetivo no es hacer nada útil todavía, sino verificar algo mucho más básico:
↓
¿Puedo desplegarlo?
↓
¿Cloudflare puede ejecutarlo?
↓
¿Puedo acceder a él desde Internet?
Eso era exactamente lo que nos interesa comprobar primero.
Elegir ¡Comienza con Hola Mundo! hace que Cloudflare cree por nosotros una aplicación mínima y funcional.
No estamos instalando un servidor. No estamos contratando una máquina virtual. No estamos configurando Apache, PHP, Tomcat ni nada parecido. Estamos creando un pequeño programa que Cloudflare ejecutará cuando alguien acceda a su dirección.
El concepto más importante: una petición entra y una respuesta sale.
Para entender un Worker conviene olvidarse por un momento de Cloudflare. Imaginemos que alguien accede a:
https://miservicio.example/
Se produce una petición HTTP. Nuestro programa recibe esa petición y decide qué responder.
Conceptualmente:
│
│ petición HTTP
▼
┌─────────────────────┐
│ Nuestro programa │
│ │
│ ¿Qué me están │
│ pidiendo? │
└──────────┬──────────┘
│
│ respuesta HTTP
▼
Usuario o aplicación
En un Worker, la función fundamental gira precisamente alrededor de esa petición. El ejemplo inicial de Cloudflare es equivalente conceptualmente a algo como:
async fetch(request) {
return new Response("Hello World!");
}
};
La parte importante es fetch(request). Podemos interpretarlo como: «Cada vez que llegue una petición a este Worker, ejecuta este código».
Y return new Response(...) significa: «Esto es lo que devolveremos a quien realizó la petición».
Todavía no hay claves, bases de datos ni Android. Solo existe:
Petición → Worker → Respuesta
Y comprender esto hace mucho más fácil entender todo lo que construiremos después.
Elegimos un nombre para nuestro Worker
Durante la creación asignamos al Worker el nombre:
electro-ahorro
El nombre identifica nuestro pequeño servicio dentro de Cloudflare. Cloudflare terminó proporcionándonos una dirección pública similar a esta:
https://electro-ahorro.miguel.workers.dev/
Aquí aparece otro concepto importante. ¿Qué es workers.dev?
Cloudflare proporciona un dominio para que podamos publicar Workers sin necesidad de comprar ni configurar nuestro propio dominio. En nuestro caso:
workers.dev
es el dominio proporcionado por Cloudflare.
Nuestra dirección queda formada aproximadamente así:
+
subdominio de nuestra cuenta
+
workers.dev
Para nuestras primeras pruebas esto es especialmente cómodo porque ya tenemos una dirección HTTPS pública sin tener que configurar certificados ni servidores.
Desde cualquier cliente capaz de hacer una petición HTTPS podemos llamar al Worker:
- Navegador
- Android
- curl
- Postman
- otro servidor
Una decisión que puede llamar la atención: no protegerlo con Cloudflare Access.
Durante la creación aparecía también una opción parecida a: Protect with Cloudflare Access La dejamos desactivada.

A primera vista puede parecer contradictorio. Nuestro objetivo era mejorar la seguridad y, sin embargo, dejamos desactivada una opción que dice «proteger». La explicación está en qué queremos proteger y quién debe poder acceder al Worker.
Cloudflare Access está pensado para controlar quién puede entrar en determinados recursos mediante mecanismos de autenticación y políticas de acceso.
Eso es muy útil, por ejemplo, para:
↓
Inicio de sesión
↓
Solo empleados autorizados
Pero nuestra situación es diferente. Nuestro Worker tendrá que ser llamado por una aplicación Android instalada en los teléfonos de los usuarios:
↓
Worker
No queremos que cada petición obligue al usuario a iniciar sesión previamente en Cloudflare. Por eso Cloudflare Access no era el mecanismo adecuado para esta comunicación.
Eso no significa que vayamos a dejar el servicio sin protección. Más adelante aplicaremos controles adecuados para una API pública.
Es una distinción importante: Públicamente accesible no significa necesariamente desprotegido.
La dirección puede ser accesible desde Internet y, aun así, nuestro código puede controlar qué peticiones acepta, cuánto se puede enviar, qué operaciones permite y cómo limitar posibles abusos.
Creamos el Worker
Al confirmar la creación, Cloudflare realiza el despliegue. Este término —desplegar— aparecerá muchas veces. En este contexto significa básicamente: Tomar la versión de nuestro código y hacer que sea la versión que Cloudflare ejecuta públicamente.
Podemos imaginar dos estados:
│
│ código que estamos preparando
│
▼
[DESPLEGAR]
│
▼
Internet
│
│ versión activa
▼
usuarios
Modificar el código en el editor no siempre significa que los usuarios estén ejecutando inmediatamente esa modificación. El botón Desplegar publica esa nueva versión.
Nuestro primer Worker ya estaba en Internet
Nota sobre este artículo: no vamos a explicar cómo se programa un Worker ni a analizar su código. El objetivo es entender la arquitectura, las opciones que hemos utilizado en Cloudflare y por qué las hemos elegido.
Después del despliegue, el Worker queda operativo. En ese momento todavía hace muy poco, pero hemos conseguido algo importante:
↓
infraestructura de Cloudflare
↓
dirección HTTPS pública
↓
respuesta desde Internet
Y todo ello sin haber configurado:
- IP pública
- servidor Linux
- firewall
- puerto 443
- certificado TLS
- servidor web
- DNS propio
Cloudflare se estaba ocupando de esa infraestructura pero ¿Dónde está físicamente nuestro Worker?
Aquí merece la pena aclarar una idea. Cuando utilizamos un servidor tradicional solemos pensar en una máquina concreta:
- Servidor contratado
- IP determinada
- Centro de datos determinado
Los Workers siguen otro modelo. Cloudflare dispone de infraestructura distribuida y ejecuta estos programas dentro de su plataforma. Nosotros trabajamos con el servicio lógico electro-ahorro y no con una máquina que tengamos que mantener.
Por eso una de las ventajas del modelo serverless es precisamente abstraernos de esa infraestructura.
Para nuestro proyecto queríamos escribir:
- recibir petición
- procesarla
- llamar a otro servicio
- devolver respuesta
y no administrar servidores.
Una primera pieza del rompecabezas
Al terminar este paso todavía no hemos solucionado el problema de la clave secreta, pero ya tenemos creada la pieza que necesitábamos para resolverlo.
Antes:
Ahora ya podíamos construir:
│
│ sin conocer la clave
▼
Cloudflare Worker
│
│ clave protegida aquí
▼
Servicio externo
El siguiente problema era, precisamente, ¿Cómo guardamos un secreto dentro de Cloudflare sin escribirlo en nuestro código?
Y esa fue una de las partes más interesantes de todo el proceso.
Capítulo 3. Guardar una credencial de forma segura en Cloudflare
En este punto ya tenemos creado nuestro Worker y una dirección pública desde la que podía responder a peticiones, pero todavía falta resolver el problema que había motivado todo este despliegue: necesitamos utilizar una credencial para acceder a un servicio externo, pero no queremos incluirla dentro de la aplicación Android.
Ese era, en realidad, uno de los principales motivos para introducir Cloudflare entre la aplicación y el proveedor: El problema de guardar una clave en una aplicación
Una aplicación Android acaba distribuyéndose como un APK o un App Bundle. Aunque el código se compile y pueda ofuscarse, debemos asumir que un usuario con suficientes conocimientos puede inspeccionarlo. Por eso no es buena idea incluir una credencial privada directamente dentro de la aplicación.
Nuestro objetivo era pasar de esto:
│
│ contiene la credencial
▼
Servicio externo a esto:
Aplicación Android
│
│ no conoce la credencial
▼
Cloudflare Worker
│
│ utiliza la credencial
▼
Servicio externo
La aplicación solamente necesita conocer la dirección del Worker. La credencial queda fuera del teléfono.
Variables y secretos en tiempo de ejecución
Dentro de la configuración del Worker entramos en:
Ajustes → Variables y secretos en tiempo de ejecución
Cloudflare distingue, entre otras posibilidades, entre una variable normal y un secreto. La diferencia es importante.
Una variable normal puede utilizarse para valores que no necesitan permanecer ocultos, por ejemplo:
- Nombre de un entorno
- Una opción de configuración
- Una URL pública
- Un indicador de funcionamiento
Pero una credencial privada no pertenece a esa categoría. Para ella utilizamos un Secreto.
Creamos el secreto
En nuestro caso añadimos una nueva entrada con:
- Tipo: Secreto
- Nombre: GEMINI_API_KEY
- Valor: nuestra clave privada
Lo realmente importante es el tipo: Secreto.
No queríamos una simple variable de configuración, sino un valor que Cloudflare tratase específicamente como información sensible.
¿Qué significa “Valor cifrado”?
Después de guardar el secreto, Cloudflare ya no nos mostró la credencial. En su lugar aparecía Valor cifrado.
Esta pantalla resulta especialmente importante para entender qué hemos conseguido. La clave existe y el Worker puede utilizarla cuando se ejecuta, pero no necesitamos escribirla dentro del código ni entregársela a la aplicación Android. Además, el panel no vuelve a mostrarnos alegremente el valor introducido.
Desde el punto de vista de nuestra arquitectura, hemos trasladado el secreto desde un lugar poco apropiado, una APK instalado en miles de dispositivos a un entorno que controlamos, una Configuración del Worker en Cloudflare
Ese cambio es mucho más importante que la tecnología concreta a la que accedamos con esa clave. Hoy estamos utilizando una credencial para un determinado servicio, pero el mismo planteamiento serviría para muchos otros casos.
¿Por qué no escribir la clave directamente en el Worker?
Podríamos haber pensado en introducir la credencial directamente dentro del programa del Worker. Funcionalmente podría funcionar pero sería una mala práctica.
En ese caso estaríamos mezclando:
+
Configuración
+
Secretos
y aumentaríamos el riesgo de que la credencial apareciese accidentalmente en un repositorio, una copia de seguridad, una captura de pantalla o cualquier otro lugar por el que pasase el código.
Con un secreto de Cloudflare separamos ambas cosas:
│
│ referencia al secreto
▼
Configuración protegida de Cloudflare
│
▼
Credencial real
El programa sabe que existe una determinada credencial, pero no necesita contenerla. Esta separación es una práctica muy habitual cuando se trabaja con servicios en la nube.
¿Significa esto que nuestra API ya es completamente segura?
No. Y esta distinción es importante. Hemos protegido la credencial del servicio externo pero nuestro Worker tiene una dirección pública. Eso significa que alguien que descubriese esa dirección podría intentar realizar peticiones contra ella.
Por tanto, existen dos problemas distintos:
- Proteger la credencial privada
- Proteger nuestro servicio frente a posibles abusos
En este capítulo hemos solucionado el primero. El segundo lo abordaremos más adelante mediante controles adicionales.
Que la clave esté protegida no significa automáticamente que el endpoint público no pueda ser utilizado por terceros.
Una ventaja adicional: podemos cambiar la clave sin actualizar Android
Esta arquitectura tiene además una consecuencia muy práctica.
Imaginemos que algún día necesitamos sustituir la credencial. Si estuviera integrada dentro de la aplicación tendríamos que:
↓
Generar una nueva versión
↓
Publicarla
↓
Esperar a que los usuarios actualicen
Al estar almacenada en Cloudflare podemos cambiarla allí. La aplicación Android sigue llamando al mismo Worker y no necesita conocer el cambio. Es decir:
│
│ misma dirección
▼
Worker
│
│ nueva credencial
▼
Servicio externo
Esto no solo mejora la seguridad. También facilita mucho el mantenimiento.
Lo que habíamos conseguido hasta este punto
Nuestro sistema empieza ya a tomar forma:
│ Aplicación Android │
│ │
│ No contiene la clave │
└───────────┬───────────┘
│
│ HTTPS
▼
┌───────────────────────┐
│ Cloudflare Worker │
│ │
│ Accede a un secreto │
│ almacenado en │
│ Cloudflare │
└───────────┬───────────┘
│
│ credencial
▼
┌───────────────────────┐
│ Servicio externo │
└───────────────────────┘
Todavía no sabemos si todo ese recorrido funcionaba realmente. Hemos creado el Worker. Hemos guardado el secreto. Pero falta comprobar una cosa fundamental:
¿Puede nuestro Worker recuperar ese secreto y utilizarlo realmente para comunicarse con el servicio externo?
Eso es exactamente lo que comprobamos en el siguiente paso.
Capítulo 4. Primera prueba real: comprobar que el Worker puede actuar como intermediario
Hasta este momento hemos hecho tres cosas:
- creado una cuenta en Cloudflare;
- creado nuestro primer Worker;
- guardado una credencial privada como secreto.
Pero todavía nos falta comprobar lo más importante: que todas esas piezas funcionaban juntas. Queremos verificar este recorrido completo:
│
▼
Cloudflare Worker
│
│ utiliza el secreto
▼
Servicio externo
│
▼
Cloudflare Worker
│
▼
Respuesta en el navegador
En esta ocasión sí vamos a mostrar el código utilizado, porque se trata de una prueba muy pequeña y ayuda a comprender qué hace realmente un Worker.
No entraremos en cómo se programa Cloudflare Workers ni en JavaScript en general. Aprender a programar Workers queda fuera del objetivo de este artículo. Solo analizaremos las partes necesarias para entender nuestra prueba.
El editor de Cloudflare
Cloudflare incluye un editor desde el que podemos modificar directamente el código del Worker y desplegar una nueva versión.
Para nuestra primera prueba sustituimos el ejemplo Hello World por un pequeño programa con dos funciones:
- /: serviría para comprobar que el Worker estaba funcionando.
- /test: intentaría utilizar el secreto que habíamos almacenado para realizar una petición real al servicio externo.
El código utilizado es este:
async fetch(request, env) {
const url = new URL(request.url);
// Página principal: comprueba únicamente que el Worker funciona.
if (request.method === "GET" && url.pathname === "/") {
return jsonResponse({
ok: true,
servicio: "Electro Ahorro - Nova",
estado: "Worker activo"
});
}
// Prueba temporal de conexión con el servicio externo.
if (request.method === "GET" && url.pathname === "/test") {
if (!env.GEMINI_API_KEY) {
return jsonResponse({
ok: false,
error: "No se encuentra el secreto GEMINI_API_KEY."
}, 500);
}
try {
const response = await fetch(
"https://generativelanguage.googleapis.com/v1beta/models/gemini-3.5-flash-lite:generateContent",
{
method: "POST",
headers: {
"Content-Type": "application/json",
"x-goog-api-key": env.GEMINI_API_KEY
},
body: JSON.stringify({
contents: [{
role: "user",
parts: [{
text: "Responde únicamente con la palabra OK."
}]
}]
})
}
);
const data = await response.json();
if (!response.ok) {
return jsonResponse({
ok: false,
codigo: response.status,
error: data
}, response.status);
}
const texto = data?.candidates?.[0]?.content?.parts?.[0]?.text?.trim() || "";
return jsonResponse({
ok: true,
modelo: "gemini-3.5-flash-lite",
respuesta: texto
});
} catch (error) {
return jsonResponse({
ok: false,
error: "Error de conexión con el servicio externo."
}, 500);
}
}
return jsonResponse({
ok: false,
error: "Ruta no encontrada."
}, 404);
}
};
function jsonResponse(data, status = 200) {
return new Response(JSON.stringify(data, null, 2), {
status,
headers: {
"Content-Type": "application/json; charset=utf-8",
"Cache-Control": "no-store"
}
});
}
No necesitamos comprender cada símbolo para entender lo importante.
¿Qué hace la primera parte?
Esta condición:
if (request.method === "GET" && url.pathname === "/")
indica que, cuando alguien accede a la dirección principal del Worker, queremos devolver una respuesta muy sencilla. El resultado esperado es:
"ok": true,
"servicio": "Electro Ahorro - Nova",
"estado": "Worker activo"
}
Con esta prueba todavía no estábamos utilizando ninguna credencial ni accediendo a ningún servicio externo. Solo queremos verificar:
↓
Cloudflare
↓
Nuestro Worker
↓
Respuesta
Es una prueba básica, pero muy útil. Si esta parte falla, no tiene sentido investigar todavía el resto.
¿Qué significa /test?
La segunda dirección es /test. Una dirección como esta suele denominarse una ruta o endpoint.
Nuestra dirección principal era:
https://electro-ahorro....workers.dev/
y al añadir test obtenemos:
https://electro-ahorro....workers.dev/test
El mismo Worker puede, por tanto, comportarse de distintas maneras dependiendo de la ruta utilizada. Podemos imaginarlo como distintas puertas de entrada:
│
├── /
│ └── comprobar que funciona
│
└── /test
└── probar conexión externa
Más adelante utilizaremos esta misma idea para ofrecer diferentes servicios desde un único Worker.
La parte fundamental: acceder al secreto
La línea realmente importante para nuestro objetivo es esta:
env.GEMINI_API_KEY
Recordemos que nosotros hemos creado en Cloudflare un secreto llamado GEMINI_API_KEY. El Worker puede acceder a ese valor durante su ejecución mediante el entorno que Cloudflare le proporciona.
Lo importante es lo que no aparece en el código:
AIza............
La clave real no está escrita allí.
El programa únicamente conoce el nombre del secreto GEMINI_API_KEY. Cloudflare se ocupa de proporcionar internamente su valor cuando el Worker se ejecuta. Esta era precisamente la separación que buscábamos:
CÓDIGO: "Utiliza GEMINI_API_KEY" por un lado y, por otro lado CLOUDFLARE
↓
valor secreto real
Una prueba deliberadamente sencilla
Para comprobar la conexión podemos hacer algo extremadamente simple. El Worker enviaría al servicio externo esta petición: Responde únicamente con la palabra OK.
No pretendíamos comprobar todavía desarrollar ninguna funcionalidad de nuestra aplicación. Solo queríamos saber:
↓
¿Puede conectarse con el proveedor?
↓
¿El proveedor acepta la credencial?
↓
¿Puede devolverse la respuesta?
Si la respuesta final era OK tendríamos comprobada toda la cadena.
Desplegar no es lo mismo que editar
Después de introducir el código tenemos que pulsar Desplegar. Este paso merece atención, especialmente para quien utiliza Cloudflare por primera vez.
Podemos modificar el código en el editor, pero esos cambios no tienen por qué ser todavía los que estén ejecutándose públicamente. El proceso es:
↓
Desplegar
↓
Nueva versión activa
Primera comprobación: el Worker estaba activo
Probamos primero la dirección principal del Worker. La respuesta es:
"ok": true,
"servicio": "Electro Ahorro - Nova",
"estado": "Worker activo"
}
Esto confirma que:
↓
Cloudflare
↓
Internet
funciona correctamente. Ya podemos pasar a la prueba interesante.
Segunda comprobación: utilizar el secreto
A continuación accedemos a /test. El resultado es:
"ok": true,
"modelo": "gemini-3.5-flash-lite",
"respuesta": "OK"
}
Esta captura representa uno de los momentos más importantes de todo el proceso, no porque la respuesta «OK» sea especialmente interesante, sino porque demuestra que detrás de esas dos letras han sucedido bastantes cosas:
- Nuestro navegador realizó una petición.
- Cloudflare recibió la petición.
- Cloudflare ejecutó nuestro Worker.
- El Worker recuperó el secreto.
- El Worker utilizó ese secreto para autenticarse ante el servicio externo.
- El servicio externo procesó la petición.
- La respuesta volvió al Worker.
- El Worker nos devolvió el resultado.
Y en ningún momento nuestro navegador había necesitado conocer la clave privada.
Lo que habíamos conseguido
Volvamos al problema del principio. Antes teníamos:
│
│ necesita conocer la clave
▼
Servicio externo
Ahora hemos demostrado que era posible utilizar:
│
│ petición HTTPS
▼
Cloudflare Worker
│
│ secreto almacenado
│ en Cloudflare
▼
Servicio externo
Nuestra aplicación podría limitarse a hablar con el Worker. La clave privada permanecería fuera del APK.
¿Está terminado?
No. Y es importante no confundir dos conceptos.
Hasta aquí hemos solucionado: Cómo evitar incluir la credencial privada del proveedor dentro de nuestra aplicación.
Pero todavía queda otro problema: La dirección del Worker es pública:
Una persona podría descubrirla inspeccionando el tráfico de la aplicación o analizando el APK, y aunque no consiguiese la clave privada, podría intentar utilizar directamente nuestro Worker.
Es decir, hemos protegido nuestra API KEY pero todavía tenemos que proteger adecuadamente nuestro WORKER PÚBLICO
Capítulo 5: Securizar un intermediario entre una app móvil y una API de inteligencia artificial
Cuando una aplicación móvil utiliza una API de inteligencia artificial aparece un problema bastante evidente: la clave de acceso al proveedor no debería almacenarse dentro de la aplicación.
Una APK puede analizarse, descompilarse o ejecutarse bajo herramientas de instrumentación. La ofuscación puede dificultar el trabajo, pero no convierte una clave incluida en la aplicación en un secreto realmente seguro. Por este motivo, una arquitectura bastante habitual consiste en introducir un pequeño servicio intermedio:
↓
Servidor / función serverless
↓
API de inteligencia artificial
La aplicación ya no conoce la clave del proveedor. Solo conoce la dirección del servicio intermedio, mientras que la credencial permanece almacenada como secreto en el servidor.
Esto resuelve el problema principal, pero introduce otro: el servidor intermedio pasa a ser una API pública.
Ocultar la clave no basta
Supongamos que una aplicación Android utiliza un Worker, una función serverless o cualquier pequeño backend para acceder a un modelo de IA. La aplicación envía algo similar a:
POST /chat
y el servidor utiliza internamente su clave para realizar la consulta al proveedor.
La API key está perfectamente protegida porque nunca llega al dispositivo, pero cualquiera que descubra la URL del servidor podría intentar llamar directamente al endpoint.
No necesita robar la clave. Le basta con utilizar el servidor del desarrollador como intermediario.
El riesgo, por tanto, cambia:
- Antes: robar la API key
- Después: abusar del servicio que posee la API key
Esta diferencia es importante porque obliga a pensar la seguridad por capas.
Primera capa: limitar lo que acepta el servidor
Una API pública nunca debería aceptar cantidades arbitrarias de información. Un error relativamente frecuente consiste en hacer algo parecido a:
if (data.text.length > 16000) {
// rechazar
}
A primera vista parece correcto, pero hay un detalle: el cuerpo completo ya ha sido recibido y procesado antes de comprobar su tamaño.
Un cliente malicioso podría enviar cuerpos enormes aunque la aplicación legítima nunca lo hiciese. Una protección más adecuada consiste en imponer primero un límite de bytes:
↓
¿Tamaño excesivo?
├── Sí → rechazar
↓
Leer cuerpo
↓
¿Supera el máximo durante la lectura?
├── Sí → cancelar
↓
Parsear JSON
Después pueden mantenerse límites funcionales adicionales, como:
- Pregunta: máximo X caracteres
- Contexto: máximo Y caracteres
Son dos controles diferentes:
- el límite de bytes protege el servidor;
- el límite de caracteres protege la lógica de la aplicación.
Segunda capa: separar operaciones públicas y administrativas
No todas las operaciones de una API tienen el mismo nivel de riesgo. Imaginemos un sistema RAG en el que el endpoint de embeddings acepta:
type=query
para las consultas normales de los usuarios, pero también:
type=document
para generar los embeddings de los documentos de conocimiento. La primera función necesita estar disponible para la aplicación. La segunda quizá solo se utilice unas pocas veces al año desde el ordenador del desarrollador. No tiene sentido dejar ambas con el mismo nivel de acceso.
Una solución sencilla consiste en exigir una credencial administrativa únicamente para las operaciones de mantenimiento:
↓
type=query
↓
permitido
Administrador
↓
type=document
↓
requiere credencial
En este caso sí tiene sentido utilizar un secreto convencional porque no se distribuye dentro de la APK. Puede permanecer exclusivamente en el servidor y utilizarse manualmente cuando sea necesario realizar tareas administrativas.
La aplicación móvil nunca necesita conocerlo.
El secreto administrativo tampoco debe estar en los scripts
Mover una clave fuera de Android para terminar escribiéndola dentro de un script tampoco es una buena solución. Por ejemplo:
$secret = "mi_clave_super_secreta"
terminará probablemente en una copia de seguridad, un repositorio, una carpeta compartida o un fichero olvidado.
Una alternativa mucho más sencilla consiste en que la herramienta administrativa solicite la credencial cuando realmente la necesite:
│
├── No → utilizar caché
│
└── Sí → solicitar secreto
La contraseña puede introducirse de forma oculta durante la ejecución y utilizarse únicamente en memoria.
Esto resulta especialmente útil en procesos incrementales. Si una base de conocimiento contiene cientos de embeddings y solo cambia uno, no debería ser necesario regenerar los demás ni solicitar credenciales si no se va a realizar ninguna operación remota.
No devolver errores internos del proveedor
Otro pequeño detalle importante son los mensajes de error.
Un servicio intermedio puede recibir del proveedor de IA mensajes bastante detallados:
- modelo inexistente
- cuota agotada
- credencial inválida
- proyecto no autorizado
- endpoint incorrecto
- ...
Devolver esos mensajes directamente al cliente rara vez aporta algo útil. Es preferible traducirlos a respuestas públicas neutras:
- 429: El servicio está recibiendo demasiadas solicitudes.
- 503: El servicio de inteligencia artificial no está disponible temporalmente.
El servidor puede conservar el código HTTP que permita diagnosticar el problema, pero no necesita exponer toda la información interna del proveedor.
Los logs también forman parte de la seguridad
Durante el desarrollo resulta muy tentador escribir:
console.log(context);
console.log(response);
Es extremadamente cómodo para depurar. También puede terminar creando accidentalmente un historial de conversaciones de los usuarios.
En un asistente basado en IA conviene establecer desde el principio una regla muy sencilla:
- Registrar:
- códigos HTTP
- errores técnicos
- tiempos
- estado de las operaciones
- No registrar:
- preguntas
- respuestas
- contexto RAG
- credenciales
Los servicios de infraestructura ya suelen almacenar algunos metadatos técnicos de cada petición. No es necesario añadir nosotros mismos el contenido de las conversaciones.
Un servidor público seguirá siendo público
Hay que aceptar una realidad incómoda: si una aplicación móvil necesita llamar a un servidor sin que el usuario tenga que iniciar sesión, la dirección de ese servidor terminará siendo conocida.
Ocultarla dentro de la APK no es seguridad. Fragmentar la cadena tampoco. Cambiarle el nombre tampoco. Ofuscarla únicamente aumenta un poco el esfuerzo necesario para encontrarla.
Lo mismo ocurre con un supuesto secreto común incluido dentro de la aplicación:
X-App-Key: valor_secreto
Si todas las instalaciones llevan el mismo valor, alguien suficientemente interesado puede extraerlo y reproducir las peticiones. Puede servir como obstáculo menor, pero no debería considerarse autenticación fuerte.
Rate limiting: limitar el daño, no autenticar al usuario
Una de las protecciones más útiles contra abuso automatizado es limitar el número de peticiones.
Por ejemplo:
/embed → máximo M peticiones por minuto
Esto puede detener fácilmente un script que intente realizar miles de consultas.
Pero el rate limiting tiene un problema cuando se basa exclusivamente en la dirección IP. Una IP no equivale necesariamente a una persona.
En redes móviles, CGNAT, empresas, universidades, VPN o proxies, decenas o cientos de usuarios pueden compartir la misma dirección pública. Por eso los límites deben ser suficientemente holgados.
Su función no debería ser contar exactamente cuántas preguntas realiza cada usuario sino detectar comportamientos claramente anormales.
El rate limiting es una mitigación de abuso, no una autenticación.
Evitar convertir el backend en un proxy genérico
Existe otro problema especialmente importante cuando se utiliza RAG. Supongamos que la aplicación realiza localmente la búsqueda de los documentos relevantes y después envía:
"question": "...",
"context": "..."
}
al backend. Funciona perfectamente, pero el servidor está confiando completamente en el cliente. Un tercero podría sustituir context por cualquier contenido y tratar de utilizar el backend como un proxy de propósito general hacia el modelo.
Una arquitectura más cerrada consiste en que el cliente envíe únicamente identificadores:
"question": "...",
"chunk_ids": [
"abc123",
"def456",
"ghi789"
]
}
y que sea el propio servidor quien recupere los textos correspondientes desde un almacén controlado.
El cliente puede seguir realizando localmente la búsqueda vectorial:
↓
embedding
↓
búsqueda local
↓
TOP 3 IDs
↓
Servidor
↓
recupera los textos oficiales
↓
IA
Así se mantiene una búsqueda rápida en el dispositivo, pero el servidor deja de aceptar contexto arbitrario.
Esto tampoco demuestra que quien realiza la petición sea la aplicación legítima. Sin embargo, consigue algo muy valioso: aunque alguien utilice el endpoint, solo puede hacerlo dentro del conjunto de información autorizado por el servicio.
¿Y sistemas más avanzados?
Existen mecanismos considerablemente más fuertes.
En Android, por ejemplo, las APIs de atestación pueden permitir que un backend compruebe que una petición procede de una aplicación reconocida y de un entorno válido.
También existen:
- tokens efímeros;
- firmas de peticiones;
- claves respaldadas por hardware;
- certificados cliente;
- sistemas de App Check;
- protección contra replay;
- WAF;
- sistemas avanzados de detección de bots.
Todos ellos pueden tener sentido pero la seguridad también debe ser proporcional al problema.
Una aplicación pequeña, sin cuentas de usuario y con un riesgo económico reducido no necesariamente necesita la misma infraestructura que una aplicación bancaria.
Añadir demasiados mecanismos puede introducir:
- más dependencias;
- más puntos de fallo;
- problemas con dispositivos legítimos;
- mayor mantenimiento;
- más datos que gestionar;
- peor experiencia para el usuario.
La seguridad razonable suele estar formada por pequeñas capas
Una arquitectura sencilla puede conseguir una protección bastante buena combinando medidas pequeñas:
↓
Secret almacenado únicamente en servidor
↓
Límite de tamaño de las peticiones
↓
Límites funcionales de texto
↓
Operaciones administrativas protegidas
↓
Errores públicos neutros
↓
Logs sin contenido de usuario
↓
Rate limiting, cuando resulte conveniente
Ninguna de estas medidas aislada hace que una API pública sea inexpugnable.
Juntas consiguen algo mucho más importante: reducir la superficie de ataque y limitar enormemente las consecuencias de un posible abuso.
Seguridad no significa complicarlo todo
Uno de los aprendizajes más interesantes al diseñar este tipo de arquitectura es que no siempre es necesario perseguir una seguridad absoluta.
En una aplicación cliente nunca podremos garantizar que nadie analice el protocolo, inspeccione la APK o reproduzca una petición.
El objetivo razonable es diferente:
La clave realmente valiosa no debe abandonar nunca el servidor y, aunque alguien descubra cómo funciona la comunicación, las posibilidades de abuso deben estar suficientemente limitadas.
A partir de ahí pueden añadirse nuevas capas si el proyecto crece o si aparecen ataques reales.
La seguridad deja entonces de ser una colección de tecnologías y se convierte en una decisión de ingeniería: proteger aquello que realmente importa, con una complejidad proporcional al riesgo.
Conclusiones
Securizar el acceso a una API desde una aplicación móvil no consiste únicamente en ocultar una clave.
Sacar la API key de la APK y trasladarla a un servicio intermedio es un primer paso fundamental, pero a partir de ahí hay que proteger también ese servicio para evitar que termine convirtiéndose en una puerta pública hacia el proveedor de IA.
La solución más razonable no siempre pasa por añadir mecanismos complejos. En muchos proyectos basta con aplicar varias capas sencillas y bien elegidas:
- mantener las credenciales exclusivamente en el servidor;
- limitar el tamaño de las peticiones;
- separar claramente las operaciones públicas de las administrativas;
- no exponer mensajes internos del proveedor;
- evitar registrar preguntas, respuestas o contextos sensibles;
- añadir limitaciones de uso cuando el riesgo lo justifique;
- y asumir conscientemente qué parte del acceso seguirá siendo pública.
También es importante distinguir entre conceptos que a menudo se mezclan. El rate limiting reduce el abuso, pero no autentica a la aplicación. La ofuscación dificulta el análisis, pero no protege un secreto incluido en una APK. Un token temporal no aporta demasiado si cualquiera puede obtenerlo. Y una arquitectura más compleja no es necesariamente más segura si introduce nuevos puntos de fallo o mantenimiento.
La seguridad debe ser proporcional al riesgo real del proyecto.
Para una aplicación pequeña, sin cuentas de usuario y con un uso relativamente limitado de la IA, puede ser mucho más sensato aplicar unas pocas medidas sólidas y comprensibles que introducir desde el principio sistemas de atestación, certificados cliente, firmas criptográficas o infraestructuras difíciles de mantener.
Si el proyecto crece o aparece abuso real, siempre se pueden incorporar capas adicionales más adelante.
La idea principal es sencilla:
La información realmente sensible debe permanecer fuera de la aplicación y, aunque alguien descubra cómo funciona la comunicación, el daño potencial debe estar suficientemente acotado.
Ese equilibrio entre seguridad, simplicidad, privacidad y mantenimiento suele ser mucho más valioso que perseguir una seguridad absoluta que, en una aplicación cliente, difícilmente existe.






No hay comentarios:
Publicar un comentario