diff --git a/content/posts/notes_general/notes_traefikv3.md b/content/posts/notes_general/notes_traefikv3.md new file mode 100644 index 0000000..f2d3ce4 --- /dev/null +++ b/content/posts/notes_general/notes_traefikv3.md @@ -0,0 +1,1199 @@ +--- +weight: 4 +title: "Apuntes de Traefik v3" +date: 2024-11-08T20:17:21+0100 +draft: false +summary: "Apuntes de Traefik v3" +categories: + - notes +tags: + - selfhosted + - vps + - docker + - traefik +--- + +Apuntes sobre Traefik v3. + + + +{{< admonition type=info title="Artículo Actualizado" open=true >}} + +**ESTE ARTÍCULO YA ES VÁLIDO** + +Este artículo es una actualización del [post inicial]({{% ref "/posts/notes_general/notes_traefik" %}}) redactado para Traefik V2.0. + +Para que las configuraciones propuestas funcionen en la nueva versión 3 de Traefik en realidad no ha sido necesario cambiar nada. La actualización se ha limitado a eliminar las referecias `version` en los ficheros _docker-compose_ y a renombrar las redes internas de **Docker**. Estas redes se han renombrado a `frontnet`y `backnet` para evitar ambigüedades. + +{{< /admonition >}} + + +# Traefik + +{{< admonition type=abstract title="Referencias" state=open >}} + +Estas notas sobre **Traefik** son un refrito de las siguientes fuentes: +- [Traefik - Quick Start](https://doc.traefik.io/traefik/getting-started/quick-start/) +- [Joshua Avalon Blog: Setup Traefik v2](https://joshuaavalon.io/setup-traefik-v2-step-by-step) +- [Digital Ocean: Howto use Traefik as reverse proxy for Docker](https://www.digitalocean.com/community/tutorials/how-to-use-traefik-v2-as-a-reverse-proxy-for-docker-containers-on-ubuntu-20-04) +- [Hash with openssl](https://blog.roberthallam.org/2020/05/generating-a-traefik-nginx-password-hash-without-htpasswd/) +- [Containeroo: Traefik 2.0 + Docker — a Simple Step by Step Guide](https://medium.com/@containeroo/traefik-2-0-docker-a-simple-step-by-step-guide-e0be0c17cfa5) +- [Containeroo: Traefik 2.0 + Docker — an Advanced Guide](https://medium.com/@containeroo/traefik-2-0-docker-an-advanced-guide-d098b9e9be96) +- [Containeroo: Traefik 2.2 + Docker: Entry Point Configuration](https://medium.com/@containeroo/traefik-2-2-docker-global-entrypoint-configuration-ff11d7f84913) + +{{< /admonition >}} + + + + +## _Edge Router_ + +**Traefik** es un _edge router_ (router de borde, router frontera, proxy +inverso) Es decir que es el punto de entrada a tu servidor (o +plataforma) y se encarga de interceptar cada petición de entrada y de +enrutarlas a los servicios correspondientes. Puede hacer el +enrutamiento basándose en muchos criterios diferentes (nombre del +host, cabeceras, etc.) + +{{< admonition type=tip title="¿Para qué sirve?" state=open >}} +Lo normal es que instales **Traefik** entre tu platafoma de servicios e +Internet. De esta forma no tienes que abrir mas que uno o dos puertos +hacia la red (tipicamente el 80 y el 443) y se podrá atender todas las +peticiones a los distintos servicios implementados en nuestra plataforma a través de sólo estos dos puertos de entrada. + +En este artículo **Plataforma de servicios** equivale a un VPS, a una raspi en tu casa, a +un servidor dedicado, etc. etc. + +{{< /admonition >}} + +## _auto-discovery_ de servicios + +Normalmente los _edge_router_ o proxy inversos necesitan un fichero de +configuración detallando las reglas para enrutar las peticiones a los +servicios. **Traefik** obtiene las reglas de los propios servicios. Así +que en el momento de desplegar un nuevo servicio, un contenedor +_Docker_ en lo que respecta a este documento, especificaremos la +información que ese servicio debe dar a **Traefik** para establecer los +enrutamientos. + +De esta forma cuando se despliega un servicio, **Traefik** lo detecta y +actualiza las reglas en tiempo real. De la misma forma, si el servicio +se cae o se para, **Traefik** elimina las rutas asociadas en tiempo +real. + +Para descubrir los servicios **Traefik** usa el API de la plataforma que +los alberga, en el lenguaje propio de **Traefik** usa un ___Provider___. +Como nosotros vamos a usar **Traefik** en _Docker_ vamos a centrarnos en +el _docker provider_. + +## Ejemplo 1: Uno facilito + +El siguiente fichero _docker-compose_ contiene una configuración lo +más sencilla posible con el __docker provider__. + +Preparamos los ficheros y directorios de trabajo: + +```bash +mkdir -p ~/work/docker/ejemplo_01/traefik +touch ~/work/docker/ejemplo_01/traefik/docker-compose.yml +``` + +El contenido del fichero `~/work/docker/ejemplo01/traefik/docker-compose.yml` +sería: + +```yml +services: + reverse-proxy: + # The official v3 Traefik docker image + image: traefik:v3.0 + # Enables the web UI and tells Traefik to listen to docker + command: --api.insecure=true --providers.docker + ports: + # The HTTP port + - "80:80" + # The Web UI (enabled by --api.insecure=true) + - "8080:8080" + volumes: + # So that Traefik can listen to the Docker events + - /var/run/docker.sock:/var/run/docker.sock +``` + +Este fichero _docker-compose_: +- Define el servicio _reverse-proxy_ + - Basado en la imagen `traefik:v3.0` En realidad en el momento de escribir esto deberíamos usar `traefik:v3.2.0` + - pasamos las opciones `--api.insecure=true` y `--providers.docker` al comando de arranque de la imagen **Traefik** + - Conectamos los puertos 80 y 8080 del contenedor al exterior (a la red host) + - Definimos un _bind mount_ para que el socket de comunicación con _Docker_ esté disponible para el contenedor. Lo necesita para + que **Traefik** pueda conectar el _provider docker_ al API de _Docker_ en la máquina _host_ + +{{< admonition type=danger title="API Insecure" open=true >}} + +¡Cuidado! la opción `api.insecure` __no debe__ usarse nunca en producción. Con esta opción estamos dejando el acceso al API de nuestro **Traefik** abierto y sin protección. + +Más adelante veremos opciones para configurar de forma segura el acceso al API. + +{{< /admonition >}} + +En cuanto lanzemos nuestro _docker-compose_: + +`docker compose --verbose up -d` + +Podremos acceder al _dashboard_ de **Traefik** visitando , donde `my_server_ip` será la dirección del server donde has lanzado el contenedor o `localhost` si estás haciendo las pruebas en tu ordenador personal. + +{{< admonition type=tip title="firewall" open=true >}} + +Si estás lanzando el contenedor en un servidor moderno (Debian 12 Server en nuestro caso) ya no usas **iptables** sino **nftables** para implementar el _firewall_. + +**Docker** sigue usando comandos **iptables** para abrir los puertos y establecer las reglas **nat** necesarias para los contenedores arrancados. Esos comandos se traducen a comandos **nftables** con herramientas del sistema operativo (`iptables-nft`) + +Puedes comprobar las reglas _nftables_ creadas por **Docker** listando todas las reglas con `sudo nft list ruleset`. + +Docker inserta nuevas reglas en las tablas `ip filter` e `ip6 filter` Y también en las dos tablas `ip nat` e `ip6 nat`. + +Para nuestro contenedor de ejemplo tendríamos: + +```bash +sudo nft list ruleset + +# Warning: table ip filter is managed by iptables-nft, do not touch! +table ip filter { +. +. +chain DOCKER { + iifname != "br-c044d323c53d" oifname "br-c044d323c53d" ip daddr 172.18.0.2 tcp dport 80 counter packets 12 bytes 600 accept + iifname != "br-c044d323c53d" oifname "br-c044d323c53d" ip daddr 172.18.0.2 tcp dport 8080 counter packets 10 bytes 564 accept + } + + chain DOCKER-ISOLATION-STAGE-1 { + iifname "br-c044d323c53d" oifname != "br-c044d323c53d" counter packets 814 bytes 1292817 jump DOCKER-ISOLATION-STAGE-2 + iifname "docker0" oifname != "docker0" counter packets 0 bytes 0 jump DOCKER-ISOLATION-STAGE-2 + counter packets 1931 bytes 1511228 return + } + + chain DOCKER-ISOLATION-STAGE-2 { + oifname "br-c044d323c53d" counter packets 0 bytes 0 drop + oifname "docker0" counter packets 0 bytes 0 drop + counter packets 847 bytes 1295253 return + } + + chain DOCKER-USER { + counter packets 1931 bytes 1511228 return + } +. +. +# Warning: table ip6 filter is managed by iptables-nft, do not touch! +table ip6 filter { +. +. + chain DOCKER { + } + + chain DOCKER-ISOLATION-STAGE-1 { + iifname "br-c044d323c53d" oifname != "br-c044d323c53d" counter packets 0 bytes 0 jump DOCKER-ISOLATION-STAGE-2 + iifname "docker0" oifname != "docker0" counter packets 0 bytes 0 jump DOCKER-ISOLATION-STAGE-2 + counter packets 0 bytes 0 return + } + + chain DOCKER-ISOLATION-STAGE-2 { + oifname "br-c044d323c53d" counter packets 0 bytes 0 drop + oifname "docker0" counter packets 0 bytes 0 drop + counter packets 0 bytes 0 return + } + + chain DOCKER-USER { + counter packets 0 bytes 0 return + } +. +. +. +# Warning: table ip nat is managed by iptables-nft, do not touch! +table ip nat { + chain DOCKER { + iifname "br-c044d323c53d" counter packets 0 bytes 0 return + iifname "docker0" counter packets 0 bytes 0 return + iifname != "br-c044d323c53d" tcp dport 80 counter packets 12 bytes 600 dnat to 172.18.0.2:80 + iifname != "br-c044d323c53d" tcp dport 8080 counter packets 10 bytes 564 dnat to 172.18.0.2:8080 + } + + chain POSTROUTING { + type nat hook postrouting priority srcnat; policy accept; + oifname != "br-c044d323c53d" ip saddr 172.18.0.0/16 counter packets 6 bytes 410 masquerade + oifname != "docker0" ip saddr 172.17.0.0/16 counter packets 0 bytes 0 masquerade + ip saddr 172.18.0.2 ip daddr 172.18.0.2 tcp dport 80 counter packets 0 bytes 0 masquerade + ip saddr 172.18.0.2 ip daddr 172.18.0.2 tcp dport 8080 counter packets 0 bytes 0 masquerade + } + + chain PREROUTING { + type nat hook prerouting priority dstnat; policy accept; + fib daddr type local counter packets 15705 bytes 853686 jump DOCKER + } + + chain OUTPUT { + type nat hook output priority -100; policy accept; + ip daddr != 127.0.0.0/8 fib daddr type local counter packets 0 bytes 0 jump DOCKER + } +} +table ip6 nat { + chain DOCKER { + } +} +``` + +Podemos comprobar que, como comentamos, la interacción de **Docker** con nuestro cortafuegos **nftables** se hace a través de comandos **iptables**, y las reglas se insertan mediante la herramienta `iptables-nft`, que se encarga de traducir comandos de **iptables** en comandos de **nftables**. + +Si quieres aprender más de la gestión que hace _Docker_ del cortafuegos [aquí tienes mas información](https://docs.docker.com/network/iptables/) + +{{< /admonition >}} + +Con este **Traefik** básico que hemos lanzado, ya prodríamos lanzar servicios que conectaran con el mismo. Vamos a añadir unas lineas a nuestro fichero _docker-compose_, que quedaría así: + +```yml +services: + reverse-proxy: + # The official v3 Traefik docker image + image: traefik:v3.2.0 + # Enables the web UI and tells Traefik to listen to docker + command: --api.insecure=true --providers.docker + ports: + # The HTTP port + - "80:80" + # The Web UI (enabled by --api.insecure=true) + - "8080:8080" + volumes: + # So that Traefik can listen to the Docker events + - /var/run/docker.sock:/var/run/docker.sock + # ... + whoami: + # A container that exposes an API to show its IP address + image: traefik/whoami + labels: + - "traefik.http.routers.whoami.rule=Host(`whoami.docker.localhost`)" +``` + +Hemos incluido un nuevo servicio _whoami_ en nuestro fichero `docker-compose.yml`. Es un servicio bastante chorras que nos facilita la gente de _**Traefik**_ para hacer pruebas. Simplemente responde peticiones con algunos datos como direcciones IP o o el nombre de la máquina servidora. En este caso responderá con datos de la "máquina virtual", es decir el contenedor `whoami` dentro de _Docker_. + +Si levantamos el nuevo servicio con `docker-compose up -d whoami` podremos consultar el dashboard de _**Traefik**_ o directamente el _raw api_ y comprobaremos que _**Traefik**_ ha detectado el nuevo servicio `whoami` y ha configurado la ruta. + +Si queremos comprobarlo en la práctica podemos ejecutar una _request_ con _curl_, con el comando: `curl -H Host:whoami.docker.localhost http://my_server_ip` + +```bash +curl -H Host:whoami.docker.localhost http://my_server_ip +Hostname: 0e1584c3151f +IP: 127.0.0.1 +IP: 172.19.0.3 +RemoteAddr: 172.19.0.2:55008 +GET / HTTP/1.1 +Host: whoami.docker.localhost +User-Agent: curl/7.68.0 +Accept: */* +Accept-Encoding: gzip +X-Forwarded-For: 192.168.0.154 +X-Forwarded-Host: whoami.docker.localhost +X-Forwarded-Port: 80 +X-Forwarded-Proto: http +X-Forwarded-Server: 3b4d7b2f09be +X-Real-Ip: 192.168.0.154 +``` + +**¿Qué esta pasando aquí?** + +**Traefik** abre dos _entry points_ uno atiende peticiones en el puerto 80 y otro en el 8080 (para acceder al _dashboard_). + +Cuando lanzamos nuestra petición con `curl` **Traefik** la recibe por el puerto 80, comprueba que hay un _router_ que encaja con la cabecera (`whoami.docker.localhost`) y, de acuerdo con esa regla, le pasa la petición al contenedor del servicio `whoami`. + +Este _router_ se configuró automáticamente gracias a la etiqueta `traefik.http.routers.whoami.rule=Host('whoami.docker.localhost')` que asociamos al contenedor `whoami` en el fichero `docker-compose.yml`. Estas etiquetas son procesadas a través del _provider_ Docker para reconfigurar **Traefik** en tiempo real. + + +{{< admonition type=warning title="localhost vs my_server_ip" state=open >}} + +Yo estoy probando **Traefik** en un servidor en mi intranet, en mi caso un mini-pc, en tu caso podría ser una raspi, o podrías estar haciendo las pruebas en un vps. O también podrías estar haciendo las pruebas en tu propio ordenador. + +Si estás haciendo las pruebas en tu propio ordenador, tienes que usar `localhost` o `127.0.0.1` en todos los sitios donde he dicho `my_server_ip`, si estás usando un servidor de cualquier tipo, pues la ip correspondiente. Si tienes un VPS correctamente enrutado en el DNS podrías usar el nombre de tu VPS con dominio, p. ej. `http://miHost.miDominio.com/whoami`, tanto para la etiqueta en el fichero `docker-compose` como para la dirección a visitar con el navegador. + +En nuestro caso de ejemplo hemos declarado la ruta para **Traefik** como si estuvieramos en nuestro ordenador (_localhost_), así que para hacer la prueba rápida con _curl_, especificamos la cabecera de nuestra *request* como `whoami.docker.localhost` aunque después __encaminamos__ la _request_ a nuestro servidor con la URL `http://`, donde dirección IP es `127.0.0.1` si estás probando en tu ordenador local, o la que corresponda a la máquina remota. + +{{< /admonition >}} + + +Con esta configuración tan sencilla podemos ver otra funcionalidad muy importante de **Traefik**, si lanzamos un segundo servidor whoami (un segundo contenedor) con el comando: + +`docker compose up -d --scale whoami=2` + +**Traefik** se encargará de hacer __reparto de carga__ entre los dos servidores. Si lanzamos el comando _curl_ anterior, veremos como nos van respondiendo los dos contenedores; veremos como la dirección correspondiente al contenedor `whoami` que contesta la petición va cambiando entre los dos valores: `172.19.0.3` y `172.19.0.4` (aunque en cada escenario de pruebas estas IP pueden cambiar) + +Para parar los contenedores ejecuta `docker compose down`. + +## Conceptos centrales en **Traefik** + +### Configuraciones de **Traefik** + +En **Traefik** distinguimos entre dos configuraciónes: + +___static configuration___ + +: La configuración de arranque de **Traefik**, aquí se definen los + ___Providers__ (_Docker_ en nuestro ejemplo anterior) y los ___entry + points___ (los puertos 80 y 8080 en nuestro ejemplo anterior). Es + decir, elementos que no cambian muy a menudo a lo largo de la vida + del _edge router_ + + **Traefik** lee su configuración estática de tres posibles orígenes (mutuamente exclusivos): + 1. [Fichero de configuración](https://doc.traefik.io/traefik/reference/static-configuration/file/) + 2. [Parámetros por linea de comandos](https://doc.traefik.io/traefik/reference/static-configuration/cli/) + 3. [En variables de entorno](https://doc.traefik.io/traefik/reference/static-configuration/env/) + +___dynamic configuration___ + +: La configuración dinámica del enrutado. Comprende toda la definición + del procesado de las _request_ recibidas por el router. Esta + configuración puede cambiar y se recarga en caliente sin parar el + router y sin provocar pérdidas de conexión o interrupción del + servicio. + + **Traefik** obtiene su configuración dinámica a través de los + __Providers__. En el ejemplo anterior _Docker_ informa a **Traefik** + de las rutas que necesita cada servicio gracias a las etiquetas + ([_Labels_](https://docs.docker.com/engine/reference/commandline/run/#set-metadata-on-container--l---label---label-file)) + que añadimos en la configuración (del servicio) (ver + [referencia](https://doc.traefik.io/traefik/providers/docker/)). Es + un caso de configuración dinámica de **Traefik** a traves del _Docker + Provider_. + + +### _Providers_ + +Son los agentes capaces de cambiar la configuración dinámica de **Traefik** hay varios tipos: _file_, _Kubernetes_, _Docker_ etc. etc. + +En nuestro caso nos vamos a centrar en el _docker provider_ + +**Los _providers_ son parte de la configuración estática de **Traefik**.** + + +### _Entry Point_ + +Son los componentes principales del _"frontend"_ de nuestro _edge +router_. Básicamente son los puertos de entrada donde se reciben las +peticiones de servicio del "exterior". + +**Los _entry point_ son parte de la configuración estática.** + +{{< admonition type=tip title="Traefik requests" state=open >}} + +**Traefik** puede atender peticiones _HTTP_ o peticiones _TCP_ por qué +puede actuar como un _router_ de nivel 7 (_HTTP_) o de nivel 4 +(_TCP_). + +{{< /admonition >}} + +### _Services_ + +Serían los componentes del _"backend"_, en nuestro caso serán +servicios implementados en contenedores Docker. + +**Los _services_ son parte de la configuración dinámica.** + +### _Routers_ + +Son las reglas para pasar peticiones desde el _"frontend"_ al _"backend"_. +Se implementan y reconfiguran por la información recibida por los +_providers_. + +**Los _routers_ son parte de la configuración dinámica.** + +### _Middlewares_ + +Pueden hacer cambios en las peticiones o en las respuestas a +peticiones. Hay multitud de _middlewares_ y además se pueden encadenar +para acumular cambios. + +**Los _middlewares_ son parte de la configuración dinámica.** + + +## Ejemplo 2: Certificados SSL con _Let's Encrypt_ + +Vamos con un ejemplo un pelín más complejo. Vamos a definir un +contenedor de **Traefik** que simplemente expone su _Dashboard_, de +momento no vamos a definir más contenedores, pero podremos añadirlos a +este escenario en el futuro. + +La gracia del ejemplo es que **Traefik** obtendrá los certificados SSL +del dominio (p.ej. miDominio.com) automáticamente desde [_Let's +Encrypt_](https://letsencrypt.org/). Y expondrá el _dashboard_ en un +subdominio, digamos en . + +Preparamos los directorios y los ficheros de trabajo. + +```bash +mkdir -p ~/work/docker/ejemplo_02/traefik +touch ~/work/docker/ejemplo_02/traefik/{traefik.yml,traefik_dynamic.yml,acme.json} +chmod 600 ~/work/docker/ejemplo_02/traefik/acme.json +``` + +- `traefik.yml` será el fichero de configuración estática de **Traefik**. +- `traefik_dynamic.yml` será el fichero de configuración dinámica de **Traefik** +- `acme.json` es el fichero que almacenará los certificados HTTPS. Es **imprescindible** configurar los permisos correctamente o no funcionará. + +### Configuración estática de **Traefik** + +Primero vamos a definir la configuración estática de nuestro +**Traefik**, es imprescindible tener una. La almacenaremos en el fichero +`traefik.yml` y más adelante tendremos que hacer que el contenedor +**Traefik** pueda acceder a esa configuración (con un _bind mount_ por +ejemplo) + +Dentro de esta configuración definiremos: + +___entry points___ + +: Definiremos dos _entry points_: `http` y `https` que aceptarán + peticiones en los puertos 80 y 443. + + Los nombres de los _entry points_ son etiquetas arbitrarias, puedes + llamarlos como quieras. + + ```traefik.yml + entryPoints: + http: + address: :80 + https: + address: :443 + ``` + + La redirección de puertos se solía hacer con un _middleware_ pero + desde la version 2.2 de **Traefik** podemos definirla en la + configuración estática añadiendo unas lineas en la propia definición + de los _entry points_: + + ```traefik.yml + entryPoints: + http: + address: :80 + http: + redirections: + entrypoint: + to: https + https: + address: :443 +``` + +__providers___ + +: Vamos a definir dos _providers_ diferentes, uno de tipo `docker` y + otro de tipo `file`. + + Nuestro objetivo es usar **Traefik** en Docker así que siempre deberíamos + definir el _provider_ docker. Recordemos que a través de este + _provider_ los contenedores (servicios) que definamos en nuestro + _Docker_ informarán a **Traefik** de que rutas necesitan para recibir + peticiones. + + ```traefik.yml + entryPoints: + http: + address: :80 + http: + redirections: + entrypoint: + to: https + https: + address: :443 + + providers: + docker: + endpoint: unix:///var/run/docker.sock + exposedByDefault: false + file: + filename: traefik_dynamic.yml + ``` + + El _docker provider_ de **Traefik** se comunica con _Docker_ (en la + máquina _host_) a traves del _socket_ de _Docker_ (en el sistema de + ficheros de la máquina _host). Así que necesitaremos que el + contenedor _Docker_ pueda acceder al _socket_ con otro _bind mount_ + + El parámetro `exposedByDefault` por defecto vale `true` y hace que + todos los contenedores en _Docker_ sean visibles para **Traefik**. El + valor recomendado es justo el contrario: `false`, para que **Traefik** + "vea" únicamente los contenedores que tiene que tiene que enrutar. + + {{< admonition type=warning >}} + + En este ejemplo concreto el `docker provider` no hara nada. Pero + como estamos viendo como usar **Traefik** con _Docker_ es mejor + acostumbrarse a definirlo siempre. En cuanto definamos algún + contenedor nos hará falta. + + {{< /admonition >}} + + Además del _provider docker_ definimos un segundo _provider_ de tipo + _file_. Este segundo _provider_ nos permitirá especificar la + configuración dinámica mediante un fichero. En el futuro la + configuración dinámica podría ampliarse a través del _docker + provider_ si definimos nuevos contenedores. + + {{< admonition type=info title="provider file: dirname vs filename" state=open >}} + + Podemos definir un _provider file_ especificando un `dirname` en + lugar de un `filename`. En ese caso podríamos tener varios ficheros + en el directorio especificado para añadir modificaciones a la + configuración dinámica de **Traefik** + + {{< /admonition >}} + +__certificates resolvers__ + +: Se encargará de negociar los certificados SSL con _Let's Encrypt_ + ¡automáticamente! + + Los _certificates resolvers_ son parte de la configuración estática. + + Hay [varias maneras](https://doc.traefik.io/traefik/v2.0/https/acme/) + de que **Traefik** obtenga los certificados, en este ejemplo usamos + una muy simple, el _http challenge_. + + ```traefik.yml + entryPoints: + http: + address: :80 + http: + redirections: + entrypoint: + to: https + https: + address: :443 + + providers: + docker: + endpoint: unix:///var/run/docker.sock + exposedByDefault: false + file: + filename: traefik_dynamic.yml + + certificatesResolvers: + myresolver: + acme: + email: + storage: acme.json + httpChallenge: + entryPoint: http + ``` + + Las lineas añadidas al fichero `traefik.yml` definen un + _certificates resolver_ llamado `myresolver` (es una etiqueta, + puedes llamarlo como quieras). Tenemos que especificar un correo + electrónico (sólo a efectos de notificaciones) y el fichero (o la + ruta) donde **Traefik** almacenará los certificados obtenidos. Además + para el _httpChallenge_ tenemos que usar obligatoriamente el puerto + 80, así que especificamos `entryPoint: http` que previamente + asociamos a ese puerto. + + **Traefik** no va a pedir certificados de dominio por iniciativa + propia, tendremos que indicar, en la configuración dinámica, que + servicios queremos asegurar con un certificado (en principio todos) + + Tambien tenemos que acordarnos de que más adelante tendremos que + crear un _bind mount_ para tener fácil acceso al fichero + `acme.json`. + + {{< admonition type=tip title="Protocolo ACME" state=open >}} + + El agente ACME de **Traefik** genera una pareja de claves + pública-privada para hablar con la autoridad certificadora (CA) de + _Let's Encrypt_. Después "pregunta" a la CA que debe hacer para + probar que controla un dominio determinado. + + La CA le planteará un conjunto de posibles "desafíos". Puede pedirle + por ejemplo, que cree un subdominio determinado en el dominio + reclamado, o servir un contenido arbitrario en una dirección HTTP + del dominio. Junto con los desafíos la CA envia un _nonce_ (un + número de un sólo uso) + + Si el agente ACME consigue completar alguno de los desafíos + planteados. Informa a la CA y le devuelve el _nonce_ firmado con su + clave. + + La CA comprueba la solución del desafío y que la firma del _nonce_ + son válidas. A partir de este punto la pareja de claves asociada al + dominio se considera autorizada, y el agente ACME ya puede solicitar + certificados y/o revocarlos usando esta pareja de claves. + + {{< /admonition >}} + +___dashboard___ + +: Activamos el _dashboard_. Necesitamos el _dashboard_ activo. Más + adelante cuando definamos la configuración dinámica indicaremos en + que URL podemos acceder al _dashboard_ y le añadiremos seguridad + para que no sea público. + + ```traefik.yml + entryPoints: + http: + address: :80 + http: + redirections: + entrypoint: + to: https + https: + address: :443 + + providers: + docker: + endpoint: unix:///var/run/docker.sock + exposedByDefault: false + file: + filename: traefik_dynamic.yml + + certificatesResolvers: + myresolver: + acme: + email: + storage: acme.json + httpChallenge: + entryPoint: http + + api: + dashboard: true + ``` + +Con esto tendríamos completa la configuración estática de nuestro +**Traefik**. Podríamos incluso arrancar nuestro contenedor **Traefik** en +_Docker_, teniendo cuidado de mapear los puertos 80 y 443 al host y +estableciendo los _bind mounts_ necesarios para que el contenedor lea +el fichero de configuración estática y el almacen de claves +`acme.json`. Por ejemplo podríamos ejecutar: + +```bash + docker run -d \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -v $PWD/traefik.yml:/traefik.yml \ + -v $PWD/acme.json:/acme.json \ + -p 80:80 \ + -p 443:443 \ + --name traefik \ + traefik:v3.2.0 +``` + +Claro que la configuración dinámica estaría vacía, así que nuestro +**Traefik** sería bastante tonto. Expondría los puertos 80 y 443, pero +no sabría que hacer con las peticiones que le llegaran a esos puertos. +Además, aunque levantaría el _dashboard_ y su acceso en el puerto 8080 +del contenedor, este no sería accesible. Por un lado no hemos añadido +la opción `insecure: true` y por otro no hemos establecido usuario y +password así que no hay manera de consultar el `dashboard` (todavía) + +### Configuración dinámica de **Traefik** + +Vamos a completar la configuración de nuestro _edge router_ definiendo +la parte dinámica de la misma. + +La vamos a definir en un fichero, pero igual que la estática no hay +por que hacerlo así. + +Ya hemos dicho que nuestro router básico solo expondrá el _dashboard_, +así que tenemos que definir la capa de seguridad de acceso al +_dashboard_ y una ruta apuntando al servicio. + +Para usar el _middleware_ [_Basic +Auth_](https://doc.traefik.io/traefik/middlewares/basicauth/), +necesitamos declarar parejas de usuario:password. Las contraseñas deben +ser _hash_ MD5, SHA1 o BCrypt. + +Para generar las credenciales, es decir la pareja de usuario y +password "_hasheada_" podemos usar distintas herramientas. + +Con `htpasswd`: + +```bash +sudo apt install apache-utils +echo $(htpasswd -nb ) +``` + +Con `openssl`: + +```bash +openssl passwd -apr1 +``` + +{{< admonition type=info title="Hashes" state=open >}} + +Si ejecutas varias veces el comando `openssl` con la misma contraseña +los resultados serán **distintos**. + +Esto es normal, en cada ejecución se cambia el `salt` para que +funcione así. Esto impide, por ejemplo, que un atacante detecte si +estas reutilizando contraseñas. + +Si te fijas en la salida tiene tres campos delimitados +por `$$` + +El primer campo es `apr1` el tipo de _hash_ que estamos usando. + +El segundo campo es el `salt` (semilla) utilizado para generar el _hash_ + +El tercer campo es el _hash_ propiamente dicho + +Por otro lado veréis que en muchos sitios de internet indican que hay que duplicar +los símbolos '$' para escaparlos, tipicamente usando algún filtro +_sed_ como `| sed -E "s:[\$]:\$\$:g"` pero **solo aplica** cuando +usamos _labels_ para configurar los servicios, **no en nuestro caso** +ya que usamos un fichero la configuración dinámica. + +{{< /admonition >}} + +Una vez que tenemos nuestra contraseña super segura en formato _hash_ +podemos añadir las siguientes lineas a nuestra configuración dinámica, +para definir un _middleware_ que llamaremos `myauth`: + +```yaml +# Declaring the user list +http: + middlewares: + myauth: + basicAuth: + users: + - "user1:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/" + - "user2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0" +``` + + +Una vez definido nuestro _middleware_ de tipo _basicAuth_ solo nos +falta definir el +[_router_](https://doc.traefik.io/traefik/routing/routers/) para +nuestro _dashboard_. Lo llamaremos `mydash`: + +```yaml +http: + middlewares: + myauth: + basicAuth: + users: + - "user1:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/" + - "user2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0" + + routers: + mydash: + rule: Host(`dashtraefik.miDominio.com`) + middlewares: + - myauth + service: api@internal # This is the defined name for api. You cannot change it. + tls: + certresolver: myresolver +``` + +Con esto completamos la configuración de nuestro **Traefik**, antes de +poder probar tendríamos que asegurarnos de apuntar el nombre +`dashtraefik.miDominio.com` a la dirección IP correcta en nuestro gestor del dominio. + +Si ya tenemos el DNS correctamente configurado podríamos lanzar la +prueba con: + +```bash + docker run -d \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -v $PWD/traefik.yml:/traefik.yml \ + -v $PWD/traefik_dynamic.yml:/traefik_dynamic.yml \ + -v $PWD/acme.json:/acme.json \ + -p 80:80 \ + -p 443:443 \ + --name traefik \ + traefik:v3.2.0 +``` + +Si no hay errores en nuestro DNS ni en la configuración del **Traefik** +comprobaremos que podemos acceder tanto a + como + (el primero estará redirigido al segundo) + +Veremos también que **Traefik** ha negociado con _Let's Encrypt_ para +obtener los certificados SSL para nuestro dominio. Prueba a hacer un +`cat acme.json` en el servidor para ver el contenido del fichero. + +En caso de problemas, probablemente el primer paso sea comprobar los +logs del contenedor **Traefik** con `docker logs traefik` + +## Ejemplo 3: Una configuración sencilla para empezar con Traefik en Producción + +Vamos a ver una configuración sencilla que nos sirva de base para +empezar en producción. + +Posiblemente quieras crear un usuario específico para mantener el +_docker_ en tu servidor. + +```bash +sudo adduser dockadmin +gpasswd -a dockadmin sudo +gpasswd -a dockadmin docker +``` + +Preparamos los directorios y ficheros de trabajo: + +```bash +mkdir -p ~/work/docker/ejemplo_03/{portainer,traefik/{configurations,letsencrypt}} +touch ~/work/docker/ejemplo_03/{docker-compose.yml,traefik/{traefik.yml,letsencrypt/acme.json,configurations/middlewares.yml}} +chmod 600 ~/work/docker/ejemplo03/traefik/letsencrypt/acme.json +``` + +### _Networks_ + +Vamos a organizar nuestro _Docker_ con dos _networks_: + +___frontnet___ + +: En la red _frontnet_ tendremos todos los contenedores que expongan + un interfaz de usuario "al exterior". Evidentemente nuestro + contenedor **Traefik** tiene que estar en esta red. Otro ejemplo + sería un servidor web. En realidad todos los contenedores que van + implementar servicios a través de **Traefik** tienen que estar + conectados a esta red. (Podríamos haberla llamado _traefiknet_) + +___backnet___ + +: Los contenedores que no exponen servicios al exterior solo tienen + que estar en esta red. Por ejemplo un servidor de base de datos es + muy posible que esté conectado exclusivamente a la red _backnet_. + + Los contenedores de la red _frontnet_ pueden necesitar una conexión + adicional con la red _backnet_. Por ejemplo un contenedor + _Wordpress_ tiene que estar en la red _frontnet_ para publicar las + páginas web a través de **Traefik** pero también en la red _backnet_ + para comunicarse con el servidor de base de datos. + +{{< image src="/images/traefik/TraefikNetworks.jpg" >}} + +Creamos las redes en _Docker_ con: + +```bash + docker network create --subnet 172.20.0.0/16 backnet + docker network create --subnet 172.21.0.0/16 frontnet + docker network ls # Para listar las redes existentes +``` + +Los rangos de direcciones IP son arbitrarios, igual que los nombres +(_frontnet_ y _backnet_) de las redes. Podríamos usar otros cualquiera. + + +### **Traefik** + +Vamos a especificar la configuración de **Traefik**: + +* Tendremos la configuración estática en el fichero `traefik.yml` +* Definiremos un directorio `configurations` mediante un _provider + file_, todos los ficheros que pongamos en ese directorio se cargarán + como parte de la configuración dinámica. Cualquier modificación en + los ficheros o cualquier nuevo fichero se cargará "al vuelo", sin + necesidad de reiniciar **Traefik**. + +#### _middlewares_ comunes + +En el fichero `/configurations/middlewares.yml` vamos a definir +_middlewares_ que usaremos con muchos (o todos) nuestros contenedores. + +Ya sabemos que en **Traefik** podemos definir la configuración dinámica +de muchas formas, podríamos usar _labels_ asociadas al contenedor +**Traefik** para definir estos _middlewares_, pero creo que así queda más +ordenado. + +Definimos los _middlewares_: + +___secureHeaders___ + +: Que define una serie de cabeceras que vamos a aplicar a todas las + conexiones HTTP de nuestro **Traefik**. + * ___frameDeny: true___ Para evitar ataques + [_click-jacking_](https://developer.mozilla.org/en-US/docs/Web/Security/Types_of_attacks#click-jacking) + * ___sslRedirect: true___ Para permitir solo peticiones _https_ + * ___browserXssFilter___ Para paliar ataques de [_cross site + scripting_](https://developer.mozilla.org/en-US/docs/Web/Security/Types_of_attacks#cross-site_scripting_xss) + * ___contentTypeNosniff: true___ Ver + [referencia](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options) + * ___forceSTSHeader: true___: fuerza cabeceras STS para todas las + conexiones, con el flag _preload_ y la directiva + _includeSubdomains_ y un _max-age_ de un año. + +___local-whitelist___ + +: Los servicios a los que apliquemos este _middleware_ sólo se podrán + acceder desde IP en la _whitelist_. Este _middleware_ es + interesante si accedes a los servicios desde la intranet. + +___user-auth___ + +: Para definir credenciales de acceso a nuestros servicios con + _basicAuth_. **Traefik** nos pedirá usuario y contraseña para los + servicios a los que apliquemos este _middleware_. + +___localsec___ + +: Este es un ejemplo de "cadena de _middlewares_", aplicar este + _middleware_ equivale a aplicar _secureHeaders_ y + _local-whitelist_. ___chain___ es muy interesante para organizar + nuestros _middlewares_ y aplicar la misma configuración a varios + contenedores. + +Contenido del fichero `middlewares.yml`: + +```yaml +http: + middlewares: + secureHeaders: + headers: + frameDeny: true + sslRedirect: true + forceSTSHeader: true + stsIncludeSubdomains: true + stsPreload: true + stsSeconds: 31536000 + + local-whitelist: + ipWhiteList: + sourceRange: + - "10.0.0.0/24" + - "192.168.0.0/16" + - "172.0.0.0/8" + + user-auth: + basicAuth: + users: + - "user1:$apr1$MTqfVwiE$FKkzT5ERGFqwH9f3uipxA1" + - "user2:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/" + - "user3:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0" + localsec: + chain: + middlewares: + - local-whitelist + - secureHeaders +``` + +#### Configuración estática + +En el fichero `traefik.yml` definimos la configuración estática de **Traefik**: + +* Activamos el _dashboard_ +* Definimos los _entry points_ + * `htpp` redirigido a `https` + * `https` al que aplicamos los _middleware_ con las cabeceras de + seguridad y el _certresolver_ apuntando a _Let's Encrypt_ para + generar certificados SSL +* Definimos los _providers_ + * `docker` imprescindible claro + * `file` apuntando al directorio `/configurations` para cargar + ficheros que amplien la configuración dinámica +* Definimos el _certresolver_ para obtener los certificados SSL + automáticamente desde _Let's Encrypt_ + +El contenido del fichero `traefik.yml` nos quedaría: + +```yaml +# traefik.yml +api: + dashboard: true + +entryPoints: + http: + address: :80 + http: + redirections: + entryPoint: + to: https + + https: + address: :443 + http: + middlewares: + - secureHeaders@file + tls: + certResolver: letsencrypt + +providers: + docker: + endpoint: "unix:///var/run/docker.sock" + exposedByDefault: false + file: + directory: /configurations + +certificatesResolvers: + letsencrypt: + acme: + email: youruser@mailprovider.com + storage: acme.json + httpChallenge: + entryPoint: http +``` + +### docker-compose.yml + +Ahora que ya tenemos listos los ficheros de configuración de **Traefik** +vamos a definir el fichero `docker-compose.yml`: +* Definimos siempre la versión de nuestras imágenes _Docker_ + explicitamente. Es muy mala idea usar `:latest` en producción, hay + que saber que versión usamos y hacer las actualizaciones con + conocimiento de causa. +* Definimos un nombre (`traefik`) para nuestro contenedor, y definimos + una política para `restarts` +* Establecemos la opción `security_opt: no-new-privileges` para + impedir escaladas de privilegios con `setuid` +* Definimos los _volumes_ del contenedor **Traefik** + * Mapeamos `/etc/localtime` para que el contenedor sincronice la + hora con el _host_ + * Mapeamos el _socket_ de _Docker_, imprescindible para el + _provider docker_ + * Mapeamos el fichero de configuración estática `traefik,yml`, el + directorio de configuraciones dinámicas `/configurations` y el + fichero `acme.json` para el _certresolver_ +* Añadimos etiquetas (_labels_) para la configuración dinámica del + contenedor **Traefik** (concretamente del _dashboard_): + * El servicio (se refiere al _dashboard_)se proveerá a través de **Traefik** + * El contenedor se conecta a la red _frontnet_ + * Se configura el router `traefik-secure` + * Para el servicio _Traefik dashboard_(`api@internal`) + * Con _middleware_ `user-auth` + * Con la regla ``Host(`dashtraefik.yourdomain.com`)`` +* Por último declaramos la red (_network_) _frontnet_ como `external` + ya que la hemos creado previamente. + + +```yaml +services: + traefik: + image: traefik:v3.2.0 + container_name: traefik + restart: unless-stopped + security_opt: + - no-new-privileges:true + networks: + - frontnet + ports: + - 80:80 + - 443:443 + volumes: + - /etc/localtime:/etc/localtime:ro + - /var/run/docker.sock:/var/run/docker.sock:ro + - ./traefik/traefik.yml:/traefik.yml:ro + - ./traefik/configurations:/configurations:ro + - ./traefik/letsencrypt/acme.json:/acme.json + labels: + - "traefik.enable=true" + - "traefik.docker.network=frontnet" + - "traefik.http.routers.traefik-secure.entrypoints=https" + - "traefik.http.routers.traefik-secure.rule=Host(`dashtraefik.yourdomain.com`)" + - "traefik.http.routers.traefik-secure.service=api@internal" + - "traefik.http.routers.traefik-secure.middlewares=user-auth@file" + +networks: + frontnet: + external: true +``` + +Si ya tenemos nuestro DNS configurado para que el nombre +`dashtraefik.yourdomain.com` se resuelva correctamente ya podemos +arrancar nuestro **Traefik**: + +```bash +cd ~/work/docker/ejemplo_03 +docker-compose --verbose up -d +``` + +Ya tenemos nuestro contenedor **Traefik** funcionando correctamente. +Estamos listos para añadir nuevos servicios a nuestra plataforma. + +### Nuestro primer servicio: Portainer + +Vamos a definir nuestro primer servicio conectado por **Traefik** en el +mismo fichero `docker-compose.yml`. Añadimos la definición del nuevo +servicio en las lineas que van de la 27 a la 45: + +```yaml +services: + traefik: + image: traefik:v3.2.0 + container_name: traefik + restart: unless-stopped + security_opt: + - no-new-privileges:true + networks: + - frontnet + ports: + - 80:80 + - 443:443 + volumes: + - /etc/localtime:/etc/localtime:ro + - /var/run/docker.sock:/var/run/docker.sock:ro + - ./traefik/traefik.yml:/traefik.yml:ro + - ./traefik/configurations:/configurations:ro + - ./traefik/letsencrypt/acme.json:/acme.json + labels: + - "traefik.enable=true" + - "traefik.docker.network=frontnet" + - "traefik.http.routers.traefik-secure.entrypoints=https" + - "traefik.http.routers.traefik-secure.rule=Host(`dashtraefik.yourdomain.com`)" + - "traefik.http.routers.traefik-secure.service=api@internal" + - "traefik.http.routers.traefik-secure.middlewares=user-auth@file" + + portainer: + image: portainer/portainer-ce:2.24.0-alpine + container_name: portainer + restart: unless-stopped + security_opt: + - no-new-privileges:true + networks: + - frontnet + volumes: + - /etc/localtime:/etc/localtime:ro + - /var/run/docker.sock:/var/run/docker.sock:ro + - ./portainer:/data + labels: + - "traefik.enable=true" + - "traefik.docker.network=frontnet" + - "traefik.http.routers.portainer-secure.entrypoints=https" + - "traefik.http.routers.portainer-secure.rule=Host(`portainer.yourdomain.com`)" + - "traefik.http.routers.portainer-secure.service=portainer" + - "traefik.http.services.portainer.loadbalancer.server.port=9000" + +networks: + frontnet: + external: true +``` + +Añadimos un nuevo servicio `portainer`, con las opciones de rearranque +y de seguridad iguales a las de **Traefik**. Mapeamos también el fichero +`/etc/localtime` para que el contenedor se ponga en hora con el +_host_; y el _socket_ `docker.sock` que _Portainer_ necesita +(_Portainer_ es un _frontend_ para _Docker_) + +Añadimos también las etiquetas para informar a **Traefik** del nuevo +servicio. +* El servicio se llama _portainer_ +* Acepta peticiones en el puerto 9000 +* _Portainer_ usará el _entry point_ `https` de **Traefik** +* Y la regla de enrutado será ``Host(`portainer.yourdomain.com`)`` + + + +Una vez completada la configuración del DNS, podemos levantar nuestro +nuevo servicio con: + +```bash +docker-compose up -d portainer +``` + +{{< admonition type=warning title="Login en Portainer" state=open >}} + +_Portainer_ implementa su propio sistema de gestión y autenticación de +usuarios. Además da problemas si activamos el _middleware_ `user-auth` + +**Tendrás que crear un usuario `admin` y protegerlo con contraseña tan +pronto como lo arranques.** + +{{< /admonition >}} diff --git a/static/images/traefik/TraefikNetworks.jpg b/static/images/traefik/TraefikNetworks.jpg new file mode 100644 index 0000000..de6dcff Binary files /dev/null and b/static/images/traefik/TraefikNetworks.jpg differ