El circuito, en vivo
Pulsa un nodo para ver qué hace, qué ve y cómo comprobarlo. El paquete morado va cifrado con TLS de punta a punta; solo el último salto, dentro de la máquina del alumno, es HTTP plano.
Pulsa un nodo
- Dónde está
- Qué ve
- Qué hace
- Fichero de configuración
- Comprobarlo
Paso a paso
El mismo recorrido en una tabla, con nombres, direcciones y el comando que lo demuestra.
| # | Componente | Dónde / IP / puerto | Qué hace | Comprobar con |
|---|
Guía del alumno
Lo que necesitas tener y lo que tienes que hacer. Tu usuario de ejemplo aquí es jviejo; sustitúyelo por el que te asigne el profesor.
Lo que necesitas tener
- Docker (con Compose) en tu ordenador, o Traefik v3 como binario.
- Cliente OpenSSH (
ssh); viene en macOS, Linux y Windows 10+. - Tu aplicación escuchando en un puerto local (p. ej.
3333). - Salida a Internet hacia
35.255.164.51puerto 22 (SSH). Nada más. - El usuario que te da el profesor:
[a-z][a-z0-9]{2,19}. - El equipo encendido y el túnel abierto mientras quieras ser visible.
Lo que NO necesitas
- IP pública, ni abrir puertos en tu router.
- Un dominio propio, ni DNS que configurar.
- Generar el certificado a mano: lo pide tu Traefik solo.
Qué intercambias con el profesor
| Tú entregas | Solo tu clave pública (.pub). La privada nunca sale de tu máquina. |
|---|---|
| Tú recibes | Tu usuario y el host del túnel (hoy 35.255.164.51; tunnel.codecrypto.dev cuando exista el DNS). |
| Tus URLs | cualquier-cosa.usuario.alumnos.codecrypto.dev, todas por un túnel. |
| Reglas del nombre | Minúsculas, números y guiones; mínimo una etiqueta antes de tu usuario (lista.jviejo…). jviejo.alumnos… sin nada delante no vale. |
Pasos
Genera tu clave SSH (en tu máquina) y envía la pública
ssh-keygen -t ed25519 -f ~/.ssh/codecrypto_alumno -C "jviejo" cat ~/.ssh/codecrypto_alumno.pub # esto es lo que envías al profesor
Configura el túnel en
~/.ssh/configHost relay-jviejo HostName 35.255.164.51 User jviejo IdentityFile ~/.ssh/codecrypto_alumno IdentitiesOnly yes ServerAliveInterval 30 ServerAliveCountMax 3 ExitOnForwardFailure yes # socket en el servidor -> tu Traefik local RemoteForward /var/lib/alumnos/jviejo/web.sock 127.0.0.1:18443Esa línea
RemoteForwardes el-Rde ssh: el servidor crea el socket y lo entrega a tu puerto18443.Levanta tu Traefik (el «borde»)
Copia la carpeta
relay/alumno/edge/(docker-compose.yml,traefik.yml,dynamic.yml) y descríbela así. Una ruta por cada nombre que quieras publicar:# dynamic.yml http: routers: miapp: rule: Host(`miapp.jviejo.alumnos.codecrypto.dev`) entryPoints: [websecure] tls: certResolver: le # certificado de Let's Encrypt automático service: miapp services: miapp: loadBalancer: servers: - url: "http://host.docker.internal:3333" # tu app fuera de Docker # - url: "http://mi-contenedor:80" # o un contenedor del mismo composedocker compose up -d # tras cambiar dynamic.yml: docker restart alumno-edge-traefik
Abre el túnel
ssh -N relay-jviejo # déjalo abierto
La primera vez, tu Traefik pide el certificado a Let's Encrypt a través de ese mismo túnel: tarda unos segundos.
Comprueba desde fuera
curl -sv https://miapp.jviejo.alumnos.codecrypto.dev/ -o /dev/null
Debes ver un certificado de Let's Encrypt para tu nombre y un
HTTP 200.Mantenlo vivo (opcional)
brew install autossh # macOS autossh -M 0 -f -N relay-jviejo
python3 -m http.server sirve todo el directorio donde lo lances, incluido .git y ficheros con claves. Usa --directory sobre una carpeta pensada para publicarse.
Si algo falla
| Lo que ves | Causa más probable | Solución |
|---|---|---|
ERR_CERT_COMMON_NAME_INVALID | Llega a tu Traefik pero no tiene ruta para ese nombre. | Añade el router con Host(...) y reinicia el borde. |
remote port forwarding failed for listen path | Ya hay otro túnel tuyo abierto, o el directorio/usuario no existe. | Cierra el otro ssh; confirma tu usuario con el profesor. |
| La conexión se corta sin respuesta | El túnel no está abierto (no hay socket). | Lanza ssh -N relay-jviejo. |
| Certificado de «TRAEFIK DEFAULT CERT» | Tu Traefik no tiene (todavía) certificado para ese nombre. | Revisa docker logs alumno-edge-traefik; el túnel debe estar abierto para validar. |
404 page not found | Tu Traefik no reconoce el host de la petición. | El Host() debe coincidir exactamente con el nombre usado. |
502 Bad Gateway desde tu Traefik | Tu app no escucha o escucha solo en 127.0.0.1 y el borde corre en Docker. | Escucha en 0.0.0.0 o usa un contenedor en la misma red. |
Permission denied (publickey) | El profesor aún no dio de alta tu clave, o usas otra. | Comprueba IdentityFile y que enviaste la .pub correcta. |
Administración
En el servidor (ssh jose@35.255.164.51, con sudo). Los ficheros viven en /opt/platform/relay-alumnos.
Alta y baja
# alta: crea usuario, directorio y registra la clave pública sudo /opt/platform/relay-alumnos/bin/alta-alumno.sh jviejo 'ssh-ed25519 AAAA… jviejo' # baja: corta sesiones, borra clave, usuario y sockets sudo /opt/platform/relay-alumnos/bin/baja-alumno.sh jviejo
El script valida el usuario ([a-z][a-z0-9]{2,19}, sin nombres reservados) y que la clave sea de una sola línea y no lleve opciones inyectadas.
Quién está conectado y qué pasa
# túneles abiertos (un socket por alumno) sudo ls -l /var/lib/alumnos/*/web.sock # tráfico real: SNI, destino, bytes y duración docker logs nginx-alumnos --since 10m 2>&1 | grep -v 'sni=-' # sshd: quién abrió sesión sudo journalctl -u ssh --since "10 min ago" | grep -i 'accepted publickey'
Las líneas con sni=- son el healthcheck del contenedor, no tráfico.
Piezas instaladas y su configuración
| sshd | /etc/ssh/sshd_config.d/50-alumnos.conf | Grupo alumnos: sin shell, solo reenvío inverso a sockets Unix (AllowTcpForwarding remote + PermitListen 127.0.0.1:1 para anular los puertos TCP). |
| nginx | /opt/platform/relay-alumnos/nginx/nginx.conf | stream + ssl_preread; el map elige /var/lib/alumnos/$alumno/web.sock. |
| Traefik | /opt/platform/traefik/dynamic.yml | Bloque tcp: con el router alumnos (passthrough). Copias: .bak-relay, .bak-wildcard. |
| Claves | /etc/ssh/authorized_keys/<usuario> | Gestionadas por el administrador, no por el alumno (restrict,port-forwarding). |
*.alumnos.codecrypto.academy) siguen por sus routers. La regex del relay se comprobó contra los 111 nombres reales sin ninguna coincidencia.Escalabilidad
Qué aguanta hoy, qué se rompe primero y por dónde crecer. Las cifras marcadas medido salen de este servidor; las estimado son extrapolaciones lineales a partir de una sola sesión, no pruebas de carga.
Capacidad estimada solo del relay
| Alumnos con túnel abierto | RAM de sshd | Qué más pesa |
|---|---|---|
| 100 | ≈ 0,9 GB estimado | Nada relevante. |
| 500 | ≈ 4,3 GB estimado | Tormentas de reconexión (ver abajo). |
| 1 000 | ≈ 8,5 GB estimado | Comparte el servidor con la plataforma: ancho de banda y un único punto de fallo. |
El servidor no descifra TLS (passthrough), así que casi no gasta CPU: solo copia bytes y mantiene el cifrado SSH. Cada petición cruza la red del servidor dos veces (entra por 443 y sale por el túnel, y lo mismo en la respuesta), de modo que el ancho de banda consumido es ≈ 2× la carga útil.
Qué se rompe primero (por orden)
| # | Cuello de botella | Por qué | Remedio |
|---|---|---|---|
| 1 | Límites de Let's Encrypt | Hay un tope semanal de certificados nuevos por dominio registrado (codecrypto.dev, común a todos los alumnos; unos 50 por semana, verifica el valor vigente). Cada nombre nuevo es un certificado. Las renovaciones no cuentan. | (a) Pedir ampliación a Let's Encrypt. (b) Registrar alumnos.codecrypto.dev en la Public Suffix List: cada usuario.alumnos… pasa a ser su propio dominio. (c) Un comodín por alumno con DNS-01 delegado (p. ej. acme-dns): 1 certificado por alumno y no por proyecto. (d) Otra CA ACME (ZeroSSL, Google Trust Services). |
| 2 | Tormenta de reconexión | sshd tiene MaxStartups 10:30:100 medido. Tras un corte, cientos de túneles reconectando a la vez empiezan a rechazarse a partir de 10 conexiones sin autenticar. | Subir MaxStartups (p. ej. 100:30:300) y que el cliente reconecte con espera aleatoria (launchd/systemd con ThrottleInterval o un sleep $((RANDOM%30))). |
| 3 | Gestión de claves a mano | Un fichero por alumno en /etc/ssh/authorized_keys/ y un alta manual por cada uno. | Usar certificados SSH de vida corta firmados por Vault (ya los usas en el bastión con vault write ssh/sign/alumno) con TrustedUserCAKeys: sin ficheros por alumno y con caducidad automática. Altas en lote desde un CSV o desde la plataforma. |
| 4 | Un solo servidor | Es punto único de fallo y el ancho de banda de la VM es compartido con toda la plataforma. | Repartir alumnos en varios nodos de relay: una capa L4 delante con un map usuario → nodo (el socket vive en el nodo donde el alumno abrió el túnel) o una zona de DNS por grupo. |
| 5 | Abuso y cuotas | Cada alumno publica lo que corre en su máquina y no hay límite por alumno. Un límite de conexiones por nombre no es viable: el SNI aún no existe cuando nginx aplica limit_conn. | Limitar ancho de banda en stream (proxy_download_rate / proxy_upload_rate) y recursos del usuario con un slice de systemd. Para limitar por visitante hay que activar PROXY protocol (hoy nginx solo ve la IP de Traefik). |
| 6 | Visibilidad | No hay métricas por alumno. | El log de nginx ya incluye SNI, bytes y duración: enviarlo a la pila de observabilidad que ya tienes (/opt/platform/observability) y sacar bytes por alumno y nº de túneles activos. |
Hoja de ruta
- Lo que hay hoy, más el email de contacto de ACME.
- Subir
MaxStartupsy reconexión con retardo aleatorio. - Script de alta en lote desde CSV.
- Decidir ya la estrategia de certificados (límite nº 1).
- Certificados SSH efímeros con Vault.
- Public Suffix List o comodín por alumno.
- PROXY protocol y límites de ancho de banda.
- Métricas y alertas por alumno.
- Varios nodos de relay con enrutado
usuario → nodo. - Alta automatizada desde la plataforma del campus.
- Pruebas de carga reales antes de cada curso.