- JavaScript 100%
| prueba en local | ||
| .gitignore | ||
| cloudflare pasarela-gw.txt | ||
| deploy.mjs | ||
| README.md | ||
| Secret ROUTES.json | ||
| servicios.json | ||
| worker.js | ||
Pasarela de Cloudflare Access (plan gratuito)
Tus apps (Uptime Kuma, Paperless, Zipline, addy, n8n, Bitwarden…) solo saben dar una URL y no saben hacer login con Google. Esta pasarela les sirve de intermediaria y les añade la credencial de máquina que Cloudflare Access sí entiende.
app (solo URL) ──▶ https://status-<token>.634562284.xyz/...
│ Worker: añade CF-Access-Client-Id / -Secret
▼
https://status.634562284.xyz/... (Access lo deja pasar)
Coste: 0 €. Access gratis incluye hasta 50 service tokens; el plan gratis de Workers da 100.000 peticiones/día (de sobra: Uptime Kuma solo consulta cuando abres la app, y los mensajes de WebSocket no cuentan como peticiones).
Lo importante que cambia respecto a tu idea: la pasarela no repite tu login de Google. Tú sigues entrando con Google desde el navegador; la pasarela lleva un service token (una clave de API de Cloudflare) que Access acepta sin ver a nadie. Por eso no hay navegador integrado, no hay sesión que caduque, no gasta batería y se configura una sola vez para todos tus dispositivos.
1. Crear el service token
- Dashboard de Cloudflare → Zero Trust (
one.dash.cloudflare.com) → Access → Service credentials → Service Tokens → Create Service Token. - Nombre (p. ej.
pasarela-gw), duración (un año o la que quieras), Create. - Copia ahora el Client ID y el Client Secret. El secreto solo se muestra una vez.
2. Añadir una política "Service Auth" en cada app de Access
En Zero Trust → Access → Applications, abre la app de
status.634562284.xyz (la que ya tienes con Google):
- Pestaña Policies → Add a policy.
- Action:
Service Auth. - Include:
Service Token→ seleccionapasarela-gw. - Save.
Repite para paperless.634562284.xyz y cualquier otro subdominio que quieras
abrir a la pasarela.
- Un solo service token sirve para todas las apps. Usa siempre
pasarela-gwen cada políticaService Auth; no crees uno por servicio. El Worker tiene un único parCF_ACCESS_CLIENT_ID/CF_ACCESS_CLIENT_SECRET, así que un token distinto del que está en los secretos no pasaría nunca. Lo que sí va por servicio es el hostname y su entrada enROUTES(el token del hostname), que es otra cosa. - Ponle fecha larga al service token (un año o más) y apúntala en el calendario: como es el mismo para todo, cuando caduque dejarán de entrar todas las apps a la vez. Se renueva creando otro token y actualizando los dos secretos del Worker y las políticas donde esté incluido.
- Tu política Allow con Google para humanos se queda igual: los dos flujos
conviven. Cloudflare evalúa
Service Authantes queAllow. - Si en la app tienes activada Strict service token authentication, es
correcto: las peticiones con token solo podrán pasar por
Service Auth.
3. Crear el Worker
- Dashboard → Workers & Pages → Create → Hello World.
- Nombre:
gw-pasarela→ Deploy (con el código de ejemplo; lo cambiamos ahora). - Settings → Code → borra todo y pega el contenido de
worker.js→ Deploy.
Todo esto (pasos 3, 4 y 5) también se puede hacer desde el terminal con un solo comando: ver «Desplegar desde el terminal» más abajo. El dashboard sigue valiendo para las primeras veces y para mirar qué hay desplegado.
4. Los secretos del Worker (y el mapa ROUTES)
Workers → gw-pasarela → Settings → Variables and Secrets → Add. Tres secretos:
| Nombre | Tipo | Valor |
|---|---|---|
CF_ACCESS_CLIENT_ID |
Secret | Client ID del paso 1 |
CF_ACCESS_CLIENT_SECRET |
Secret | Client Secret del paso 1 |
ROUTES |
Secret | El JSON de abajo |
ROUTES es un diccionario que le dice a la pasarela a qué subdominio real
reenviar cada petición. Admite dos tipos de clave:
- Hostname del gateway (modo recomendado): la clave es el hostname por el que llamas a la pasarela. Es el único modo que funciona con apps que usan rutas absolutas (Uptime Kuma, Zipline, n8n…), porque la ruta viaja intacta.
- Token de ruta (modo ruta): la clave es un token aleatorio que va en la URL
(
https://gw.634562284.xyz/<token>/...). Solo sirve para apps que no usan rutas absolutas. Ver «Modo ruta» más abajo.
Formato A — modo hostname (recomendado)
Aquí se usa el token como etiqueta del hostname (no aparece en la ruta), y por
eso la ruta viaja intacta (/assets/..., /api/...).
{
"status-zhAxrIIZJDr03VQwO5941yg1xtRfQWyt.634562284.xyz": "https://status.634562284.xyz",
"paperless-Vj9pQe2xLmN8sT4rWbYc7k3dHfZ1oA6u.634562284.xyz": "https://paperless.634562284.xyz"
}
Pégalo en una sola línea en el campo del secreto:
{"status-zhAxrIIZJDr03VQwO5941yg1xtRfQWyt.634562284.xyz":"https://status.634562284.xyz","paperless-Vj9pQe2xLmN8sT4rWbYc7k3dHfZ1oA6u.634562284.xyz":"https://paperless.634562284.xyz"}
Formato B — modo ruta (el de antes, con token en la URL)
Aquí la clave es solo el token y la URL lleva /token/ después del hostname:
{
"zhAxrIIZJDr03VQwO5941yg1xtRfQWyt": "https://status.634562284.xyz",
"Vj9pQe2xLmN8sT4rWbYc7k3dHfZ1oA6u": "https://paperless.634562284.xyz"
}
En una línea:
{"zhAxrIIZJDr03VQwO5941yg1xtRfQWyt":"https://status.634562284.xyz","Vj9pQe2xLmN8sT4rWbYc7k3dHfZ1oA6u":"https://paperless.634562284.xyz"}
Se llamaría así: https://gw.634562284.xyz/zhAxrIIZJDr03VQwO5941yg1xtRfQWyt/
Los dos formatos conviven en el mismo
ROUTES. La pasarela busca primero si el hostname por el que llega la petición está como clave; si no está, usa el primer trozo de la ruta como token. Puedes migrar servicio a servicio sin romper nada:{ "zhAxrIIZJDr03VQwO5941yg1xtRfQWyt": "https://status.634562284.xyz", "paperless-Vj9pQe2xLmN8sT4rWbYc7k3dHfZ1oA6u.634562284.xyz": "https://paperless.634562284.xyz" }
Cuidado con el formato: comillas dobles en clave y valor, https:// incluido
en el valor y sin comas al final. Si se rompe, la pasarela responde
500 Pasarela mal configurada: el secreto ROUTES no es un JSON válido en vez de
un 404, y ese mensaje te dice exactamente qué falla.
- Cloudflare no deja ver el secreto una vez guardado: guarda el JSON en una nota de tu gestor de contraseñas, lo necesitarás para el paso 7 y para rotarlo.
- El token de cada servicio es aleatorio y largo. El de los ejemplos es
zhAxrIIZJDr03VQwO5941yg1xtRfQWyt(32 caracteres): en modo ruta va en la URL, en modo hostname va como etiqueta (status-<token>.634562284.xyz). Sigue siendo de primer nivel, así que el certificado gratis lo cubre. - En modo hostname, el hostname es la única barrera. No pongas
statusa secas: lleva siempre el token, como en los ejemplos.
Mayúsculas y minúsculas en el token (importante)
Un hostname no distingue mayúsculas: el navegador y el DNS lo pasan siempre a
minúsculas. Si en ROUTES escribes status-ZhAxrIIZJDr03… y entras en
https://status-zhAxrIIZ…, la clave no coincidía y salía No encontrado. — es
exactamente lo que te pasó con Uptime Kuma.
El Worker ya lo tolera: indexa las claves en minúsculas y busca también así, tanto por hostname como por token de ruta. O sea que tu JSON actual con mayúsculas funciona sin volver a tocarlo.
Aun así, lo recomendable es usar tokens solo en minúsculas (letras y números, o directamente hex): otros sitios donde ese valor aparece —el registro DNS, las reglas de Access, los logs de Cloudflare o la propia app— no siempre conservan la caja, y así no dependes de que todos lo hagan. Con 32 caracteres en minúsculas sigue habiendo de sobra entropía.
5. Hostname y ruta del Worker
A y B no son lo mismo, aunque estén en el mismo menú. Con el Worker abierto, pestaña Domains (Workers & Pages → gw-pasarela → Domains): dentro pone Custom Domains and Routes y tiene dos botones, + Add Route y + Add Domain. El primero añade solo una ruta (el DNS lo pones tú antes); el segundo añade el dominio y Cloudflare crea el DNS solo. Se elige una forma por hostname, nunca las dos:
| A. Add Domain | B. Add Route + registro A | |
|---|---|---|
| Botón | + Add Domain | + Add Route |
| Crea el DNS | Sí, Cloudflare solo | No: lo creas tú a mano antes |
| Crea la ruta | Sí, Cloudflare solo | La creas tú en el paso 2 |
| Pasos | 1 | 2, y en este orden: DNS → ruta |
| Si falta una pieza | — | DNS sin ruta → error de Cloudflare (la IP 192.0.2.1 está muerta a propósito); ruta sin DNS → la ruta no recibe nada |
En B el orden importa: primero el registro DNS, después la ruta. Una ruta solo se aplica a peticiones que ya llegan al borde de Cloudflare con ese hostname, y eso lo consigue el registro proxied. Al revés no funciona.
Cuántos registros hay que crear
Uno por cada hostname de pasarela, es decir uno por servicio y por dominio que
vayas a usar: status-<token>.634562284.xyz es uno; si abres Zipline en tres
dominios, son tres hostnames (ver «Varios dominios» más abajo). En los ejemplos
status es solo el primero: si añades Paperless, necesita su propio
paperless-<token>, con su propio registro y su propia ruta (o su propio hostname
con Add Domain).
Lo que no se toca son los subdominios reales de origen
(status.634562284.xyz, paperless.634562284.xyz): esos ya existen con tu túnel y
se quedan como están.
| Servicio | Hostname de pasarela (nuevo, apunta al Worker) | Origen (el que ya tienes) |
|---|---|---|
| Uptime Kuma | status-zhAxrIIZJDr03VQwO5941yg1xtRfQWyt.634562284.xyz |
https://status.634562284.xyz |
| Paperless | paperless-Vj9pQe2xLmN8sT4rWbYc7k3dHfZ1oA6u.634562284.xyz |
https://paperless.634562284.xyz |
A. Add Domain (recomendado: Cloudflare crea el DNS y la ruta)
Con el Worker abierto: pestaña Domains → + Add Domain. Pide dos cosas, en este orden:
- Dominio: el desplegable con las zonas de tu cuenta (
634562284.xyz,th4n4t0s.pp.ua,thanatos.pp.ua,jonatanpons.pp.ua…). Solo salen las que están en esa misma cuenta de Cloudflare. - Subdominio: escribes la parte de delante, sin el dominio. Para el
ejemplo, si elegiste
634562284.xyz, escribesstatus-zhAxrIIZJDr03VQwO5941yg1xtRfQWyt(nada más; el.634562284.xyzlo pone Cloudflare). Si el dashboard te lo enseña después en minúsculas, es normal: los hostnames no distinguen mayúsculas y la pasarela lo tiene en cuenta.
Un solo paso: no creas ningún registro a mano. Repítelo por cada servicio y por cada dominio que quieras usar. Si antes creaste a mano un registro A para ese hostname, bórralo primero: chocan.
B. Add Route + registro A (manual)
- DNS → Records → Add: tipo
A, nombrestatus-zhAxrIIZJDr03VQwO5941yg1xtRfQWyt, IPv4192.0.2.1, Proxy activado (naranja). La IP no importa: el Worker intercepta antes. - Pestaña Domains → + Add Route:
- Zona:
634562284.xyz - Ruta:
status-zhAxrIIZJDr03VQwO5941yg1xtRfQWyt.634562284.xyz/*
- Zona:
Para Paperless, repite los dos pasos con su nombre
(paperless-Vj9pQe2xLmN8sT4rWbYc7k3dHfZ1oA6u): primero su registro A, después su
ruta. Un hostname de pasarela nunca se queda solo con DNS o solo con ruta.
Varios dominios: la misma app en varios sitios (ejemplo con Zipline)
Sí, funciona: una entrada en ROUTES y un hostname de pasarela por cada dominio,
todas apuntando al origen que corresponda. Cloudflare no las mezcla, porque cada
hostname vive en su zona y el Worker mira el hostname completo. Ejemplo con Zipline
en tus tres dominios (tokens en minúsculas, como se recomienda más abajo):
{
"zipline-k3f9m2q7x1v8b5n4c6d0z7l2p9r4t8w1.634562284.xyz": "https://zipline.634562284.xyz",
"zipline-k3f9m2q7x1v8b5n4c6d0z7l2p9r4t8w1.th4n4t0s.pp.ua": "https://zipline.th4n4t0s.pp.ua",
"zipline-k3f9m2q7x1v8b5n4c6d0z7l2p9r4t8w1.thanatos.pp.ua": "https://zipline.thanatos.pp.ua"
}
En + Add Domain lo repites tres veces (una por zona): mismo subdominio escrito, zona distinta en el desplegable. En las apps usas el hostname del dominio que te interese; los tres llegan al mismo Zipline.
Tres detalles que sí importan:
- Todas las zonas tienen que estar en la misma cuenta de Cloudflare. El Worker es de una cuenta y solo puede añadir hostnames de sus propias zonas. Si algún dominio está en otra cuenta, o lo mueves, o montas el Worker también allí.
- Cada origen necesita su propia política
Service Authen Access (paso 2), porque Access protege por aplicación/dominio: tenerla enzipline.634562284.xyzno la da por hecha enzipline.thanatos.pp.ua. Ojo también con los comodines de Access: no deben cubrir los hostnames de la pasarela. - La sesión no se comparte entre dominios: la cookie del servicio vive en el hostname por el que entras, así que tendrás que loguearte una vez en cada uno. La API key de la app, si usa una, no cambia.
Puedes usar el mismo token en los tres (como arriba, más fácil de recordar) o uno distinto por dominio si quieres poder revocar uno solo.
⚠️ Usa hostnames de primer nivel:
status-zhAxrIIZJDr03VQwO5941yg1xtRfQWyt.634562284.xyzsí;status.gw.634562284.xyzno. El certificado Universal SSL gratuito cubre el dominio y un nivel de subdominio; con dos niveles el navegador dará error de certificado.
⚠️ Comprueba que ninguna aplicación de Access cubra el hostname de la pasarela. Si protegiste
*.634562284.xyzcon un comodín, las apps volverían a chocar con el muro de Google (Access se evalúa antes que el Worker). En ese caso, deja esa app solo con hostnames concretos.
Si usas Cloudflare Tunnel con wildcard
La pasarela vive en el borde de Cloudflare, no en tu túnel: el registro DNS
concreto (status-<token>) le gana al wildcard *.634562284.xyz del túnel —en DNS el
comodín solo aplica si no hay registro específico— y el Worker intercepta antes
de cualquier origen. El túnel nunca ve ese hostname.
Las reglas del túnel no admiten exclusiones, y la wildcard debe ir la última de la lista (se evalúan de arriba abajo). Si tu proxy interno enruta por hostname, puedes dejar solo la wildcard en el túnel y mover ahí las reglas concretas: así no vuelves a reordenar nada al añadir servicios.
6. Probar antes de tocar las apps
Desde un ordenador (en ventana incógnita, sin sesión de Access):
# Modo hostname: NO debe salir la pantalla de login de Google.
curl -i https://status-zhAxrIIZJDr03VQwO5941yg1xtRfQWyt.634562284.xyz/
# Modo ruta: mismo origen, con el token en la URL.
curl -i https://gw.634562284.xyz/zhAxrIIZJDr03VQwO5941yg1xtRfQWyt/
# Un hostname/token que no esté en ROUTES -> 404.
curl -i https://gw.634562284.xyz/token-quenoexiste/
| Qué te devuelve | Causa | Solución |
|---|---|---|
| El contenido real de Uptime Kuma (o su 401/403) | Funciona | Nada |
Pasarela mal configurada: faltan CF_ACCESS_CLIENT_ID… (500) |
Falta un secreto | Revísalos |
Pasarela mal configurada: el secreto ROUTES no es un JSON válido (500) |
JSON malformado | Paso 4 |
No encontrado. (404) |
El hostname o el token no está en ROUTES (mal copiado, mayúsculas…) |
Paso 4 |
Redirect a …cloudflareaccess.com |
Access cubre el hostname | Paso 5 |
| Error de Cloudflare (5xx / 1016) | Falta la ruta o el Add Domain | Paso 5 |
7. Configurar las apps
Cada app apunta a https://<servicio>-<token>.634562284.xyz (modo hostname) o
a https://gw.634562284.xyz/<token>/ (modo ruta). La excepción son las apps
que admiten cabeceras personalizadas: esas van directas a su origen, sin
pasarela (apartado Additional headers, más abajo):
| App | URL que pones en la app | Autenticación que usa la app |
|---|---|---|
| Uptime Kuma | https://status-zhAxrIIZJDr03VQwO5941yg1xtRfQWyt.634562284.xyz |
usuario y contraseña de Uptime Kuma |
| Zipline | https://zipline-<token>.634562284.xyz |
usuario y contraseña de Zipline |
| Paperless | su origen, https://paperless.634562284.xyz |
additional headers + su token (siguiente apartado) |
| Karakeep | su origen, https://karakeep.634562284.xyz |
additional headers en la extensión + su login o API key |
| addy.io | https://addy-<token>.634562284.xyz |
su API key |
| n8n | https://n8n-<token>.634562284.xyz |
su API key |
| Bitwarden | https://bitwarden-<token>.634562284.xyz |
su propio login |
<token> es el valor aleatorio de 32 caracteres que pusiste en ROUTES para ese
servicio.
El login propio de cada servicio (Uptime Kuma, Paperless, la master password de Bitwarden) sigue pidiéndose igual: la pasarela solo salta la barrera de Cloudflare Access que hay delante.
Additional headers: las apps que sí pueden mandar el service token
Algunas apps tienen un campo Additional headers / cabeceras personalizadas en los ajustes del servidor (Paperless Mobile en Android o la extensión de Chrome de Karakeep, por ejemplo). Ahí se ponen dos filas, una por cabecera:
| Key | Value |
|---|---|
CF-Access-Client-Id |
el Client ID del service token (el que acaba en .access) |
CF-Access-Client-Secret |
el Client Secret de ese mismo service token |
Y en la URL de la app pones el origen, el mismo subdominio que ya usa el túnel:
https://paperless.634562284.xyz. No pongas el hostname de la pasarela
(paperless-<token>…): con las cabeceras puestas no hace falta, y en Paperless
evita además el error de CSRF, porque la petición llega al origen con su propio
Origin y su propio Host.
Detalles que se fallan sin querer:
- En Value va solo el valor: sin
Bearer, sin comillas, sin el nombre de la cabecera delante y sin espacios al principio ni al final. - Son dos filas. No vale una sola con
Id: …, Secret: …ni unKeyllamadoAuthorization. - El service token es el mismo para todas las apps (
pasarela-gw); lo que cambia por servicio es el hostname del origen. Si creas un token nuevo por app, la pasarela no lo conoce y nunca lo enviaría. - El secreto se muestra una sola vez al crearlo; si lo pierdes, hay que rotar el token.
Resumen para Paperless en Android:
| Campo de la app | Qué poner |
|---|---|
| Servidor / URL | https://paperless.634562284.xyz (sin /api, sin barra final) |
| Additional headers → Key / Value | CF-Access-Client-Id / tu Client ID |
| Additional headers → Key / Value | CF-Access-Client-Secret / tu Client Secret |
| Autenticación | su API Auth Token de Profile → API Auth Token (40 caracteres hex) |
Ese token de Paperless es otra credencial distinta del service token: el service token abre la puerta de Cloudflare, el token de Paperless abre la aplicación. Hacen falta los dos, y no se sustituyen entre sí.
Estas apps no van en servicios.json ni necesitan Custom Domain en la pasarela
(no las añadas a servicios ni a tokens, y si antes estaban, bórralas y despliega).
Lo que sí hay que mantener es la política Service Auth en la app de Access
de su origen: es justo la que deja entrar al service token que manda la app. Si la
borras, la app vuelve a chocar con el login de Google.
Karakeep: la extensión del navegador y el Invalid origin
Karakeep admite cabeceras personalizadas en su extensión de Chrome (Configure Custom Headers), así que también va directa al origen, sin pasarela:
| Campo | Qué poner |
|---|---|
| Server address | https://karakeep.634562284.xyz (sin barra final; no el hostname de la pasarela) |
| Custom header → Key / Value | CF-Access-Client-Id / tu Client ID |
| Custom header → Key / Value | CF-Access-Client-Secret / tu Client Secret |
| Login | tu email y contraseña de Karakeep, o una API key de User Settings → API Keys |
Failed to execute 'fetch': Invalid name
Ese error no viene de Cloudflare ni de Karakeep: lo lanza Chrome antes de enviar nada, cuando el nombre de una cabecera tiene un carácter que no pertenece a un nombre HTTP válido. Lo más habitual, por orden:
- un espacio al final del nombre (al pegar
CF-Access-Client-Idse queda sin que se vea; la extensión guarda el nombre tal cual, sin recortar espacios); - el dos puntos pegado:
CF-Access-Client-Id:en vez deCF-Access-Client-Id; - un espacio o un tabulador en medio, o el nombre copiado de un
curlcompleto (-H "CF-Access-Client-Id: …") en el campo Key.
Como el fallo es al construir la petición, falla igual con usuario y contraseña que con API key (la extensión usa el mismo cliente para todo), y ningún ajuste de CORS o de Access lo arregla. Arreglo:
- En la extensión, Configure Custom Headers → borra todas las filas con la papelera (no edites encima de una fila mala).
- Escribe los dos nombres a mano, sin copiar y pegar desde el README ni desde
un
curl; comprueba que acaban enId/Secret, sin espacio detrás. - Pega los valores. Si el secreto trae un salto de línea al final, bórralo.
- Save y vuelve a entrar: el enlace debe decir
Configure Custom Headers (2), no(3)ni más (una fila duplicada suele ser la culpable). - Prueba el login. Si ahora sale un error de CORS o NetworkError en vez de
Invalid name, es el preflight de Access: en la app de Access del origen (karakeep.634562284.xyz) → Additional settings → CORS, deja Allow all origins, Allow all methods y Allow all http headers, con Allow-Credentials y Bypass options apagados. En Firefox, además, hay que permitir el origenmoz-extension://<uuid>de la extensión (issue #2119); en Chrome con Allow all origins ya queda cubierto.
Invalid origin al entrar por la pasarela
Karakeep (Better Auth) compara el Origin que manda el navegador con sus orígenes
permitidos, y los saca del host de la petición. A través de la pasarela el
Host es el del origen (karakeep.634562284.xyz) pero el Origin del navegador es
el de la pasarela (karakeep-<token>…), así que no coinciden y rechaza el login.
La solución es decirle a Karakeep que ese hostname también es suyo con su variable
AUTH_TRUSTED_ORIGINS (en las versiones recientes), en su docker-compose/.env,
y reiniciar el contenedor:
AUTH_TRUSTED_ORIGINS=*,https://karakeep-<token>.634562284.xyz
El * mantiene el comportamiento por defecto (aceptar la dirección por la que se
llegue) y detrás va el hostname de la pasarela, con https:// y sin barra final.
Si algún día añades otro hostname, sepáralos con comas.
Aun así, para Karakeep lo práctico es entrar por el origen
(https://karakeep.634562284.xyz, con el login normal de Cloudflare Access) y
reservar la pasarela para las apps que no saben mandar cabeceras. Karakeep también
valida Origin frente a Host en sus server actions, así que la interfaz web
completa a través de la pasarela puede dar problemas que esta variable no cubre.
Cuando una app dice «inválido» aunque todo esté bien
| Lo que ves | Qué está pasando |
|---|---|
| El muro de login de Google dentro de la app | Las cabeceras no llegan: un typo, un espacio en el secreto, o pusiste el nombre del token (pasarela-gw) en vez del Client ID |
| Vuelve a pedir login / salta a Google justo después de funcionar | Te falta o has borrado la política Service Auth en la app de Access de ese origen |
Invalid token / «API key no válida» (401 con JSON) |
Access ya te dejó pasar: el que rechaza es el servicio. Token mal copiado (con espacio, o menos de 40 caracteres) o regenerado después de copiarlo |
La verificación CSRF ha fallado (403) en el navegador |
Estás entrando por el hostname de la pasarela. Entra por el origen https://paperless.634562284.xyz, o añade PAPERLESS_CSRF_TRUSTED_ORIGINS con el hostname de la pasarela |
Failed to execute 'fetch': Invalid name (extensión de Chrome) |
Un nombre de cabecera tiene un carácter ilegal (espacio al final, : pegado, tab). Chrome aborta antes de enviar: ni Access ni Karakeep llegan a verlo. Bórralas y escríbelas de nuevo |
Invalid origin en el login, entrando por la pasarela |
La app compara Origin (pasarela) con el host de la petición (origen). Añade AUTH_TRUSTED_ORIGINS con el hostname de la pasarela, o entra por el origen |
Modo ruta (opcional): por qué a veces sale la pantalla en blanco
Si en vez de un hostname propio usas
https://gw.634562284.xyz/zhAxrIIZJDr03VQwO5941yg1xtRfQWyt, la pasarela reenvía
todo lo que va detrás del token. El problema: las apps web
modernas (Uptime Kuma, Zipline…) piden sus recursos con rutas absolutas
(/assets/index-…js, /api/…). El navegador las pide en la raíz del gateway
—https://gw.634562284.xyz/assets/…— y ahí no hay token, así que la pasarela
responde 404: la página carga en blanco.
Por eso el modo hostname es el recomendado. El modo ruta solo tiene sentido para apps que construyen sus URLs con rutas relativas o que soportan un base path.
Cosas que debes saber
- La URL de la pasarela es una credencial en modo ruta (el token es la única
barrera). En modo hostname la barrera es que nadie conozca el hostname, que es
más débil: trátalo como un secreto y, si se filtra, cámbialo en
ROUTES. - Muchas apps solo aceptan URL, no cabeceras ni certificados: esas van por la pasarela. Las que sí admiten cabeceras personalizadas (Paperless, entre otras) pueden llevar el service token directo y ahorrarse la pasarela (apartado Additional headers del paso 7).
- WebSockets: Uptime Kuma usa Socket.IO por WebSocket. El Worker lo reléa y
los mensajes de WebSocket no consumen cuota de peticiones. No actives
"Protect this Worker" en
gw-pasarela: eso rompe los WebSockets. - Redirects y cookies: el Worker reescribe los
Locationque apuntan al origen y ajustaDomain/Pathde las cookies. Un redirect a un sitio externo (p. ej. el OAuth de Google) se deja tal cual. - Límites gratis: 100.000 peticiones/día, 10 ms de CPU por petición y 50 subrequests externos por invocación. Un proxy lineal no se acerca a eso con uso doméstico.
- Para quitar el acceso: borra la política
Service Auth, quita el hostname deROUTES, o revoca el service token.
Desplegar desde el terminal (opcional)
Todo lo de los pasos 3, 4 y 5 se puede hacer con un solo comando: Wrangler, la
CLI oficial de Cloudflare, sube el código y crea los hostnames y pone los
secretos. Así no vuelves a tocar el dashboard: añadir un servicio es una línea en
un JSON y node deploy.mjs.
Qué hace deploy.mjs
Lee servicios.json, genera wrangler.generated.jsonc con el
código, las rutas y los Custom Domains, y lanza por ti:
npx wrangler@4 deploy --config wrangler.generated.jsonc
npx wrangler@4 secret bulk <secretos> # CF_ACCESS_CLIENT_ID, CF_ACCESS_CLIENT_SECRET y ROUTES
O sea: los pasos 3 (código), 4 (ROUTES) y 5 (Add Domain / Add Route) en una
ejecución. El ROUTES se construye desde el mismo JSON, así que ya no lo escribes
a mano en ningún sitio.
servicios.json
{
"worker": "gw-pasarela",
"compatibility_date": "2026-10-23",
"workers_dev": false,
"rutas": [
{ "hostname": "gw.634562284.xyz", "zona": "634562284.xyz" }
],
"servicios": {
"status-k3f9m2q7x1v8b5n4c6d0z7l2p9r4t8w1.634562284.xyz": "https://status.634562284.xyz",
"zipline-k3f9m2q7x1v8b5n4c6d0z7l2p9r4t8w1.thanatos.pp.ua": "https://zipline.thanatos.pp.ua"
},
"tokens": {
"k3f9m2q7x1v8b5n4c6d0z7l2p9r4t8w1": "https://n8n.634562284.xyz"
}
}
| Clave | Para qué |
|---|---|
rutas |
Entradas del tipo Add Route: hostname + zona. Sirven para los que ya tienen un registro A manual, como gw. |
servicios |
Hostname de pasarela → origen. Cada uno se crea con Add Domain y su entrada en ROUTES. |
tokens |
Modo ruta: token → origen. Solo va a ROUTES, no crea ningún hostname. |
workers_dev |
false apaga gw-pasarela.<tu-subdominio>.workers.dev, que si no queda accesible por fuera. |
Cómo se usa
Una sola vez, para autenticarte (abre el navegador de tu PC):
cd access-gateway
npx wrangler@4 login
Y para desplegar:
node deploy.mjs --dry-run # valida y enseña qué haría, sin tocar nada
node deploy.mjs # genera config, despliega y sube secretos
También valen npm run deploy:dry y npm run deploy, que hacen lo mismo.
Las credenciales del service token no van en el repo. El script las busca en
las variables de entorno CF_ACCESS_CLIENT_ID / CF_ACCESS_CLIENT_SECRET o, si
prefieres, en un secrets.local.json junto al script (ya está en .gitignore):
{
"CF_ACCESS_CLIENT_ID": "…",
"CF_ACCESS_CLIENT_SECRET": "…"
}
Necesita Node 18 o superior. Nada más que instalar: el script llama a
npx wrangler@4.
Cuatro cosas que conviene saber
- El JSON es la única fuente de verdad de los hostnames. Al desplegar, el Worker se queda exactamente con las rutas del JSON: si borras una línea y despliegas, ese Custom Domain se elimina. No lo uses como «añadir sin quitar». Para probar, edita el JSON.
ROUTESpasa a generarse. Si lo editas a mano en el dashboard y luego despliegas, el deploy lo pisa. La buena noticia es que ya no hace falta editarlo.gwva enrutas, no enservicios. Ya tiene un registro A creado a mano; como Custom Domain chocaría con él. Los hostnames nuevos sí pueden ir enservicios.- Las apps con cabeceras no van en el JSON. Paperless y Karakeep entran directas
a su origen con el service token, así que no necesitan Custom Domain ni entrada en
ROUTES: déjalas fuera deserviciosy detokens. Si las quitas y despliegas, el deploy borra sus hostnames de la pasarela (es lo correcto: van por su origen), pero no toca su DNS, su túnel ni la políticaService Authdel origen, que sigue siendo la puerta que abre el service token. - Las zonas tienen que estar en la misma cuenta de Cloudflare donde vive el Worker; si una no está, el deploy falla y dice cuál.
Probar en local
cd access-gateway
npx wrangler@4 dev --config wrangler.dev.jsonc
Banco de pruebas (17 comprobaciones) contra un origen simulado que se comporta como Access — redirige al muro de Google si no lleva credenciales:
node test/mock-origin.mjs &
node test/run.mjs
Cubre: inyección del service token, 404 con token/hostname desconocido, reescritura
de redirects (internos sí, OAuth de Google no), cookies en ambos modos, POST con
cuerpo, modo hostname con rutas absolutas (/assets/...), claves con mayúsculas en
ROUTES, y el relé de WebSocket.