Skip to content

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 con tunnel-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:

cd ~/stacks/<stack> && docker compose up -d miapp

Paso 2 — Sincronizar el tunnel (DNS + ingress)

/home/monxas/tunnel-sync.sh sync

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, label tunnel.hostname=music.monxas.casatunnel-sync.sh sync.
  • Modo C: bypass music.monxas.casa/rest (API Subsonic de Amperfy) + protección music.monxas.casa (UI, PocketID).
  • Verificado: /rest/ping.viewok sin login; / → 302 a cloudflareaccess.
  • Access apps: bypass ff1fed9c-d8c9-4b90-a8fe-57c6cd66c03f, protegida 343f3f41-1054-45c7-add0-0f2b21f513fb.

Ver también Cloudflare Access reference (lista de apps + recipes).

Quitar un subdominio

/home/monxas/tunnel-sync.sh remove miapp.monxas.casa     # DNS + ingress
# + borrar Access apps: DELETE /accounts/$ACC/access/apps/<id>  (o dashboard CF)