Podman, IPs reales y La Liga

Vistas: 1
Podman, IPs reales y La Liga

Llevo años contándote cómo monto mis cosas, y casi siempre lo he contado con una tranquilidad que no siempre estaba justificada. Pues resulta que no. En la torre donde trabajo tengo contenedores que no llevan demonio detrás, y en el servidor que sirve atareao.es tengo exactamente lo contrario, un demonio arrancado como root que gobierna todo lo que allí corre. Mismo software por debajo, dos filosofías para hacer lo mismo. Y esa diferencia, que parece un detalle de fin de semana, me ha costado un servidor que se caía, una analítica que me mentía sobre quién me visitaba y, ya puestos, un sábado por la tarde en el que media internet dejó de responder. Una de esas tres cosas acabó, literalmente, en una sentencia judicial. Así que hoy te voy a contar las tres, y te aviso desde ya de que no hay moraleja fácil ni culpable único.

Dos mundos bajo el mismo techo

Te pongo en situación. En casa, en la torre Slimbook, no hay ningún demonio de contenedores. Abro una terminal, lanzo lo que necesite y a otra cosa. Mis contenedores son procesos míos, hijos de mi sesión, y no hay ningún servicio con privilegios vigilándolos por detrás. Eso es Podman, y para un usuario normal Podman es rootless por defecto, sin que haya que activar nada.

En el servidor donde vive atareao.es, en cambio, hay un demonio. Un dockerd que arranca como root y que es el que manda, al que le pides el contenedor y él lo pone en marcha. Es el Docker de toda la vida. Dos formas de hacer lo mismo que no se diferencian en el nombre del binario, sino en dónde vive el privilegio.

Y aquí es donde entra la palabra que se ha vuelto central en toda esta historia, rootless. ¿Qué significa de verdad? Que dentro del contenedor el usuario cero no es el root de la máquina. Es un truco del kernel, los espacios de nombres de usuario. Dentro, el proceso se cree root; fuera, es un usuario sin privilegios con un rango de identificadores prestados. Y eso se ve en una línea de terminal, sin montar nada, con podman unshare cat /proc/self/uid_map te devuelve algo parecido a esto,

         0       1000          1
         1     100000     200000

El cero de la izquierda, el de dentro, corresponde a un 1000 cualquiera de fuera. O sea, que si alguien escapa del contenedor no se encuentra el root de la máquina, se encuentra un don nadie. Con un matiz honesto, para no venderte una película, rootless sí se apoya en newuidmap y newgidmap, que son binarios con el bit setuid, y así es como se monta ese mapa de identificadores. Lo que no hay es un servicio con privilegios gobernando tus contenedores.

Por eso decidí hace tiempo llevarme el servidor a ese lado. Cuantos menos permisos tenga algo, mejor, y si alguien escala privilegios dentro de un contenedor montado como root, la destroza que puede montar es de mucho cuidado. Pero la seguridad que ganaba por un lado me iba a costar algo por otro, algo que no sale en los diagramas bonitos. La red. Y antes de contarte esa parte tengo que contarte un pecado mío, porque si no, no vas a entender por qué existe todo lo que viene después.

El pecado que me da vergüenza contar

De vez en cuando, atareao.es se caía. Un rato, volvía, y a otra cosa. Yo no tenía monitorización, no tenía alertas, no tenía una gráfica de memoria. Lo que tenía era la sensación de que «últimamente el servidor va raro» y un reinicio a mano. Y aquí está el detalle que me da vergüenza contar en voz alta. No anoté ni una sola medida. Ni cuándo pasó, ni cuánto duró, ni qué estaba haciendo la máquina en ese momento. Ni un dato 😅

Cuando un problema te incomoda, el cerebro busca la explicación más barata, y la más barata no exige medir. Así que hice lo que hacemos todos cuando algo se rompe y no sabemos por qué. Buscar un culpable que esté lejos, sea grande y no se pueda quejar. Y el culpable cómodo era el proveedor. «Será Contabo», me dije. Un VPS que se cae, la red que va mal, vecinos ruidosos. No lo comprobé. Ni una línea de log, ni una gráfica de carga, ni una prueba desde fuera. Decidí que la culpa era del alojamiento y actué en consecuencia. Copia de seguridad, contenedores al servidor nuevo, DNS y a correr. Migré de Contabo a Hetzner entero, con la convicción de un tipo que ha resuelto el problema porque ha cambiado de escenario.

Spoiler. Hoy atareao.es corre en Hetzner, con Docker rootful, para más ironía, y estoy contento. Pero el problema que me llevó hasta allí no había desaparecido. Se había mudado conmigo, dentro de las cajas.

Con el tiempo lo tuve claro, el culpable estaba dentro, en la capa de caché. Un Redis, hoy Valkey, al que le puede pasar algo muy poco glamuroso si no le pones límite de memoria. Se la come. Se queda con la RAM de la máquina y el kernel, al ver que nadie suelta nada, mata al que más ocupa. El sitio se cae, tú reinicias, todo vuelve, hasta la próxima.

Y te aviso de cómo lo digo, porque no quiero venderte una certeza que no tengo. Eso es mi diagnóstico, y la doctrina general de cómo una caché tumba un WordPress (el maxmemory que no está puesto, la política de expulsión, el OOM killer, los picos al hacer fork para persistir en disco), no una causa medida. De aquello no guardé métricas. Es exactamente el pecado del párrafo anterior. Si mañana quiero saberlo de verdad, tengo que mirar valkey-cli INFO memory y buscar en el host si el kernel mató algo con journalctl -k | grep -i -E 'oom|killed process'. Sin esos datos, culpar a la caché es el mismo error que culpar al proveedor.

De propina, tres fechas que van juntas. En marzo de 2024 Redis cambió de licencia y dejó de ser open source según la definición de la OSI; ocho días después, la Linux Foundation anunció Valkey, que continúa el desarrollo desde Redis 7.2.4 y se mantiene bajo licencia BSD. En octubre de 2025 llegó una CVE crítica de ejecución remota de código en Redis con scripting Lua, la CVE-2025-49844, con dos notas para el mismo fallo, un 9,9 según NVD y un 10,0 según el investigador que la publicó, porque son conjuntos de métricas distintos. Y la lección que enlaza con todo esto es sencilla. Cambiar de nombre no te ahorra parchear. El código es el mismo y las vulnerabilidades heredadas también.

Umami, o por qué la analítica tiene que ser tuya

Todo lo anterior viene a cuento porque en ese servidor hay un contenedor con el que hablo mucho, Umami, mi analítica. Está autoalojada, corre en mi propia máquina sobre PostgreSQL, y es la que me dice cuánta gente entra, qué páginas ve y de dónde llega. Licencia MIT, sin sorpresas de propietario. El panel es mío, los datos son míos, y si mañana quiero borrarlo todo, lo borro yo.

Y hay una razón de peso para que sea Umami y no otra cosa. No usa cookies. No te deja un identificador en el navegador, no te sigue de un sitio a otro y no recoge datos personales de forma automática. Para agrupar visitas hace un hash con el identificador del sitio, tu IP, tu User-Agent y una sal que rota cada mes. Eso agrupa actividad, no fabrica una ficha tuya. Por eso no uso Google Analytics. No quiero que mi web sea una sucursal de nadie.

Con el matiz que dice su propia documentación, y que conviene repetir. Cumplir el RGPD depende de cómo lo configures tú y de qué otros servicios tenga el sitio. Umami te quita el problema del banner de cookies, no el de pensarlo. Y es justamente esa vocación de privacidad, la de medir sin molestar al que entra, la que convierte el asunto de las IPs en algo más que un capricho técnico. Si mides visitas para saber de dónde viene la gente, la IP no es un adorno. Es el dato que te da el país, te da la referencia y te dice si alguien te lee desde donde crees o desde donde no tenías ni idea.

Víctor me abre los ojos: con rootless, las IPs reales no llegan

El aviso no lo pillé yo en mis propios datos. Me lo trajo Víctor, el de vctrsnts.github.io, que le da vueltas a la infraestructura más de lo que es razonable y que cuenta en su web cómo ha montado sus cosas, con una frase seca. «Con contenedores rootless, la IP real del visitante no llega».

Y lo peor no es eso. Lo peor es que la analítica no se queja. Umami cuenta, dibuja gráficas y todo parece sano. Lo que no llega es el «quién». Dentro del contenedor las peticiones se ven con la dirección del intermediario, no con la del visitante. El panel parece correcto y te está contando otra película. En mi caso, si no llega la IP real, tampoco llega el país, ni la referencia geográfica, ni esa información básica que te da una idea de hacia dónde va la cosa.

Y ahí me tragué una cosa incómoda. Yo había montado todo aquello y el fallo lo vio alguien que lo mira desde fuera. Sin Víctor, seguiría creyendo que mi analítica era perfecta, porque nada en la pantalla me decía lo contrario. Lo primero que hice fue no tocar producción. Nada de probar apaños en el servidor que sirve el sitio. En producción se opera, no se experimenta. Así que monté un entorno de pruebas en un VPS aparte, con Podman rootless, para reproducir el escenario sin afectar a nadie. Y con dos máquinas, la de verdad y la de jugar, ya puedes separar «esto pasa por el diseño» de «esto pasa porque es mi servidor». Lo que encontré fue que el problema no era mío. Era estructural.

No es cosa de Podman, es cosa de rootless

Vamos a la red, que es donde está la sorpresa. Cuando publicas un puerto en un contenedor rootless, ese puerto no lo abre el kernel por ti. Lo abre un proceso en tu espacio de usuario que escucha ahí y reenvía hacia dentro del contenedor. En Podman ese intermediario se llama rootlessport, y su propia documentación lo describe como «a userspace proxy that does not preserve client source IPs», o sea, un proxy en espacio de usuario que no conserva las IPs de origen. En Docker el reenviador es el de RootlessKit, y arrastra exactamente el mismo comportamiento por defecto. La guía de problemas conocidos de Docker lo dice sin rodeos, el reenvío con -p no propaga la IP de origen por defecto.

Y no es casualidad que los dos hagan lo mismo, porque los dos se apoyan en la misma pieza, RootlessKit, que se describe a sí misma como una implementación nativa de Linux de «root falso» usando user_namespaces. Así que la frase que ordena todo este bloque es esta. No es cosa de Podman, es cosa de rootless. Donde no hay NAT del kernel, hay un proxy en medio. Dentro del contenedor ves al mensajero, no a quien escribió la carta. La IP de origen se pierde en el pasamanos.

Si quieres comprobar de qué lado estás, hay dos preguntas y cuatro comandos, y con ellos te haces el mapa entero,

# ¿mi motor es rootless?
podman info --format '{{.Host.Security.Rootless}}'      # true / false
docker info --format '{{.SecurityOptions}}'             # busca 'rootless' en la lista

# ¿quién publica mis puertos? (la pieza que se come la IP)
podman info --format '{{.Host.RootlessPortForwarder}}'  # rootlessport / pasta
podman info --format '{{.Host.RootlessNetworkCmd}}'     # pasta / slirp4netns

En un Podman 6.x rootless de los de hoy lo normal es que te devuelva rootlessport y pasta. Lo tienes todo automatizado en el primero de los scripts que he dejado en el gist del episodio, que además te enseña el mapa de UID y el reenviador en pantalla.

Y aquí tengo que poner un matiz, porque en 2026 la frase «rootless siempre pierde la IP» ya no es exacta, y decirlo así no debilita nada, lo hace más creíble. Lo que se pierde es la IP con el comportamiento por defecto, que es el que yo tenía montado. Pero RootlessKit publica una tabla de sus modos de reenviar puertos y algunos sí la conservan. Con el driver builtin, que propaga la IP solo en TCP desde RootlessKit 3.0, y siempre que desactives el userland-proxy de Docker; con implicit, que exige red pasta; o con pesto, todavía experimental y con una versión mínima de passt, la IP de origen sí llega. En Podman, además, hay dos ejes que no hay que confundir, el modo de red y el reenviador. pasta es el modo de red por defecto desde Podman 5.0, y el reenviador se cambia en containers.conf con la clave rootless_port_forwarder. Lo que yo tenía, en el momento de los hechos, era el comportamiento por defecto. Y el de por defecto no la conserva.

El parche de Cloudflare y la pregunta de quién te fías

Y aquí me tengo que parar y admitir algo incómodo. Mi analítica funcionaba bien. Durante mucho tiempo. Veía las visitas, veía los países, veía las referencias. Nada sospechoso. ¿Por qué? Porque delante de todo había Cloudflare. Y Cloudflare, cuando te pasa una petición, escribe la IP real del visitante en una cabecera, CF-Connecting-IP. La pone un proxy de confianza y viaja hasta el servicio. Umami la leía y registraba a la gente de verdad, aunque dentro del contenedor la IP se hubiera perdido mucho antes.

O sea, que el problema del rootless no estaba resuelto. Estaba tapado. Y tapado con una cabecera que solo es buena mientras quien la escribe sea Cloudflare. El día que eso cambie, el agujero queda a la vista de todos. Que es exactamente lo que pasó después.

Con el diagnóstico hecho, hay dos sitios donde se trata, y conviene tratar los dos. En el proxy, Traefik decide de qué proxies se fía. En la analítica, Umami decide qué cabecera lee. Y la regla que hay que tener grabada es esta. Una cabecera de cliente es un papel en blanco, la rellena el primero que pasa. Por eso hay que fiarse solo del salto que de verdad está delante, y hay que leer una cabecera que el proxy reescriba, no una que pueda escribir cualquiera que alcance el servicio.

En Traefik eso se hace en los entrypoints, con forwardedHeaders y con proxyProtocol. Y ojo con un error que se comete mucho por comodidad. No pongas el rango entero de la red de contenedores en trustedIPs «para no complicarte». Si lo haces, la lista de confianza se convierte en decorado, porque el par que ve Traefik suele ser una dirección interna de esa misma red, y si el rango la incluye, Traefik se fía siempre. La regla es que la lista de confianza lleva una dirección, no una red. /32 para IPv4 y /128 para IPv6, y solo el salto que controlas. Así queda el entrypoint,

entryPoints:
  websecure:
    address: ":8443"
    proxyProtocol:
      trustedIPs:
        - "127.0.0.1/32"
    forwardedHeaders:
      insecure: false
      trustedIPs:
        - "127.0.0.1/32"

El 127.0.0.1/32 no es un ejemplo de relleno. Es el montaje real, porque el borde reenvía a Traefik por loopback, y la dirección que Traefik ve llegar es esa. Si tu borde está en otra máquina, pones la dirección de ese borde y usas /32. Y hay un detalle de implementación que conviene saber, porque explica por qué no basta con «activar PROXY protocol» y ya. Si el par no está en trustedIPs, Traefik no rechaza la conexión, ignora el saludo y la deja pasar. Por eso el aviso de su propia documentación es tajante, hay que configurar el protocolo PROXY en los dos lados o te arriesgas a facilitar la falsificación de peticiones.

En Umami, la pieza es una variable de entorno. CLIENT_IP_HEADER le dice de qué cabecera saca la IP del visitante, y el código la usa sin comprobar quién la ha escrito, así que la elección importa,

CLIENT_IP_HEADER=x-real-ip

La diferencia, en una frase, es la que separa una cabecera que el proxy reescribe de una que cualquiera puede escribir. x-real-ip la reconstruye Traefik desde la conexión real, la borra si viene de un par desconocido y la reescribe con la dirección de verdad. cf-connecting-ip o x-forwarded-for las puede mandar el primero que alcance el servicio, y Umami, para más inri, se queda con el valor de más a la izquierda de la cadena, que es justo el que pone el cliente. Con el puerto de Traefik alcanzable solo desde el borde, la cabecera que el proxy controla no se puede falsear desde fuera. Y no, no hay ningún secreto en ese fichero, es una preferencia, no una credencial. Este es el segundo script del gist, el que valida el YAML y comprueba que la lista lleva solo la dirección del borde y que no hay ningún insecure activado.

El sábado por la tarde que dejó de funcionar

Con el parche de Cloudflare, esto funcionaba. Funcionaba de maravilla. Hasta un sábado por la tarde. Y no fue que fuera lento, no. Dejó de funcionar. Me pasé un par de horas buscando qué se había roto, convencido de que sería el rootless, o Traefik, o alguna de mis chapuzas. Hasta que me di cuenta de que no era nada de eso. Era un bloqueo por rangos enteros de direcciones IP.

El origen de todo esto es la sentencia 310/2024, de 18 de diciembre de 2024, del Juzgado de lo Mercantil número 6 de Barcelona. Esa resolución autoriza un mecanismo dinámico por el que LaLiga y Telefónica Audiovisual Digital pueden ir comunicando a los operadores direcciones IP nuevas que sustituyan a las identificadas inicialmente, y los operadores demandados, entre ellos Movistar, MásOrange, Vodafone y DIGI, las ejecutan durante los partidos. El bloqueo recae sobre direcciones IP, no sobre dominios. Y los bloqueos no los ejecuta quien pide la medida, los ejecutan los operadores en cumplimiento de la resolución. En el procedimiento originario las demandadas se allanaron íntegramente, así que el juzgado resolvió sin más trámites y no hubo contradicción técnica. Hay un antecedente que cambia el relato y que merece la pena conocer, porque en 2023, en un procedimiento sobre otra plataforma, varios operadores sí se opusieron al bloqueo por IP por riesgo de sobrebloqueo y el juzgado limitó el bloqueo a servidores dedicados en exclusiva a aquello, recurriendo al DNS para el resto. Las cosas salen distintas cuando hay contradicción técnica. Pero eso es otra historia.

Lo que pone números a la factura es el informe de OONI del 30 de junio de 2026 sobre el daño colateral de este bloqueo. Y las cifras son de las que se quedan grabadas, 7.441 direcciones IP bloqueadas en algún momento, repartidas entre 36 proveedores de infraestructura y alojamiento, y más de 554.000 dominios afectados de los 9,2 millones que probaron. De esos dominios, el 90,4 % estaban servidos por Cloudflare, desde solo 2.218 direcciones. Hay casos que retratan el absurdo mejor que cualquier gráfico, como el de un proveedor con 18.592 dominios cayendo por culpa de una sola dirección. Y el dato que a mí me dejó helado, en ventanas de una hora, bloquear solo entre cuatro y veinte direcciones tumbaba más de 400.000 dominios a la vez. En una hora concreta llegaron a medirse cerca de 478.000 dominios caídos de golpe.

¿Y por qué bloquear una IP arrastra cientos de miles de dominios? Pues porque ahí delante hay proxies inversos. Miles de dominios comparten un puñado de direcciones, y se distinguen entre ellos solo por el SNI del TLS o por la cabecera Host. O sea, que cuando alguien bloquea una dirección de Cloudflare no está señalando a un sitio, está señalando a un vector compartido. El bloqueo por IP es, en la práctica, un bloqueo por vecindario, y en un mundo donde las IPv4 están agotadas y el alojamiento compartido es la norma, eso significa que caen también organizaciones que no tienen nada que ver, desde medios y ONG hasta universidades y servicios de mensajería. El propio informe lo documenta con nombres y apellidos. Vercel, que se vio afectada, lo denunció como un bloqueo indiscriminado que ignora el SNI. Y el daño colateral no es un efecto imprevisto que nadie hubiera visto venir. LaLiga, en una nota informativa de febrero de 2025, reconoció «la disfunción de diversas webs en todo el territorio nacional» y la atribuyó a que Cloudflare comparte infraestructura. Eso no es una admisión de responsabilidad jurídica, pero acredita que el efecto se conocía.

Y lo que falta es la parte que a un servidor le duele de verdad. No existe un listado oficial público y completo de las direcciones bloqueadas por jornada, ni un registro de quién decide cada inclusión, ni un aviso a los servicios afectados. Yo me enteré porque el sitio dejó de responder, no porque nadie me hubiera avisado.

Sobre esto quiero ser claro, porque es donde es fácil resbalar. Aquí no se discute la legitimidad de perseguir la piratería, ni entra fútbol, ni entra política. Lo que se discute es el medio técnico. Bloquear direcciones compartidas produce daño a terceros que no son parte, y no hay ni lista pública, ni aviso, ni cauce de corrección. Eso es un problema de proporcionalidad y de transparencia, no de aficiones.

Qué puedes hacer tú si te pasa (y qué no)

Si un sitio legítimo te falla de un día para otro, hay tres cosas que puedes hacer, y conviene hacerlas en este orden. Primero guarda la prueba con fecha. Después mide. Y después denuncia.

La prueba es más sencilla de lo que parece. Se trata de guardar el dominio, tu operador, la hora exacta y el error tal cual, y de anotar si la dirección que hay detrás es de un rango compartido. Porque ese es el paso que convierte «mi web está caída» en «está caído un vector compartido», y cambia por completo a quién le estás reclamando,

# ¿responde? ¿con qué error?
curl -v --max-time 15 https://tudominio.example

# ¿resuelve? ¿a qué dirección y con qué servidor?
dig tudominio.example +short
dig tudominio.example | grep -i status     # busca EDE: 15 (Blocked) si tu resolutor lo expone

# ¿de quién es la dirección? (sin instalar whois, RDAP por HTTP)
curl -sSL "https://rdap.org/ip/<direccion>"

Los tres datos que hay que anotar siempre son el dominio, el operador y la fecha y hora, con el mensaje de error literal. Y si puedes repetir la prueba desde otra conexión, otro operador o una VPN, conviertes una queja en evidencia. Guarda los originales, no pantallazos reenviados. El tercer script del gist automatiza exactamente esto y guarda el informe con la fecha en el nombre del fichero, usando RDAP cuando no hay whois, que da la misma información sin instalar nada. Y funciona de verdad, porque en una de mis pruebas el resolutor de la red devolvió un EDE: 15 (Blocked), o sea, un bloqueo por DNS documentado con fecha.

El segundo paso es medir con OONI Probe, que es libre, no pide registro y publica tus mediciones en OONI Explorer. Y hay que decir la verdad sobre lo que hace. Una anomalía sola no prueba un bloqueo ni identifica al responsable, pero suma, y engorda el conjunto de datos que hizo posible el informe del que hablábamos antes. Medir no es ganar el caso, es dejar constancia.

Y el tercer paso es denunciar. Primero al operador, y pidiendo respuesta por escrito, que es la vía contractual y la que después habilita escalar. Y luego por los canales que existen, la Oficina de Atención al Usuario de Telecomunicaciones para averías e interrupciones del servicio, la Agencia Española de Protección de Datos solo si hay un ángulo de protección de datos, por ejemplo una inspección de comunicaciones, y la CNMC como coordinador de servicios digitales. Con un aviso que conviene tener claro antes de perder el tiempo, el informe anual de neutralidad de red de la Secretaría de Estado se inhibe en estos casos y los considera medidas de gestión del tráfico en ejecución de sentencia.

Y aquí viene la parte que no me interesa disimular. No hay un caso documentado de una reclamación individual que haya levantado uno de estos bloqueos. Lo que hacen las reclamaciones es dejar constancia y engordar las estadísticas, que es útil, pero no es ganar. Las correcciones que se conocen, como la de Vercel, fueron internas de quien bloqueaba, no el resultado de una reclamación administrativa. Así que te lo digo tal cual. Esto es cómo se denuncia, no cómo se gana. Y decírtelo así es lo que hace creíble todo lo demás.

El borde lo pongo yo, HAProxy al 443 y Traefik al 8443

Vale, y ahora vuelvo a mi servidor, que es lo que sí puedo arreglar. Si Cloudflare no puede estar delante, porque resulta que su rango entero se cae cuando hay fútbol, entonces el borde lo pongo yo. Y el borde significa HAProxy. Pero fíjate bien en dónde, en el host, en el metal, como servicio del sistema. No en un contenedor. El borde es lo primero que toca el visitante, y ahí quiero un proceso que controle yo, con la configuración a la vista y fuera del espacio de nombres de nadie.

HAProxy recibe la conexión de frente y, en lugar de terminar él el cifrado, la pasa al siguiente salto avisándole de quién viene, en modo TCP y con el protocolo PROXY. Con eso no tengo certificados duplicados ni doble descifrado, y el siguiente salto sabe exactamente qué dirección llamó. El recorrido del visitante queda así, el visitante entra por el 443, HAProxy lo recibe, le añade el saludo del protocolo PROXY con la dirección de origen y lo pasa al 8443, donde Traefik lee ese saludo de un proxy del que se fía y ya sabe quién llamó.

Aquí está el detalle que hace que todo encaje con el rootless, y es que HAProxy escucha en el 443. El puerto privilegiado, el de verdad, lo toma el host. En rootless, un proceso sin privilegios no puede abrir puertos por debajo del 1024, y el kernel se lo impide porque hace falta la capacidad de enlazar puertos privilegiados. Por eso un Traefik en rootless no puede quedarse el 443 por mucho que se lo pidas. Pero si el 443 lo abre HAProxy en el host, el problema desaparece, porque el contenedor no pelea por un puerto que nunca pidió. Así queda la configuración del borde,

global
    maxconn 4096
    log stdout format raw local0

defaults
    mode tcp
    log global
    option tcplog
    timeout connect 5s
    # Passthrough de TLS: puede haber sesiones mudas mucho rato (WebSocket, descargas).
    timeout client  1h
    timeout server  1h

frontend borde_https
    bind *:443
    default_backend traefik

backend traefik
    mode tcp
    server traefik1 127.0.0.1:8443 send-proxy-v2

Y estas son las cuatro líneas que sostienen todo el diseño. bind *:443 es el puerto privilegiado, que abre el host y no el contenedor. mode tcp es lo que hace que HAProxy no descifre nada, solo atraviese la conexión. send-proxy-v2 es lo que avisa al siguiente salto de quién llama. Y 127.0.0.1:8443 es Traefik, en rootless, en un puerto alto. Dos detalles de esta configuración que no son casuales. Los timeout de cliente y servidor van a una hora, y no a los 50 segundos de la plantilla clásica, porque en modo TCP la conexión puede quedarse callada mucho rato, con un WebSocket o una descarga larga, y un timeout corto corta conexiones legítimas. Y Traefik publica su puerto alto en loopback, para que solo el borde pueda alcanzarlo y no el mundo. La validación de sintaxis es haproxy -c -f config/haproxy.cfg, y el cuarto script del gist la ejecuta de verdad, rompiendo a propósito una copia para comprobar que el chequeo rechaza lo que tiene que rechazar.

En el servidor, HAProxy corre como servicio de systemd, y aquí lo que hay que copiar es la unit oficial del proyecto, que arranca con -Ws, el modo maestro-trabajador con soporte de notify para systemd, permite recargar con SIGUSR2 y trae, comentadas, las opciones de sandboxing para endurecerlo sin tocar el binario.

Y esto es lo que más me ha convencido del diseño. No he necesitado ninguno de los apaños habituales para el rootless 🔥 No he tocado el net.ipv4.ip_unprivileged_port_start que habría que bajar para que un contenedor pudiera abrir el 443. No le he dado la capacidad CAP_NET_BIND_SERVICE al binario del reenviador, que es lo que pasta desaconseja por el riesgo de convertir esa capacidad en un vector de ataque. No he montado reglas de firewall para redirigir el 443 a un puerto alto. Esos tres remedios existen y son legítimos, pero sirven para que el contenedor pueda abrir el puerto bajo. Aquí el contenedor no lo abre, lo abre el host. Cada cosa en la capa que le toca. Y el resultado es que la IP real vuelve a aparecer en los logs y en la analítica, sin depender de que una empresa de terceros esté delante.

Y el borde, además, es el sitio donde puedo poner reglas, dejar pasar o no determinado tráfico y variar direcciones antes de que lleguen a Traefik. Eso lo estoy trabajando con Shuul, una herramienta propia que puede funcionar como WAF o como fail2ban, y es la vía por la que voy a seguir. Cuando la tenga lista te la contaré con más detalle, porque ahora mismo es eso, algo que estoy preparando.

¿Y por qué no monto Traefik directamente en el metal?

Buena pregunta, y me la hice yo antes de decidir. Si el borde ya está en el host, ¿por qué Traefik sigue dentro de un contenedor? La respuesta es el aislamiento de redes. Montar Traefik como rootless, en su contenedor, me permite segmentar completamente las redes que cuelgan de él. Si lo pusiera en modo host, perdería esa segmentación. Y la segmentación es exactamente lo que quiero.

En mi montaje, la parte que se enfrenta a internet, la que está conectada al 8443, vive en una red. Y todo lo que hay detrás, la base de datos, la caché, vive en otra red completamente aislada, cuyo único punto de contacto es el servidor web, que por necesidad habla con las dos. Traefik ve lo que hay en el frente, pero no ve el fondo, y HAProxy tampoco. De manera que si alguien escalara privilegios y lograra escapar del frente, no tendría ruta hasta la base de datos ni hasta la caché. Y esto hay que decirlo sin venderlo como una bala de plata. La segmentación no sustituye al parcheo, no arregla una caché mal dimensionada y no protege de un bloqueo de red externo. Es defensa en profundidad, una puerta más en el camino. Pero es una puerta que antes no tenía.

Lo que me llevo

Cierro. Todo esto empezó porque un servidor se caía y yo decidí que la culpa era de mi proveedor sin medir una sola cosa. Terminó con una sentencia tumbando medio internet un sábado por la tarde y conmigo montando el borde a mano. Y la lección no es «usa Podman» ni «huye de Cloudflare». La lección es que casi nunca falla lo que crees. Falla una capa que no mediste.

Medir antes de culpar. Medir antes de mudarte. Medir antes de cambiar de herramienta, porque el problema puede venir del modelo y no de la marca. Y si un sitio legítimo te falla de un día para otro, prueba con fecha, mide y denuncia. Aunque no te levanten el bloqueo, quedará constancia de que te pasó. Que es poco, ya lo sé, pero es más que el silencio.

Y si todo esto te ha pasado de cerca, tengo curiosidad por tu caso. ¿Tienes rootless en producción? ¿Te ha tumbado algo un bloqueo de estos? Cuéntamelo, porque de eso salen los mejores episodios. Y gracias a Víctor, por su nombre y por su web, que sin su aviso yo seguiría creyendo que mi analítica era perfecta. Nos oímos en el próximo episodio.


Más información

Deja una respuesta