Circuito de la información
Relay de alumnos · *.alumnos.codecrypto.dev

Cómo llega una petición desde cualquier navegador hasta la aplicación que un alumno tiene en su propio ordenador, sin que el alumno tenga IP pública, abra puertos ni gestione certificados a mano.

Servidor alumnos-vm · 35.255.164.51 Debian 12 · OpenSSH 9.2 Traefik 3.7 · nginx 1.27 Ejemplo https://lista3.jviejo.alumnos.codecrypto.dev

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.

—
SERVIDOR alumnos-vm · 35.255.164.51 · GCP e2-standard-8 MÁQUINA DEL ALUMNO · detrás de NAT · sin IP pública INTERNET
TLS de punta a punta (visitante ⇄ Traefik del alumno) Dentro del túnel SSH (segunda capa de cifrado) HTTP plano, solo en localhost del alumno
—

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.

#ComponenteDónde / IP / puertoQué haceComprobar 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.51 puerto 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ú entregasSolo tu clave pública (.pub). La privada nunca sale de tu máquina.
Tú recibesTu usuario y el host del túnel (hoy 35.255.164.51; tunnel.codecrypto.dev cuando exista el DNS).
Tus URLscualquier-cosa.usuario.alumnos.codecrypto.dev, todas por un túnel.
Reglas del nombreMinúsculas, números y guiones; mínimo una etiqueta antes de tu usuario (lista.jviejo…). jviejo.alumnos… sin nada delante no vale.

Pasos

  1. 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
  2. Configura el túnel en ~/.ssh/config

    Host 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:18443

    Esa línea RemoteForward es el -R de ssh: el servidor crea el socket y lo entrega a tu puerto 18443.

  3. 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 compose
    docker compose up -d                 # tras cambiar dynamic.yml:  docker restart alumno-edge-traefik
  4. 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.

  5. 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.

  6. Mantenlo vivo (opcional)

    brew install autossh                   # macOS
    autossh -M 0 -f -N relay-jviejo
Lo que publiques es público. Cualquiera con el nombre puede entrar (y los nombres con certificado quedan en los registros públicos de Certificate Transparency). No expongas carpetas completas: 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 vesCausa más probableSolución
ERR_CERT_COMMON_NAME_INVALIDLlega 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 pathYa 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 respuestaEl 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 foundTu Traefik no reconoce el host de la petición.El Host() debe coincidir exactamente con el nombre usado.
502 Bad Gateway desde tu TraefikTu 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.confGrupo 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.confstream + ssl_preread; el map elige /var/lib/alumnos/$alumno/web.sock.
Traefik/opt/platform/traefik/dynamic.ymlBloque 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).
No toca lo existente. Los proyectos de la plataforma (*.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.

≈ 8,5 MB
RAM por túnel de alumno (2 procesos sshd) medido
17,6 MiB
nginx-alumnos entero, 9 procesos, 0 % CPU medido
18 GB libres
de 32 GB (8 vCPU), con 113 contenedores ya en marcha medido

Capacidad estimada solo del relay

Alumnos con túnel abiertoRAM de sshdQué más pesa
100≈ 0,9 GB estimadoNada relevante.
500≈ 4,3 GB estimadoTormentas de reconexión (ver abajo).
1 000≈ 8,5 GB estimadoComparte 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 botellaPor quéRemedio
1Límites de Let's EncryptHay 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).
2Tormenta de reconexiónsshd 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))).
3Gestión de claves a manoUn 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.
4Un solo servidorEs 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.
5Abuso y cuotasCada 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).
6VisibilidadNo 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

Fase 1 · hasta ~100 alumnos
  • Lo que hay hoy, más el email de contacto de ACME.
  • Subir MaxStartups y reconexión con retardo aleatorio.
  • Script de alta en lote desde CSV.
  • Decidir ya la estrategia de certificados (límite nº 1).
Fase 2 · 100–500
  • 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.
Fase 3 · 500+
  • Varios nodos de relay con enrutado usuario → nodo.
  • Alta automatizada desde la plataforma del campus.
  • Pruebas de carga reales antes de cada curso.
Lo que no está medido. No se ha hecho ninguna prueba de carga: las cifras de capacidad son extrapolaciones de una sola sesión. Antes de abrirlo a una clase completa conviene simular, por ejemplo, 100 túneles concurrentes y una reconexión simultánea.