You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1153 lines
39 KiB

---
weight: 4
title: "Apuntes de Traefik v2"
date: 2021-01-31T22:28:31+0100
draft: false
summary: "Apuntes de Traefik v2"
categories:
- notes
tags:
- docker
- traefik
---
Apuntes sobre Traefik v2.
<!--more-->
# 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 >}}
<!--
-
- https://doc.traefik.io/traefik/v2.0/user-guides/docker-compose/basic-example/
- [[https://doc.traefik.io/traefik/v2.0/][Documentación Oficial]]
- [Simplecto: un ejemplo simple con nginx y certificados](https://www.simplecto.com/use-traefik-with-nginx-apache-caddyserver-serve-static-files/)
- [IMPORTANTE: DNS challenge en OVH](https://medium.com/nephely/configure-traefik-for-the-dns-01-challenge-with-ovh-as-dns-provider-c737670c0434)
https://medium.com/@containeroo/traefik-2-0-route-external-services-through-traefik-7bf2d56b1057
https://www.simplecto.com/use-traefik-with-nginx-apache-caddyserver-serve-static-files/
-->
## _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 atender todas las
peticiones a los distintos servicios a través de estos dos puertos.
**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_.
## Ejemplo01: 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
version: '3'
services:
reverse-proxy:
# The official v2 Traefik docker image
image: traefik
# 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:latest`
- 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
<http://my_server_ip:8080>, 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 verás que el propio
_Docker_ se encarga de abrir el _firewall_ añadiendo las rutas
necesarias a las _iptables_.
Puedes comprobar las reglas iptables para el puerto 8080 con el
comando `iptables-save | grep 8080`
[Más info](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
version: '3'
services:
reverse-proxy:
# The official v2 Traefik docker image
image: traefik
# 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 del servidor `whoami` como la dirección
IP o el nombre de la máquina. 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_
<http://my_server_ip:8080> o directamente el _raw api_
<http://my_server_ip:8080/api/rawdata> 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.
El servicio `whoami` atiende la petición y devuelve la respuesta al
usuario.
{{< 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.
El caso es que 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://my_server_ip`
{{< /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.
Si quieres parar los contenedores ejecuta `docker-compose down`
## Conceptos centrales en _Traefik_
### _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_.**
### 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_.
### _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.**
## Ejemplo02: 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 <https://dashtraefik.miDominio.com>.
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
### 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: <my_email@mailprovider.com>
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: <my_email@mailprovider.com>
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:v2.4.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 usurio: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 <USER> <PASSWORD>)
```
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:v2.4.0
```
Si no hay errores en nuestro DNS ni en la configuración del _Traefik_
comprobaremos que podemos acceder tanto a
<http://dashtraefik.miDominio.com> como
<https://dashtraefik.miDominio.com> (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`
## Ejemplo03: 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_:
___frontend___
: En la red _frontend_ 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 _traefik_)
___backend___
: 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 _backend_.
Los contenedores de la red _frontend_ pueden necesitar una conexión
adicional con la red _backend_. Por ejemplo un contenedor
_Wordpress_ tiene que estar en la red _frontend_ para publicar las
páginas web a través de _Traefik_ pero también en la red _backend_
para comunicarse con el servidor de base de datos.
{{< image src="/images/traefik/traefik_networks.png" >}}
Creamos las redes en _Docker_ con:
```bash
docker network create --subnet 172.20.0.0/16 backend
docker network create --subnet 172.21.0.0/16 frontend
```
Los rangos de direcciones IP son arbitrarios, igual que los nombres
(_frontend_ y _backend_) de las redes. Podríamos usar otros cualquiera.
{{< admonition type=warning title="Traefik V1: frontend y backend" state=open >}}
**Ojo** en _Traefik V1_ los términos _frontend_ y _backend_ tienen un significado especial. Nada que ver con las redes definidas en el ejemplo03. Cuando consultes información en internet **asegúrate** de que referencian la versión actual de _Traefik_ (2.4 cuando escribo esto). Si hablan mucho de definir el _frontend_ y el _backend_ es muy posible que estén hablando de _Traefik V1_
{{< /admonition >}}
### _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 _frontend_
* 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_) _frontend_ como `external`
ya que la hemos creado previamente.
```yaml
version: '3'
services:
traefik:
image: traefik:v2.4
container_name: traefik
restart: unless-stopped
security_opt:
- no-new-privileges:true
networks:
- frontend
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=frontend"
- "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:
frontend:
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 29 a la 47:
```yaml
version: '3'
services:
traefik:
image: traefik:v2.4
container_name: traefik
restart: unless-stopped
security_opt:
- no-new-privileges:true
networks:
- frontend
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=frontend"
- "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.0.1-alpine
container_name: portainer
restart: unless-stopped
security_opt:
- no-new-privileges:true
networks:
- frontend
volumes:
- /etc/localtime:/etc/localtime:ro
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./portainer:/data
labels:
- "traefik.enable=true"
- "traefik.docker.network=frontend"
- "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:
frontend:
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 >}}