778 - ¡Adiós Docker! Cómo configurar Traefik con Podman (Rootless y Seguro)

778 - ¡Adiós Docker! Cómo configurar Traefik con Podman (Rootless y Seguro)

Aprende a configurar Traefik con Podman usando Quadlets. Guía completa sobre puertos rootless, persistencia en Systemd, seguridad avanzada y HTTP/3.

1:25
-3:15

Como configurar Traefik en Podman, ha sido probablemente una de las preguntas mas recurrentes desde que empecé con esta serie de episodios dedicados a Podman. Lo cierto, es que, en ningún momento me habría planteado realizar un migración tan radical como esta sin saber que mi proxy de cabecera iba a tener soporte. Pero si, lo tiene, y es tan sencillo como te podrías haber imaginado. Simplemente se trata de que el socket de Podman esté en funcionamiento y algunos detalles mas que he ido desgranando a lo largo de estos episodios. Así, el objetivo de este episodio, es básicamente dejar funcionando Traefik con Podman, para que puedas utilizarlo como tu proxy de cabecera, y así poder gestionar tus servicios de una manera sencilla y eficiente. Por supuesto como un Quadlet cualquiera.

¡Adiós Docker! Cómo configurar Traefik con Podman (Rootless y Seguro)

Preliminares

Los puertos 80 y 443

Antes de meternos con Traefik, hay que tener en cuenta que estoy ejecutando Podman en modo rootless, es decir, sin privilegios de superusuario. Esto es algo que me gusta mucho, porque me permite ejecutar mis contenedores sin necesidad de tener permisos de administrador, lo que aumenta la seguridad y la flexibilidad. Sin embargo, esto también implica que el socket de Podman no está disponible para todos los usuarios, sino que está limitado al usuario que lo ejecuta. Por lo tanto, para que Traefik pueda comunicarse con Podman, es necesario configurar el socket de manera adecuada.

Por otro lado, al no ser un usuario con privilegios, directamente no puedo utilizar el puerto 80 ni el 443. Es necesario realizar cambios. Por esta razón la primera operación que tienes que hacer para poder utilizar estos dos puestos es ejecutando la siguiente instrucción,

echo "net.ipv4.ip_unprivileged_port_start=80" | sudo tee /etc/sysctl.d/rootless-ports.conf
sudo sysctl -p /etc/sysctl.d/rootless-ports.conf

Persistencia de usuario

Por defecto, Systemd mata los procesos de un usuario cuando este cierra la sessión. Para que tus contenedores sigan corriendo y a se inicien automáticamente al arrancar el sistema, es necesario configurar Systemd para que no mate los procesos de tu usuario. Para ello, puedes ejecutar la siguiente instrucción,

loginctl enable-linger $USER

El socket de Podman

Para utilizar el socket de Podman en modo rootless (como usuario normal), el proceso es similar al de los Quadlets, pero enfocado en el servicio de API de Podman. Para activar el socket de usuario,

systemctl --user enable --now podman.socket

Indicarte que el socket de Podman no se encuentra en /, si no que se encuentra normalmente en /run/user/${UID}/podman/podman.sock. De cualquier forma, puedes ver la ubicación exacta del socket ejecutando el siguiente comando,

podman info | grep socket

Existen determinadas herramientas que tiran del socket de Docker. Para que esas herramientas sepan donde buscar, simplemente tienes que configurar la variable DOCKER_HOST con la ruta del socket de Podman. Para ello, puedes ejecutar la siguiente instrucción,

export DOCKER_HOST=unix://${XDG_RUNTIME_DIR}/podman/podman.sock

En el caso del quadlet como veremos a continuación, la forma de hacerlo es la siguiente,

[Container]
Image=docker.io/library/traefik:latest
# Mapeas el socket del host al interior del contenedor
Volume=%t/podman/podman.sock:/var/run/docker.sock:Z
  • %t. Es un especificador de Quadlet que apunta automáticamente al «Runtime directory» del usuario (/run/user/$UID).
  • :Z:. Es vital en sistemas como Fedora o RHEL para que SELinux permita el acceso al socket.

Los archivos de Traefik

Por la parte de Podman

Por la parte de podman, he definido 4 Quadlets,

  • traefik.container. Define el contenedor de Traefik, con la imagen que vamos a utilizar, el mapeo del socket de Podman, y la configuración de los puertos 80 y 443.
  • traefik-certs.volume. Define un volumen para almacenar los certificados de Traefik, que se mapea al interior del contenedor.
  • traefik-plugins.volume. Define un volumen para almacenar los plugins de Traefik, que se mapea al interior del contenedor.
  • proxy.network. Define una red personalizada para que los contenedores puedan comunicarse entre sí. Esta red la utilizaremos en el resto de contenedores que necesiten ser accesisbles desde el mundo exterior.

Del traefik.container, que está disponible en GitHub, quiero destacar algunos puntos que me parecen realmente importantes.

  • Los puertos. He declarado el 443/udp además del tcp y del 80. Esto es para poder habilitar HTTP/3, que es el protocolo mas moderno y eficiente para la comunicación entre el cliente y el servidor. HTTP/3 utiliza QUIC como protocolo de transporte, que a su vez utiliza UDP en lugar de TCP. Por lo tanto, para habilitar HTTP/3 en Traefik, es necesario exponer el puerto 443 tanto para TCP como para UDP.
  • El socket, como indiqué anteriormente Volume=%t/podman/podman.sock:/var/run/docker.sock:ro

En este caso, como HTTP/3 usa UDP de forma intensiva, el kernel de Linux puede ser un cuello de botella con los tamaños de búfer por defecto. Para que evitarlo, crea el archivo /etc/sysctl.d/99-traefik-http3.conf, con el siguiente contenido,

net.core.rmem_max=2500000
net.core.wmem_max=2500000

Aplica los cambios con sudo sysctl --system

Y por otro lado están los archivos de configuración estática y dinámica de Traefik. Realmente, estos los podría haber metido en un volumen, pero como es algo que habitualmente modifico, lo he dejado disponible en el host para modificarlos cuando me rote.

Aquí me planteé donde guardarlos, y en seguida caí en la cuenta que, al fin y al cabo, son archivos de configuración, dotfiles, con lo que su sitio ideal es en ~/.config/traefik. Y lo mismo para el resto de contenedores que necesiten tener una configuración personalizada, lo ideal es que esa configuración esté disponible en el host para poder modificarla de forma sencilla, y luego mapearla al interior del contenedor.

El Quadlet

Para levantar Traefik en Podman, he creado un Quadlet ~/.config/containers/systemd/traefik.container con el siguiente contenido,

[Unit]
Description=Traefik Edge Router (Rootless)
After=network-online.target

[Container]
Image=docker.io/library/traefik:latest
ContainerName=traefik

# Puertos (Recuerda haber bajado el net.ipv4.ip_unprivileged_port_start a 80)
PublishPort=80:80
PublishPort=443:443/tcp
PublishPort=443:443/udp

# 1. Archivo de configuración estática
Volume=%h/.config/traefik/traefik.yml:/etc/traefik/traefik.yml:ro,Z
# 2. Volumen de Podman para certificados (Gestionado por Podman, no bind-mount)
Volume=traefik-certs.volume:/etc/traefik/certs:Z
# 3. Socket de Podman del usuario para el discovery
Volume=%t/podman/podman.sock:/var/run/docker.sock:ro
# 4. Directorio dinámico opcional
Volume=%h/.config/traefik/dynamic:/etc/traefik/dynamic:ro,Z
# 5. Persistencia de Plugins
Volume=traefik-plugins.volume:/plugins-storage:rw,Z

Network=proxy.network

# Definición del Healthcheck
HealthCmd=traefik healthcheck --ping --ping.address=:8082
HealthInterval=30s
HealthRetries=3
HealthTimeout=5s
HealthStartPeriod=10s

# Hardening
# 1. No permitir escalada de privilegios y limitamos recursos.
PodmanArgs=--security-opt=no-new-privileges
# 2. Hacer el sistema de archivos del contenedor de solo lectura
# (Traefik solo necesita escribir en /etc/traefik/certs y /plugins-storage que son volúmenes)
ReadOnly=true
# 3. Eliminar capacidades de root innecesarias
# Traefik solo necesita NET_BIND_SERVICE para los puertos 80/443
DropCapability=all
AddCapability=cap_net_bind_service

[Service]
Environment=SOPS_AGE_KEY_FILE=%h/.secrets/sops/age/key.txt
Environment=PATH=/usr/local/bin:/usr/bin:%h/.local/bin
Restart=always
# 4. Evita ataques DoS que consuman RAM/CPU
MemoryLimit=512M
CPUWeight=50

[Install]
WantedBy=default.target

De este container quiero destacarte algunos aspectos, que como te decía anteriormente son realmente interesantes.

  • HTTP/3. Para poder utilizar HTTP/3 he mapeado el puerto 443 tanto TCP como UDP, dado que se utiliza el protocolo QUIC que está basado en UDP. Ahora solo falta que no me olvide de abrir en el firewall ambos puertos, porque si no habré hecho un pan como una torta.
  • Rootless con privilegios mínimos. Como ya sabes, estoy ejecutando Traefik con privilegios mínimos pero además con DropCapability=all y AddCapability=cap_net_bind_service, lo que significa que el contenedor no tiene capacidades de root innecesarias, pero sí tiene la capacidad necesaria para enlazar los puertos 80 y 443, que he agregado explícitamente. Por otro lado con no-new-privileges me aseguro de que el contenedor no pueda escalar privilegios, lo que añade una capa adicional de seguridad. Pero además con ReadOnly=true hago que el sistema de archivos del contenedor sea de solo lectura, lo que limita aún más las posibilidades de un atacante en caso de que logre comprometer el contenedor. Esto es especialmente importante para un proxy de borde como Traefik, que está expuesto a Internet y puede ser un objetivo atractivo para los atacantes.
  • Gestión de Volúmenes. He definido volúmenes específicos para los certificados y los plugins de Traefik, lo que permite una gestión más organizada y segura de estos datos. Además, el socket de Podman se mapea como solo lectura, lo que garantiza que Traefik pueda comunicarse con Podman sin riesgo de modificar el socket. La idea e tener los plugins en un volumen es para evitar tener que descargarlos cada vez que inicio el contenedor
  • Comunicación vía socket del usuario. %s se expande a la ruta del Runtime Directory del usuario, normalmente a /run/user/1000. Al montar %t/podman/podman.sock, Traefik puede escuchar cuando se levanta otro Quadlet y crear las rutas automáticas, tal y como hace Docker, pero de forma completamente aislada dentro de la sesión de usuario.
  • Healthcheck. He definido un healthcheck para Traefik utilizando el comando traefik healthcheck --ping --ping.address=:8082, lo que permite monitorear la salud del contenedor y reiniciarlo automáticamente si no responde correctamente. No solo esto, si no que tal y como expliqué en un episodio anterior, esto me permite que cuando se actualice, si la cosa no va bien se realice un rollback automático a la versión anterior, lo que es una gran ventaja en términos de estabilidad y confiabilidad.
  • Control de recursos. He definido límites de memoria y peso de CPU para evitar que un ataque de denegación de servicio (DoS) pueda consumir todos los recursos del sistema. Esto es especialmente importante para un proxy de borde como Traefik, que está expuesto a Internet y puede ser un objetivo atractivo para los atacantes.
    _ Gestión de secretos. He configurado variables de entorno para la gestión de secretos con SOPS, lo que permite una integración segura y eficiente de secretos en Traefik.

Configuración estática

Traefik permite tener configuración estática y dinámica. La estática es la que se carga al iniciar el contenedor, y la dinámica es la que se puede modificar en caliente sin necesidad de reiniciar el contenedor. En este caso, la configuración estática la he dejado en ~/.config/traefik/traefik.yml, con el siguiente contenido,

# Configuración Estática
# Desactivar protocolos TLS antiguos (Solo TLS 1.2 y 1.3)
tls:
  options:
    default:
      minVersion: VersionTLS12
      cipherSuites:
        - TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
        - TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
        - TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
        - TLS_AES_128_GCM_SHA256
        - TLS_AES_256_GCM_SHA384
        - TLS_CHACHA20_POLY1305_SHA256
api:
  dashboard: true
  insecure: false
ping:
  entryPoint: healthcheck

entryPoints:
  web:
    address: ":80"
    http:
      redirections:
        entryPoint:
          to: websecure
          scheme: https
          permanent: true

  websecure:
    address: ":443"
    http3:
      advertisedPort: 443
    # Optimización: Mantener conexiones abiertas para mejorar performance
    transport:
      respondingTimeouts:
        readTimeout: 60s
        writeTimeout: 60s
        idleTimeout: 180s

  healthcheck:
    address: ":8082"

providers:
  docker:
    endpoint: "unix:///var/run/docker.sock"
    exposedByDefault: false
  file:
    directory: /etc/traefik/dynamic
    watch: true

certificatesResolvers:
  myresolver:
    acme:
      email: tu-email@atareao.es
      storage: /etc/traefik/certs/acme.json
      tlsChallenge: {}

log:
  level: INFO
  format: common

accessLog:
  # Al no poner filePath, sale por stdout/stderr
  format: json
  bufferingSize: 100
  fields:
    names:
      StartLocal: keep # Útil para tener la hora local del VPS

experimental:
  plugins:
    traefik-oidc-auth:
      moduleName: "github.com/sevensolutions/traefik-oidc-auth"
      version: "v0.11.0"

De esta configuración estática quiero destacar algunos puntos,

  • tls. He indicado una versión mínima de TLS. Esto elimina vectores de ataque antiguos y ataques tipo Man-in-the-Middle que podrían aprovechar vulnerabilidades en versiones anteriores de TLS. Además, he especificado un conjunto de cipher suites modernas y seguras, lo que garantiza que las conexiones cifradas sean robustas y resistentes a ataques conocidos. Además está incluido TLS_CHACHA20_POLY1305_SHA256 que está especialmente pensado en dispositivos móviles.
  • api. El dashboard de Traefik se ha convertido en una pieza indispensable para mi a la hora de descubrir que está pasando en mi sistema, y para diagnosticar problemas. Por eso lo tengo habilitado, pero con insecure: false, lo que significa que solo se puede acceder a él a través de HTTPS, y no a través de HTTP.
  • ping. Es un punto de entrada que se utiliza para realizar comprobaciones de salud (health checks) en Traefik. Esto es especialmente útil para asegurarse de que Traefik está funcionando correctamente y para integrarlo con sistemas de monitoreo o balanceadores de carga.
  • respondingTimeouts. Esta configuración es una optimización para mantener las conexiones abiertas durante más tiempo, lo que puede mejorar el rendimiento en situaciones de alta carga o cuando se utilizan protocolos como HTTP/3 que pueden beneficiarse de conexiones persistentes.

Configuración dinámica

Para la configuración dinámica, he creado el directorio ~/.config/traefik/dynamic, que se mapea al interior del contenedor en /etc/traefik/dynamic. En este directorio coloco archivos de configuración, que se cargan en caliente sin necesidad de reiniciar el contenedor. Actualmente tengo dos, uno en el que se encuentran dos middleware, para encabezados de seguridad y compresión, que los utilizo a discreción y según necesidad.

http:
  middlewares:
    compresionHeaders:
      compress:
        excludedContentTypes:
          - text/event-stream
          - application/x-xz
    seguridadHeaders:
      headers:
        # HSTS (Fuerza HTTPS durante un año)
        stsSeconds: 31536000
        stsIncludeSubdomains: true
        stsPreload: true
        # Evitar Sniffing de contenido
        contentTypeNosniff: true
        # Evitar Clickjacking (Nadie puede meter tu web en un iframe)
        frameDeny: true
        # Filtro XSS básico
        browserXssFilter: true
        # Referrer Policy
        referrerPolicy: "same-origin"

Y un segundo archivo en el que defino otro middleware para la autenticación con OIDC, que utiliza el plugin traefik-oidc-auth que he incluido en la configuración estática. Este plugin me permite proteger mis servicios con autenticación de OpenID Connect, lo que añade una capa adicional de seguridad a mis aplicaciones expuestas a Internet.

En este caso la configuración tiene algunas variables en formato jinja. Esto es así porque me permite tenerlo expuesto directamente en GitHub para que cualquiera lo pueda utilizar, y que se construye cuando sincronizo el repositorio. De todas formas, esto te lo explicaré con detalle en un siguiente episodio para contarte exactamente como lo estoy haciendo.

http:
  middlewares:
    oidc-auth:
      plugin:
       traefik-oidc-auth:
         Secret: {{ env.OIDC_SECRET }}
         Provider:
           ClientId: {{ env.OIDC_CLIENT_ID }}
           ClientSecret: {{ env.OIDC_CLIENT_SECRET }}
           Url: https://auth.{{ env.FQDN }}
           TokenValidation: IdToken
         Scopes:
           - openid
           - profile
           - email


  routers:
    dashboard:
      rule: Host(`traefik.{{ env.FQDN }}`) # El subdominio para tu panel
      service: api@internal
      entryPoints:
        - websecure
      tls:
        certResolver: myresolver
      middlewares:
        - oidc-auth
        - compresionHeaders
        - seguridadHeaders

Conclusión

Como ves, salgo el detalle de habilitar el socket de Podman y configurar los puertos, el resto es prácticamente igual a lo que haría con Docker. Esto es algo que me parece realmente interesante, porque significa que puedo migrar mis servicios de Docker a Podman sin tener que preocuparme por la compatibilidad con Traefik, lo que me da una gran flexibilidad y libertad a la hora de elegir las herramientas que quiero utilizar en mi stack de autoalojamiento.

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *