Dans ce billet, nous allons mettre en place un reverse proxy traefik 2 pour plusieurs containers, le tout en SSL, caché derrière un proxy CloudFlare, avec un chiffrement de bout en bout « strict ».
Y’a du taff…
J’ai entendu beaucoup de bien du reverse-proxy traefik . Spécialisé dans le routage des services conteneurisés.
Quelques mots sur traefik… Il s’agit d’un reverse-proxy (qui redirige internet vers différents services sur votre réseau local), complètement open source.
Il gère nativement les containers de type Kubernetes, Docker, Docker Swarm, AWS, Mesos, Marathon, mais aussi les services dits « bare metal », c’est à dire les trucs à l’ancienne, installés avec leurs dépendances et tout (beurk). Evidemment, c’est docker qui m’intéresse.
Sa grande force serait dans le fait qu’il écoute le docker.sock et qu’il crée les routes tout seul comme un grand. Mouais, on va voir ça…
Il parait qu’il gère aussi tout seul le renouvellement des certificats SSL.
Du coup, j’ai eu envie de tester. Jusque là, j’avais un reverse proxy nginx qui redirigeait uniquement vers ma box domotique…. So 2019…
Aller hop, on va rajeunir tout ça et reverse-proxifier vers ma station météo (weewx), vers mon site perso WordPress (vous êtes dessus) et bien sûr ma box domotique…
Particularités
Avant d’aller plus loin, on va parler de mon setup. Parce que pu***n, y’a deux trois petits trucs qui m’ont fait bien galérer.
- Je suis en IP statique.
- J’utilise le proxy CloudFlare qui me protège des scans et du DDoS, fourni du cache et des certificats SSL, bref me protège, le tout gratuitement.
- Tous mes containers sont sur un nuc qui tourne en headless avec un debian sans interface graphique.
- J’administre tout ça depuis ma machine Windows (ben ouais, faut bien jouer aussi quand même).
Il y a des tonnes de tutos sur le net, j’en ai lu pas mal, mais il faut faire bien attention. Beaucoup de ressources parlent de traefik V1.x, dont la configuration n’est pas compatible avec traefik V2.x.
Ici, on va parler de la dernière version en date: traefik V2.2.
Parlons des concepts de ce reverse proxy. Pour être honnête, j’ai eu du mal a les appréhender, mais après avoir lu la doc, plusieurs fois, vraiment plusieurs fois, et bien ça fait sens…
Le grand principe, c’est:
- Une requête arrive.
- Elle est prise en compte par le reverse-proxy.
- En fonction de la requête, il la laisse passer (ou pas).
- Il la traite (on verra ça plus tard).
- Puis l’envoie au service concerné.
Jusque là, rien de trop neuf me direz vous… Et bien vous avez raison! Continuons…
Configurations
Ce qui est un peu plus difficile a appréhender, c’est qu’il y a deux configurations qui cohabitent:
- La statique, qui est chargée par traefik au démarrage et qui concerne tout ce qui est « entryPoints » (ports en écoute) et leurs configurations, système de journalisation, les « providers » ((qui fourni les configurations (docker, un fichier, mesos, etc…)), les certificats SSL, etc…
La configuration statique peut être fournie de trois manières:
- Dans un fichier
- En mode ligne de commande
- Dans des variables d’environnements.
En gros, soit :
- Vous montez un fichier qui contient les paramètres.
- Vous mettez des chiées de trucs genre « command –accesslog: true –accesslog.format: common –etc… »
- Vous définissez des tonnes de variables d’environnements du genre « TRAEFIK_ACCESSLOG: true ».
Bon, vous l’aurez compris, je préfère la première option. Je trouve ça moins brouillon et plus facile à maintenir.
- Et puis on a la configuration dynamique, que traefik va prendre en compte en temps réel et qui va plutôt concerner les services, leurs paramètres d’écoutes, etc…
La configuration dynamique est gérée au travers de « labels » docker, que vous ajouterez aux docker-compose de vos services déjà existants.
Aller, concrétisons un peu tout ça…
Comme d’habitude, on va commencer par créer notre docker-compose:
TRAEFIK DOCKER-COMPOSE
version: '3.4'
services:
traefik:
container_name: traefik
image: traefik:latest
restart: always
ports:
- 80:80
- 443:443
- 8080:8080
environment:
- CF_API_EMAIL="[email protected]"
- CF_API_KEY="APIzretzrtzrezt"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- /etc/localtime:/etc/localtime:ro
- ${PWD}/conf/traefik.toml:/traefik.toml:ro
- ${PWD}/conf/services.toml:/etc/traefik/services.toml
- ${PWD}/logs:/var/log/traefik
- ${PWD}/conf/acme.json:/acme.json
network_mode: "web"
Bien… Détaillons un peu ça… Du moins les trucs spécifiques.
- On ouvre les ports 80 (HTTP), 443 (HTTPS) et 8080 (api traefik).
- On crée deux variables d’environnements avec nos identifiants CloudFlare (attention, pour l’API, il faut la version globale).
- On monte le docker.sock, ben oui normal il faut bien que traefik écoute.
- On monte notre fichier de configuration statique « traefik.toml ».
- On monte notre fichier de configuration dynamique « services.toml ».
- On monte un répertoire de logs (c’est bien d’en conserver un peu quand même).
- On monte un fichier qui « acme.json » qui va contenir nos certificats.
- On se met sur un réseau « web » préalablement créé.
Ok, c’est pas mal. Avant de lancer ça, on va créer notre traefik.toml.
################################################################
#
# Configuration sample for Traefik v2.
#
################################################################
################################################################
# Global configuration
################################################################
[global]
checkNewVersion = true
sendAnonymousUsage = false
################################################################
# Entrypoints configuration
################################################################
[entryPoints]
[entryPoints.web]
address = ":80"
[entryPoints.web.http]
[entryPoints.web.http.redirections]
[entryPoints.web.http.redirections.entryPoint]
to = "websecure"
scheme = "https"
[entryPoints.websecure]
address = ":443"
[entryPoints.websecure.http]
middlewares = ["security@file", "compression@file"]
[entryPoints.websecure.http.tls]
options = "default"
certResolver = "cloudflare"
[[entryPoints.websecure.http.tls.domains]]
main = "domain.xyz"
sans = ["bateau.domain.xyz", "voiture.domain.xyz"]
[entryPoints.traefik]
address = ":8080"
################################################################
# Traefik logs configuration
################################################################
[log]
level = "INFO" #DEBUG, INFO, WARN, ERROR, FATAL, PANIC
filePath = "/var/log/traefik/traefik.log"
format = "common"
################################################################
# Access logs configuration
################################################################
[accessLog]
filePath = "/var/log/traefik/access.log"
format = "common"
[accessLog.fields]
defaultMode = "keep"
[accessLog.fields.headers]
defaultMode = "keep"
################################################################
# API and dashboard configuration
################################################################
[api]
dashboard = true
debug = false
insecure = true
################################################################
# Docker configuration backend
################################################################
[providers]
[providers.docker]
endpoint = "unix:///var/run/docker.sock"
watch = true
exposedByDefault = false
[providers.file]
filename = "/etc/traefik/services.toml"
watch = true
################################################################
# METRICS configuration
################################################################
[metrics]
[metrics.influxDB]
address = "http://10.2.2.120:8086"
protocol = "http"
pushInterval = "10s"
database = "who"
username = "knows"
password = "??"
addEntryPointsLabels = true
addServicesLabels = true
################################################################
# ACME configuration
################################################################
[certificatesResolvers.cloudflare.acme]
email = "[email protected]"
caServer = "https://acme-staging-v02.api.letsencrypt.org/directory"
storage = "acme.json"
[certificatesResolvers.cloudflare.acme.tlsChallenge]
Beaucoup d’informations ici…. Nous allons essayer de détailler dans l’ordre…
ENTRYPOINTS
Que l’on pourrait comprendre comme « points d’entrées », ou ports en écoute ». Nous en déclarons trois que l’on nomme « web » qui écoute sur le port 80, « websecure », qui écoute sur le port 443 et « traefik » qui écoute sur le port 8080. Ce dernier servira pour accéder à l’API et avoir un visu sur ce qu’il se passe.
Oui, je vous vous venir… Effectivement, il y a encore quelques détails à définir…
- Concernant l’entryPoint « web« , nous ajoutons des options qui permettent de rediriger tout ce qui arrive dessus vers l’entryPoint « websecure« , donc HTTPS.
- Concernant l’entryPoint « websecure« , nous faisons références à des « middlewares » que nous expliquerons à la prochaine étape, puis nous définissons les options TLS. En l’occurrence, notre autorité de certification est « cloudflare« , le domaine protégé est « domain.xyz » avec les deux sous-domaines « bateau » et « voiture« .
- Aucune difficulté concernant l’entryPoint « traefik« .
L’intérêt de définir ces paramètres au niveau entryPoint, c’est qu’ils sont pris en compte au niveau global, une fois pour toutes. Nous n’aurons pas besoin de rediriger chaque service vers le port 443, ni de spécifier à chaque fois les paramètres des middlewares et du TLS.
LOGS
Rien de compliqué ici, on gère les logs, ceux de traefik lui même et ceux des accès web.
Nous pourrons retrouver ces logs dans le volume que nous avons défini dans le docker-compose.
API
La non plus, rien de foufou. On active l’API. Celle à laquelle nous accéderons via l’entryPoint « traefik« .
Elle est en « insecure » parce que je ne vais pas la rendre accessible en ligne, elle ne sera accessible qu’en local. De plus, ce n’est qu’une interface de consultation. Rien n’est modifiable, je n’ai donc pas pris la peine de créer des identifiants.
Je le ferai probablement dans un second temps (oui, je sais, c’est pas une bonne pratique ^^).
PROVIDERS
La, c’est une partie importante.
Les « providers » sont ceux qui vont fournir une configuration ou un service. Ils fournissent une configuration dynamique.
Nous en définissons deux:
- provider.docker: C’est le docker.sock que l’on a monte avec notre docker-compose. Cela permet a traefik d’écouter ce qu’il se passe ai niveau docker. Il aura donc connaissance de tous vos containers, réseaux, stack, etc…
- provider.file: C’est un fichier qui contient des éléments de configuration. Il contient notamment les configurations des middlewares, on l’étudiera en même temps.
Pour des raisons de propreté et de contrôle, on indique à traefik de ne pas exposer tous les containers docker.
METRICS
Ici, on indique tous les arguments pour envoyer les statistiques dans une base de données « InfluxDB » (ce qui fera l’objet de la partie 2).
ACME
Ici, on fourni tous les arguments pour le renouvellement automatique de nos certificats CloudFlare.
Il est temps d’aller voir à quoi ressemble le services.toml…
SERVICES.TOML
[http]
[http.middlewares.compression.compress]
excludedContentTypes = ["text/event-stream"]
[http.middlewares.security.headers]
accessControlAllowMethods = ["GET", "OPTIONS", "PUT"]
accessControlMaxAge = 100
addVaryHeader = true
browserXssFilter = true
contentTypeNosniff = true
sslRedirect = true
forceSTSHeader = true
frameDeny = true
stsIncludeSubdomains = true
stsPreload = true
stsSeconds = 2592000
customFrameOptionsValue = "SAMEORIGIN"
referrerPolicy = "same-origin"
featurePolicy = "vibrate 'self'"
Content-Security-Policy = "default-src https://*bordas.xyz:443"
[tls]
[tls.options]
[tls.options.default]
minVersion = "VersionTLS12"
sniStrict = true
cipherSuites = [
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305",
"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256",
"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
"TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305",
"TLS_AES_128_GCM_SHA256",
"TLS_AES_256_GCM_SHA384",
"TLS_CHACHA20_POLY1305_SHA256"
]
curvePreferences = ["CurveP521","CurveP384"]
Et bam, voilà encore un gros fichier a comprendre….
MIDDLEWARES
Les deux premiers blocs, sont des « middlewares« . Ah! On va enfin en parler un peu…. Kézako?
En fait, ce sont des agents qui vont modifier les requêtes… Hum….
Ce sont eux qui vont forwarder les headers ou pas, qui vont paramétrer les types de réponses autorisées, etc… Je vous encourage a aller voir les différentes options…
Ici, nous en déclarons deux:
- [http.middlewares.compression.compress]
- [http.middlewares.security.headers]
Ces noms ne vous disent rien? Retournez voir le fichier traefik.toml….
Vous-vous souvenez que l’on à fait appel à deux middlewares: « security@file » et « compression@file ».
Et oui, nous avons nommé nos middlewares dans le provider file.
TLS
Ici, nous paramétrons les pré-requis pour les certificats et nommons ces options « default« .
La aussi, reprenez le fichier traefik.toml et vous remarquez que nous avons défini les options TLS sur « défault », donc ce que nous avons paramétré dans le provider.file.
SERVICES
Bon, récapitulons…
- Un docker-compose pour traefik qui devrait être fonctionnel.
- Notre traefik.toml qui définit pas mal d’options.
- Le services.toml qui sécurise les requêtes.
- Il ne nous manque plus que des services a exposer….
Si vous lancez votre docker-compose maintenant, normalement ça devrait fonctionner.
On se connecte sur le port 8080 de notre machine docker et…….. Ben ça marche pas….. Ah c’est con ça…. Mais pourquoi?
Tout simplement parce qu’on a créé un network (web) en mode bridge. Le container est donc sur un sous réseau uniquement accessible par votre machine docker…
C’est pas pratique… On va donc y remédier en modifiant un peu le kernel de notre linux et en faire un petit routeur.
La documentation docker nous explique la marche à suivre ICI, sauf que ça ne fonctionne pas. J’ai créé une issue, mais à priori, ça ne sera pas pris en compte de suite.
SYSCTL.CONF
En fait, il faut éditer le fichier « /etc/sysctl.conf » et dé-commenter la ligne « net.ipv4.ip_forward=1 ».
Ensuite, il faut modifier le forward dans l’iptables avec la commande « sudo iptables -P FORWARD ACCEPT ».
Voilà, maintenant, on peut utiliser l’IP de notre machine docker comme passerelle vers ses sous-réseaux.
Par exemple, si ma machine docker à l’IP 192.168.10.78 et mon container traefik est accessible dans le sous réseau 127.52.0.8, PORT 8080, depuis ma machine windows, je n’aurai qu’à entrer 192.168.10.78:8080.
AJOUT DES LABELS
Bam, magie, on arrive vers l’interface web de l’API traefik. Personnellement, je ne trouve pas que ça serve à grand chose, à part voir les services découverts et lorsqu’il y a des erreurs…
Et la vous pourrez voir que…. Vous n’avez pas de services a rediriger… (enfin si quelques uns, mais que des trucs interne à traefik…
Et bien c’est normal, nous avons défini le paramètre exposedByDefault = false.
Il nous faut donc explicitement autoriser les services à être routés.
Pour cela, nous allons éditer le docker-compose de notre service bateau et ajouter les labels comme ceci:
version: '3'
bateau:
container_name: bateau
image: service/bateau
network_mode: "web"
volumes:
- /etc/localtime:/etc/localtime:ro
- XXX:XXX
restart: unless-stopped
ports:
- "6000:80"
labels:
- "traefik.enable=true"
- "traefik.http.routers.weewx.entryPoints=websecure"
- "traefik.http.routers.weewx.rule=Host(`weewx.bordas.xyz`)"
- "traefik.http.services.weewx.loadbalancer.server.port=80"
- "traefik.http.routers.weewx.service=weewx"
Détaillons:
- On se met sur le même réseau que traefik.
- On autorise traefik, ainsi le service pourra être routé.
- On route vers l’entryPoint « websecure ».
- On indique quelle url rediriger.
- Vers quel port (attention, vous remarquez qu’il s’agit du port écouté par le container (80) et pas celui de la machine (6000)).
- On donne le nom du service.
- Pour le reste, il prendra les options par défaut que nous avons défini dans le fichier traefik.toml.
Aller hop, on relance notre container bateau « docker compose up -d », on attend quelques minutes et ça devrait être bon.
Et on fait de même pour tous les services que l’on souhaite…
CONGRATULATONS V1
Euh, on sabre pas encore le champagne…
ZUT. Je me suis rendu compte en allant au lit qu’il y avait un petit détail qui clochait…. Un tout petit… Ma box domotique ne recevait plus les données de mes appareils en réseau… Ben oui, bien sûr, puisqu’elle est maintenant sur le sous réseau « web » de mon serveur docker…
Comment y remédier??? Rappelez-vous, au tout début, je vous ai dit que traefik pouvait même router le trafic des services installés physiquement.
Et ben on va faire comme si notre box domotique était un service physique. Du coup, on va devoir modifier notre services.toml pour rajouter un routeur et un service. Allons-y…
Juste au dessus de la section TLS, nous allons ajouter:
[http.routers]
[http.routers.voiture]
entryPoints = ["websecure"]
rule = "Host(`voiture.domain.xyz`)"
service = "voiture"
[http.services]
[[http.services.voiture.loadBalancer.servers]]
url = "http://10.2.2.112:8123"
Ce qui indique qu’il existe un service « voiture » joignable à l’adresse http://192.168.10.120:8123.
CONGRATULATONS V2
Bon, on va quand même faire un petit troubleshooting guide… Parce qu’en fait, malgré tout, j’ai un peu galéré.
- Facile: -> Il vous faut un nom de domaine
- Facile: -> Il faut mettre à jour vos zones DNS, pour que le domaine ET les sous domaines pointent vers votre IP externe.
- Moyen : -> Si vous avez déjà des services qui écoutent en HTTPS (ma box domotique par exemple), il faudra le désactiver. C’est traefik qui va gérer le chiffrement.
- MegaRelouAIdentifier: -> Si vous utilisez le proxy CloudFlare, attention à vos options SSL/TLS… Si vous voulez commencer par exposer des services en HTTP (pour tester, ce que j’ai fait), ben faut le désactiver sinon vous aurez des erreurs 5xx. Et si vous voulez activer le HTTPS, ben faudra le réactiver (full), sinon, vous aurez des erreurs 301 ou 302 (TOO_MANY_REDIRECTS).
- ProbablementFacileMaisJaiGaléré: -> Si vous avez un wordpress, les seules choses à faire sont de modifier votre « siteurl » et « home » en ajoutant httpS au lieu de http, puis d’ajouter le code ci-dessous dans votre wp-config.php:
/**
* Handle SSL reverse proxy
*/
if (isset($_SERVER["HTTP_X_FORWARDED_PROTO"] ) && "https" == $_SERVER["HTTP_X_FORWARDED_PROTO"] ) {
$_SERVER["HTTPS"] = "on";
}
Si cela ne fonctionne pas, supprimer le container et recréez le, ça devrait aller.
Nous verrons dans un second billet comment ajouter de la belle visualisation et des statistiques à l’aide de influxdb et grafana…
Maintenant c’est l’heure du champagne… ^^