Crear un subdominio (*.monxas.casa)¶
Exponer un servicio nuevo bajo *.monxas.casa con uno de tres modos de
protección: sin auth, CF Access (PocketID), o CF Access con bypass de API
para apps móviles/CLIs.
Complementa a Adding a service (flujo Caddy/
homelab-ctl.py) y Cloudflare Access + PocketID.
Dos caminos de routing
- Directo por tunnel — lo que usan los stacks de VM208 (
media,tools, …): CF Tunnel →192.168.0.208:PORT, sin pasar por Caddy. Se sincroniza contunnel-sync.sh. - Vía Caddy HA (LXC 270/271): CF Tunnel → Caddy → backend, con
homelab-ctl.py sync. Para servicios que necesitan features de Caddy.
Esta guía cubre el camino directo por tunnel (el más común para apps nuevas de VM208).
Doc-debt (2026-07-19)
operations/scripts/tunnel-sync.md marca tunnel-sync.py como histórico, pero
/home/monxas/tunnel-sync.sh (bash) sigue vivo y es lo que sincroniza los stacks
de VM208 (verificado: music no aparece en managed.caddy y funciona por tunnel directo).
Pendiente reconciliar cuál es el canónico.
Paso 1 — Label en el compose¶
~/stacks/<stack>/compose.yaml en VM208:
services:
miapp:
image: example/miapp:latest
ports:
- "4533:4533" # host:container
labels:
- tunnel.hostname=miapp.monxas.casa
# - tunnel.port=4533 # opcional; default = primer puerto publicado
Recrear el container para que coja la label:
Paso 2 — Sincronizar el tunnel (DNS + ingress)¶
Crea el CNAME miapp.monxas.casa y la ruta ingress → http://192.168.0.208:4533.
Paso 3 — Elegir modo de protección¶
Modo A — Sin protección (la app tiene login propio)¶
No crees ninguna Access app. El servicio queda accesible con la auth de la app.
Patrón de Immich (pics) y Audiobookshelf (podcasts) — OIDC/login propio.
Nada más que hacer.
Modo B — CF Access + PocketID (UI protegida en el borde)¶
Para apps sin auth propia. Crea Access app + policy:
source ~/.env # CF_API_TOKEN, CF_ACCOUNT_ID
B="https://api.cloudflare.com/client/v4/accounts/$CF_ACCOUNT_ID/access/apps"
AID=$(curl -s -X POST -H "Authorization: Bearer $CF_API_TOKEN" -H "Content-Type: application/json" \
--data "{\"name\":\"miapp\",\"domain\":\"miapp.monxas.casa\",\"type\":\"self_hosted\",\"session_duration\":\"24h\",\"app_launcher_visible\":false,\"auto_redirect_to_identity\":false}" \
"$B" | jq -r .result.id)
curl -s -X POST -H "Authorization: Bearer $CF_API_TOKEN" -H "Content-Type: application/json" \
--data "{\"name\":\"default\",\"decision\":\"allow\",\"include\":[{\"email\":{\"email\":\"[email protected]\"}},{\"email_domain\":{\"domain\":\"monxas.com\"}}]}" \
"$B/$AID/policies"
Verificar: curl -s -o /dev/null -w "%{http_code}\n" https://miapp.monxas.casa/ → 302 (login).
App sin policy = bloquea TODO
Crea app y policy seguidas. Una Access app sin policy deniega a todos.
Modo C — CF Access + bypass de API (apps móviles / webhooks)¶
Para servicios con UI web + una API que consume una app móvil o CLI que no puede hacer login OIDC interactivo (Subsonic/Amperfy, webhooks n8n, clientes arr…). Se crean DOS apps: bypass en el path de la API + protección* en el dominio. El path más específico gana precedencia → la API pasa libre y la UI queda gateada.
Alternativa a los Service Tokens: usa bypass por path cuando la app no sabe mandar cabeceras
CF-Access-Client-Id/Secret(caso de Amperfy).
source ~/.env
B="https://api.cloudflare.com/client/v4/accounts/$CF_ACCOUNT_ID/access/apps"
mkapp(){ curl -s -X POST -H "Authorization: Bearer $CF_API_TOKEN" -H "Content-Type: application/json" --data "$1" "$B" | jq -r .result.id; }
mkpol(){ curl -s -X POST -H "Authorization: Bearer $CF_API_TOKEN" -H "Content-Type: application/json" --data "$2" "$B/$1/policies" >/dev/null; }
# 1) Bypass del path de la API (ajusta /rest al de tu app)
A1=$(mkapp "{\"name\":\"miapp-api-bypass\",\"domain\":\"miapp.monxas.casa/rest\",\"type\":\"self_hosted\",\"session_duration\":\"24h\",\"app_launcher_visible\":false,\"auto_redirect_to_identity\":false}")
mkpol "$A1" "{\"name\":\"bypass-everyone\",\"decision\":\"bypass\",\"include\":[{\"everyone\":{}}]}"
# 2) Protección del dominio (UI)
A2=$(mkapp "{\"name\":\"miapp\",\"domain\":\"miapp.monxas.casa\",\"type\":\"self_hosted\",\"session_duration\":\"24h\",\"app_launcher_visible\":false,\"auto_redirect_to_identity\":false}")
mkpol "$A2" "{\"name\":\"default\",\"decision\":\"allow\",\"include\":[{\"email\":{\"email\":\"[email protected]\"}},{\"email_domain\":{\"domain\":\"monxas.com\"}}]}"
Paths típicos a bypassear: Subsonic /rest · n8n /webhook · *arr /api.
Verificar ambos planos:
curl -s "https://miapp.monxas.casa/rest/ping.view?u=U&p=P&v=1.16.1&c=x&f=json" # API → responde SIN login CF
curl -s -o /dev/null -w "%{http_code}\n" https://miapp.monxas.casa/ # UI → 302 (protegida)
Ejemplo real — music.monxas.casa (Navidrome + Amperfy, 2026-07-19)¶
- Navidrome en stack
media, labeltunnel.hostname=music.monxas.casa→tunnel-sync.sh sync. - Modo C: bypass
music.monxas.casa/rest(API Subsonic de Amperfy) + protecciónmusic.monxas.casa(UI, PocketID). - Verificado:
/rest/ping.view→oksin login;/→ 302 acloudflareaccess. - Access apps: bypass
ff1fed9c-d8c9-4b90-a8fe-57c6cd66c03f, protegida343f3f41-1054-45c7-add0-0f2b21f513fb.
Ver también Cloudflare Access reference (lista de apps + recipes).