Tutorial para poder usar las aplicaciones detrás de la seguridad de Google de Cloudflare
  • JavaScript 100%
Find a file
2026-10-23 21:04:52 +02:00
prueba en local first commit 2026-10-23 20:50:44 +02:00
.gitignore first commit 2026-10-23 20:50:44 +02:00
cloudflare pasarela-gw.txt first commit 2026-10-23 20:50:44 +02:00
deploy.mjs first commit 2026-10-23 20:50:44 +02:00
README.md first commit 2026-10-23 20:50:44 +02:00
Secret ROUTES.json borrar forgejo de json 2026-10-23 21:04:52 +02:00
servicios.json first commit 2026-10-23 20:50:44 +02:00
worker.js first commit 2026-10-23 20:50:44 +02:00

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

  1. Dashboard de Cloudflare → Zero Trust (one.dash.cloudflare.com) → Access → Service credentials → Service Tokens → Create Service Token.
  2. Nombre (p. ej. pasarela-gw), duración (un año o la que quieras), Create.
  3. 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):

  1. Pestaña Policies → Add a policy.
  2. Action: Service Auth.
  3. Include: Service Token → selecciona pasarela-gw.
  4. 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-gw en cada política Service Auth; no crees uno por servicio. El Worker tiene un único par CF_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 en ROUTES (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 Auth antes que Allow.
  • 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

  1. Dashboard → Workers & Pages → Create → Hello World.
  2. Nombre: gw-pasarela → Deploy (con el código de ejemplo; lo cambiamos ahora).
  3. 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 status a 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:

  1. 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.
  2. Subdominio: escribes la parte de delante, sin el dominio. Para el ejemplo, si elegiste 634562284.xyz, escribes status-zhAxrIIZJDr03VQwO5941yg1xtRfQWyt (nada más; el .634562284.xyz lo 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)

  1. DNS → Records → Add: tipo A, nombre status-zhAxrIIZJDr03VQwO5941yg1xtRfQWyt, IPv4 192.0.2.1, Proxy activado (naranja). La IP no importa: el Worker intercepta antes.
  2. Pestaña Domains → + Add Route:
    • Zona: 634562284.xyz
    • Ruta: status-zhAxrIIZJDr03VQwO5941yg1xtRfQWyt.634562284.xyz/*

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 Auth en Access (paso 2), porque Access protege por aplicación/dominio: tenerla en zipline.634562284.xyz no la da por hecha en zipline.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.xyz sí; status.gw.634562284.xyz no. 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.xyz con 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 un Key llamado Authorization.
  • 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-Id se 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 de CF-Access-Client-Id;
  • un espacio o un tabulador en medio, o el nombre copiado de un curl completo (-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:

  1. En la extensión, Configure Custom Headers → borra todas las filas con la papelera (no edites encima de una fila mala).
  2. Escribe los dos nombres a mano, sin copiar y pegar desde el README ni desde un curl; comprueba que acaban en Id / Secret, sin espacio detrás.
  3. Pega los valores. Si el secreto trae un salto de línea al final, bórralo.
  4. 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).
  5. 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 origen moz-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 Location que apuntan al origen y ajusta Domain/Path de 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 de ROUTES, 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.
  • ROUTES pasa 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.
  • gw va en rutas, no en servicios. Ya tiene un registro A creado a mano; como Custom Domain chocaría con él. Los hostnames nuevos sí pueden ir en servicios.
  • 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 de servicios y de tokens. 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ítica Service Auth del 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.