Saltar al contenido principal

TLS y HTTPS

Resumen​

Los contenedores de UltraTorrent hablan HTTP plano y están diseñados para colocarse detrás de algo que termine TLS. Eso es deliberado: el manejo de certificados pertenece a un solo lugar, y todo despliegue serio ya tiene un proxy inverso.

Así que "habilitar HTTPS" significa: pon un proxy al frente y dale un certificado. Esta página cubre la mitad del certificado; Proxy inverso cubre la mitad del enrutamiento.

Tienes tres opciones realistas:

Úsala cuandoEsfuerzo
Let's Encrypt — HTTP-01La máquina es alcanzable desde el internet en el puerto 80El más bajo
Let's Encrypt — DNS-01Solo en LAN, detrás de CGNAT, o si quieres un wildcardMedio — necesita un token de API de un proveedor de DNS
Certificado personalizado / de CA internaPKI corporativa, o un homelab con su propia CAMedio — tienes que distribuir la CA a los clientes
Mira este tutorial

Video próximamente.

Requisitos previos​

  • Una instalación con Docker Compose funcionando y un proxy inverso.
  • Un nombre de dominio que controles. Una dirección IP solo de LAN no puede obtener un certificado público — necesitas un nombre de host, incluso para un servicio puramente interno (DNS-01 emite certificados para nombres de host que nunca resuelven públicamente).
  • Para HTTP-01: el puerto 80 alcanzable desde el internet, y el DNS ya apuntando al host.

Requerimientos​

Insignificantes — terminar TLS cuesta unos pocos MB de RAM. Lo que cuesta es atención: un certificado vencido es una caída del servicio.

Puertos​

PuertoNecesario para
80El desafío ACME HTTP-01 y la redirección HTTP→HTTPS. Mantenlo abierto incluso después de tener un certificado — la renovación lo usa
443HTTPS
—DNS-01 no necesita ningún puerto entrante

Volúmenes​

Lo que sea que tu proxy use para guardar certificados. Respáldalo:

ProxyAlmacén de certificados
Caddy (perfil proxy incluido)el volumen caddy_data
Traefikacme.json
NGINX + certbot/etc/letsencrypt
Nginx Proxy Managersus carpetas data/ y letsencrypt/
HAProxytu directorio de PEM combinados

Permisos​

Las llaves privadas deben ser legibles solo por el proceso que las usa:

sudo chmod 600 /etc/haproxy/certs/torrents.example.com.pem
sudo chown root:root /etc/haproxy/certs/torrents.example.com.pem

Nunca subas una llave a git; nunca pongas una en .env.

Cómo encaja todo​

Fíjate en la última línea: como TLS termina en el proxy, el WebSocket del navegador es automáticamente wss:// — eso no lo configuras en ninguna parte. Solo funciona si tu proxy reenvía el upgrade; mira Proxy inverso.

Paso a paso​

Caddy obtiene, instala y renueva un certificado de Let's Encrypt con cero configuración más allá de nombrar tu dominio.

Edita deploy/Caddyfile (o tu propio Caddyfile) para que la etiqueta del sitio sea tu dominio en vez de :80:

torrents.example.com {
encode gzip

@api path /api/* /ws/*
handle @api {
reverse_proxy backend:4000
}

handle {
# nginx-unprivileged escucha en el 8080 — no en el 80.
reverse_proxy frontend:8080
}
}
docker compose --profile proxy up -d
docker compose logs -f proxy # observa cómo se emite el certificado

Requerimientos: que el DNS de torrents.example.com ya apunte a este host, y que los puertos 80 y 443 sean alcanzables. Caddy se encarga solo de la redirección HTTP→HTTPS y de la renovación.

Wildcard / DNS-01 con Caddy necesita un plugin del proveedor de DNS, lo que implica una imagen de Caddy personalizada:

FROM caddy:2-builder AS builder
RUN xcaddy build --with github.com/caddy-dns/cloudflare
FROM caddy:2-alpine
COPY --from=builder /usr/bin/caddy /usr/bin/caddy
*.example.com {
tls {
dns cloudflare {env.CLOUDFLARE_API_TOKEN}
}
reverse_proxy frontend:8080
}

Verificación​

La cadena está completa y las fechas son correctas:

openssl s_client -connect torrents.example.com:443 -servername torrents.example.com < /dev/null 2>/dev/null \
| openssl x509 -noout -subject -issuer -dates
subject=CN = torrents.example.com
issuer=C = US, O = Let's Encrypt, CN = R11
notBefore=Jul 12 09:14:02 2026 GMT
notAfter=Oct 10 09:14:01 2026 GMT

Sin huecos en la cadena (el clásico bug de "funciona en Chrome, falla en Android"):

openssl s_client -connect torrents.example.com:443 -servername torrents.example.com < /dev/null 2>&1 | grep -i "verify"
Verify return code: 0 (ok)

HTTP redirige a HTTPS:

curl -sI http://torrents.example.com | head -1
# HTTP/1.1 301 Moved Permanently

La app funciona sobre TLS, WebSocket incluido:

curl -s https://torrents.example.com/api/system/live

Luego abre la UI y confirma que la barra de progreso de una descarga se actualiza en vivo — eso prueba que wss:// está tunelizando correctamente a través de tu terminador de TLS.

Ventana emergente de información del sitio de Chrome en la UI de UltraTorrent, mostrando &quot;La conexión es segura&quot;

¿Dónde se metió el candado?

Chrome 117 y posteriores reemplazaron el ícono del candado con un ícono de información del sitio (los dos deslizadores). Haz clic en él: un certificado funcional dice "La conexión es segura", y La conexión es segura → El certificado es válido muestra el emisor. Una cadena mal configurada dice "No es seguro" ahí, incluso cuando la página todavía carga.

Proxy inverso​

TLS y el enrutamiento son dos mitades de un mismo trabajo. Configura el enrutamiento (y el obligatorio upgrade de WebSocket) en Proxy inverso.

Actualizaciones​

Los certificados son independientes de las actualizaciones de UltraTorrent — reconstruir el stack no los toca, siempre que el almacén de certificados de tu proxy sea un volumen o una ruta del host fuera del repo.

Después de cualquier renovación de certificado, el proxy tiene que recargarse. Caddy y Traefik lo hacen solos. NGINX y HAProxy necesitan un deploy hook (mira la pestaña de certbot más arriba).

Copias de seguridad​

Respalda el almacén de certificados (caddy_data, acme.json, /etc/letsencrypt, tu directorio de PEM) junto con tu .env y el volcado de la base de datos. Perderlo es recuperable — pero Let's Encrypt aplica límites de tasa a los certificados duplicados (5 por semana por cada conjunto idéntico de nombres), así que perderlo repetidamente te dejará sin poder reemitir por días.

Resolución de problemas​

SíntomaCausaSolución
ACME falla: "Timeout during connect" en HTTP-01El puerto 80 no es alcanzable desde el internetAbre el 80 en el firewall y en el router. En hosts de nube: revisa el grupo de seguridad. O cámbiate a DNS-01, que no necesita ningún puerto entrante
ACME falla: "Invalid response … 404"Otra cosa está respondiendo en el puerto 80, o el proxy no está sirviendo /.well-known/acme-challenge/Haz que el proxy sea lo único en el puerto 80
"too many certificates already issued"Límite de tasa de Let's Encrypt — normalmente por un ciclo de reintentos fallidosEspera a que pase (una semana), y la próxima vez prueba contra --staging / el directorio staging de ACME
El certificado es válido en Chrome, no confiable en Android/iOSFalta el intermedio — serviste solo la hojaSirve la fullchain: cat certificate.crt intermediate.crt > fullchain.pem
ERR_SSL_PROTOCOL_ERROREstás accediendo por HTTPS a un puerto que solo habla HTTP (p. ej. https://host:8080)El contenedor habla HTTP plano. Pasa por el 443 del proxy
El candado es válido, pero la UI nunca se actualizaTLS está bien; el upgrade del WebSocket se está descartandoMira Proxy inverso → verificación
Advertencias de contenido mixto en la consolaAlgo está pidiendo http:// desde una página https://La SPA usa URLs relativas de forma predeterminada. Revisa CORS_ORIGIN y cualquier build arg VITE_API_URL personalizado
Traefik ignora acme.jsonPermisos de archivo equivocadoschmod 600 acme.json
Certificado vencidoLa renovación corrió pero el proxy nunca se recargóAñade un deploy hook que recargue el proxy; verifícalo con certbot renew --dry-run
Advertencia del navegador con un certificado de mkcertEl dispositivo no confía en tu CA raízInstala la CA raíz en ese dispositivo — o cámbiate a DNS-01 con un dominio real

Mejores prácticas​

  • Automatiza la renovación, y luego comprueba que funciona — certbot renew --dry-run — mucho antes del día 89.
  • Prefiere Caddy o Traefik si no tienes una opinión fuerte: emiten y renuevan certificados sin ningún cron job que se te pueda olvidar.
  • Usa DNS-01 para instalaciones solo en LAN. Un certificado público para un nombre de host privado, sin puertos entrantes, sin advertencias del navegador, sin una CA que distribuir.
  • Sirve siempre la cadena completa, nunca la hoja sola.
  • Mantén el puerto 80 abierto para las redirecciones y las renovaciones por HTTP-01, incluso cuando HTTPS ya funcione.
  • TLS 1.2 como mínimo, TLS 1.3 preferido.
  • Respalda el almacén de certificados junto con todo lo demás.
  • Nunca termines TLS dentro de los contenedores de la app — están hechos para vivir detrás de un proxy.
  • ¿Despliegue público? TLS es lo mínimo indispensable, no toda la historia. Lee también Seguridad.

Preguntas frecuentes​

¿Puedo obtener un certificado para una dirección IP pelada? De Let's Encrypt no. Usa un nombre de host (DNS-01 funciona incluso si solo resuelve en tu LAN).

¿Tengo que cambiar algo en UltraTorrent para HTTPS? Solo CORS_ORIGIN en .env, a tu origen público https://. La SPA usa URLs relativas, así que wss:// se sigue automáticamente.

¿Puedo terminar TLS directamente en el contenedor del frontend? No — es un nginx de HTTP plano por diseño. Usa un proxy.

¿Autofirmado para una prueba rápida? Está bien para un laboratorio, pero todos los dispositivos advertirán, y algunas herramientas se negarán. mkcert es una mejor versión de la misma idea.

¿El perfil de Caddy incluido renueva automáticamente? Sí, siempre que se mantenga corriendo y caddy_data persista.

Lista de verificación​

  • Existe un nombre de host y (para HTTP-01) resuelve al host
  • El proxy inverso funciona primero sobre HTTP plano
  • Los puertos 80 y 443 son alcanzables (HTTP-01) — o hay un token de API de DNS en su lugar (DNS-01)
  • Certificado emitido, se sirve la fullchain
  • openssl s_client reporta Verify return code: 0 (ok)
  • HTTP redirige a HTTPS
  • La renovación está automatizada y probada con dry-run
  • El proxy se recarga al renovar (deploy hook, para NGINX/HAProxy)
  • CORS_ORIGIN actualizado al origen https://; backend recreado
  • La UI en vivo sigue actualizándose sobre wss://
  • El almacén de certificados está incluido en las copias de seguridad
  • La llave privada está en 0600, es propiedad del usuario del proxy, y no está en git

Mira también​