Saltar al contenido principal

Asegura OpenClaw con un reverse proxy Caddy + HTTPS

· 9 min de lectura
Rafael Fernandes
Ingeniero de PLN y Redactor Técnico en WiLine
Share:
OpenClaw+

En el Artículo 1 pusimos OpenClaw en marcha — pero solo sobre HTTP plano, con el truco de allowInsecureAuth. Aquí ponemos Caddy por delante como reverse proxy: HTTPS de verdad, auth con emparejamiento de dispositivos y los puertos crudos del gateway cerrados para que el proxy sea la única vía de entrada. Cada comando y error que aparece abajo es del despliegue real.

Reproducibilidad

Continúa desde el Artículo 1 en el mismo Instancia WEC — Ubuntu 22.04.5 LTS, OpenClaw 2026.6.8. Usamos Caddy 2 (caddy:2). Como esta máquina no tiene dominio público, usamos la CA interna de Caddy (tls internal) para HTTPS autofirmado; la sección Pasar a producción muestra el cambio de una línea a certificados reales de Let's Encrypt una vez que tengas un dominio.


Lo que vas a construir

Caddy termina el HTTPS y hace de reverse proxy hacia el gateway de OpenClaw por la red interna de Docker. El gateway deja de publicar sus propios puertos — el único punto de entrada pasa a ser Caddy en :443.


Requisitos previos

  • Artículo 1 completado — OpenClaw corriendo en ~/openclaw
  • Acceso por shell a la VM
  • Un hostname para el gateway. Con un dominio público, úsalo (y obtén certificados reales de Let's Encrypt). En una máquina privada, usamos openclaw.local + una entrada en el archivo hosts.

Paso 1 — Confirmar el punto de partida

cd ~/openclaw
docker compose ps

Deberías ver openclaw-gateway Up (healthy), publicando 18789/18790/3978. Ese es nuestro estado de partida: funcionando, pero solo HTTP.

docker compose ps mostrando el gateway en marcha


Paso 2 — Escribir el Caddyfile

La config de Caddy: servir HTTPS y hacer proxy de todo hacia el gateway. tls internal usa la propia CA local de Caddy — no hace falta dominio público.

cat > Caddyfile <<'EOF'
# Caddy selects its certificate by SNI (the hostname in the TLS handshake).
# A bare IP sends no SNI, so we use a hostname. With a public domain, put your
# domain here and drop `tls internal` for automatic Let's Encrypt.
openclaw.local {
tls internal
reverse_proxy openclaw-gateway:18789
}
EOF

El destino del proxy es openclaw-gateway:18789 — Caddy alcanza el gateway por su nombre de servicio de Compose a través de la red compartida de Docker.


Paso 3 — Añadir Caddy a Compose

docker compose fusiona automáticamente un docker-compose.override.yml, así que añadimos un servicio caddy sin tocar el archivo compose oficial de OpenClaw:

cat > docker-compose.override.yml <<'EOF'
services:
caddy:
image: caddy:2
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config

volumes:
caddy_data:
caddy_config:
EOF

caddy_data persiste la CA local + los certificados entre reinicios.


Paso 4 — Iniciar Caddy

docker compose up -d
docker compose ps

Caddy arranca junto al gateway con 443 publicado, y genera su CA interna + un certificado en la primera ejecución.

Contenedor Caddy en marcha con 443 publicado

nota

docker compose up -d también inicia el helper de un solo uso openclaw-cli; que quede en reposo aquí es inofensivo — lo invocamos con docker compose run --rm cuando hace falta.


Paso 5 — Verificar HTTPS

Consulta el endpoint de salud del gateway a través de Caddy. --resolve hace que curl envíe el hostname (SNI) mientras apunta a la máquina:

curl -k --resolve openclaw.local:443:127.0.0.1 https://openclaw.local/healthz
# {"ok":true,"status":"live"}

healthz respondiendo sobre HTTPS vía Caddy

¿Te topaste con un tlsv1 alert internal error?

Eso pasa si usaste una IP pelada como dirección del sitio — mira Solución de problemas #1. Usa un hostname.


Paso 6 — Volver a asegurar el gateway

Ahora el tráfico llega por https://openclaw.local. Dos cambios de config:

  1. Añade el origen HTTPS a allowedOrigins (la Control UI rechaza orígenes desconocidos).
  2. Vuelve a desactivar allowInsecureAuth — HTTPS es un contexto seguro, así que el truco del Artículo 1 ya no hace falta.
docker compose run --rm --no-deps --entrypoint node openclaw-gateway \
dist/index.js config set --batch-json '[{"path":"gateway.controlUi.allowedOrigins","value":["https://openclaw.local"]},{"path":"gateway.controlUi.allowInsecureAuth","value":false}]'

docker compose restart openclaw-gateway

Paso 7 — Emparejar tu navegador

Abre https://openclaw.local/ en tu navegador (en una máquina privada, primero mapea 10.80.4.212 openclaw.local en el archivo hosts de tu máquina — /etc/hosts en macOS/Linux, o C:\Windows\System32\drivers\etc\hosts editado como Administrador en Windows).

Primero te toparás con una advertencia de certificado"Tu conexión no es privada" (NET::ERR_CERT_AUTHORITY_INVALID). Esto es esperado con tls internal: el certificado está firmado por la CA local de Caddy, en la que tu sistema operativo no confía. Haz clic en Avanzado → Continuar a openclaw.local (no seguro) para seguir. (Con un dominio real + Let's Encrypt, el certificado tiene confianza pública y esta advertencia nunca aparece — mira Pasar a producción.)

Advertencia de certificado del navegador — clic en Avanzado, luego Continuar

Ahora la página carga — pero con allowInsecureAuth desactivado, el gateway exige emparejamiento de dispositivo antes de que este navegador pueda usar la Control UI. Ese es el flujo seguro haciendo su trabajo. La pantalla muestra un id de solicitud; apruébalo en el host:

docker compose run --rm openclaw-cli devices approve <request-id-from-the-screen>

Pantalla de emparejamiento de dispositivo requerido

De vuelta en el navegador, haz clic en Connect otra vez — empareja y carga el dashboard.


Paso 8 — Cerrar el acceso directo (el endurecimiento de verdad)

El gateway todavía publica 18789 en el host, así que http://<vm-ip>:18789 sigue siendo alcanzable en HTTP plano, saltándose a Caddy. Ciérralo: deja de publicar los puertos del gateway para que Caddy en :443 sea la única puerta. Caddy sigue alcanzándolo por la red interna.

cat > docker-compose.override.yml <<'EOF'
services:
openclaw-gateway:
# Unpublish the gateway's host ports — Caddy reaches it internally, so the
# only entry point is HTTPS on :443.
ports: !reset []

caddy:
image: caddy:2
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config

volumes:
caddy_data:
caddy_config:
EOF
docker compose up -d

ports: !reset [] limpia los puertos que publicaba el compose base.


Paso 9 — Comprobar que está cerrado

curl -s -m 5 http://<vm-ip>:18789/healthz ; echo " <- direct HTTP (should FAIL)"
curl -sk --resolve openclaw.local:443:127.0.0.1 https://openclaw.local/healthz ; echo " <- via Caddy (should work)"

El primero no devuelve nada (conexión rechazada) — la puerta insegura desapareció. El segundo todavía devuelve {"ok":true,"status":"live"}.

Antes/después: puerto directo rechazado, HTTPS funciona

Tu OpenClaw ahora es alcanzable solo por HTTPS, con dispositivo emparejado.

Control UI de OpenClaw sobre HTTPS


Solución de problemas (errores reales)

1. tlsv1 alert internal error

Aparece cuando la dirección del sitio en el Caddyfile era una IP pelada (https://10.80.4.212):

curl: (35) error:0A000438:SSL routines::tlsv1 alert internal error

Los logs de Caddy mostraban que el certificado se obtuvo bien. Causa: TLS elige un certificado por SNI (el hostname en el handshake), pero las conexiones a una IP pelada no envían SNI — así que Caddy no puede emparejar un certificado y aborta. Solución: usa un hostname como dirección del sitio (Paso 2). Un lector con un dominio real nunca se topa con esto — el dominio es el SNI.

2. Emparejamiento de dispositivo requerido

En la primera conexión HTTPS tras desactivar allowInsecureAuth:

Device pairing required — This browser needs one-time approval from the Gateway
host before it can use the Control UI.

Causa: este es el flujo seguro correcto — con la auth insegura desactivada, cada navegador nuevo debe ser aprobado. Solución: docker compose run --rm openclaw-cli devices approve <request-id>. (La CLI puede registrar scope upgrade pending approval … using local fallback pero igual completa la aprobación.)

3. NET::ERR_CERT_AUTHORITY_INVALID

Esperado con tls internal — el certificado está firmado por la CA local de Caddy, en la que tu SO no confía. Haz clic para continuar en una máquina privada. Con un dominio público + Let's Encrypt (abajo), el certificado tiene confianza y no hay advertencia.


Pasar a producción

Con un dominio real, cambia dos líneas en el Caddyfile — apúntalo a tu dominio y quita tls internal:

- openclaw.local {
- tls internal
+ openclaw.example.com {
reverse_proxy openclaw-gateway:18789
}

Luego apunta el registro A del DNS del dominio a la IP pública de la VM y abre 80 + 443 a internet. Caddy aprovisiona y renueva automáticamente un certificado de Let's Encrypt — con confianza, sin advertencia, sin --resolve ni /etc/hosts. Actualiza allowedOrigins a https://openclaw.example.com.


Notas de seguridad

  • Punto de entrada único — solo el :443 de Caddy queda expuesto; el gateway no tiene puertos en el host (Paso 8).
  • Emparejamiento de dispositivo — cada navegador necesita aprobación de una sola vez (openclaw devices).
  • Lista de orígenes permitidos — la Control UI solo acepta https://openclaw.local.
  • Rota los secretos — regenera OPENCLAW_GATEWAY_TOKEN si alguna vez se mostró (capturas de pantalla, pantallas compartidas).

Qué sigue

Terminaste la Parte 2 de la serie Self-hosting OpenClaw. Sigue:

Los archivos complementarios (Caddyfile, docker-compose.override.yml) viven en el repo de manifiestos de WiLine (enlace pendiente con el repo).


Desmontaje

Para eliminar solo Caddy y reabrir los puertos directos, borra docker-compose.override.yml y Caddyfile, luego docker compose up -d. Para detener todo:

docker compose down