From 10fff064646e02e7771cfed990e8bc9c4a6798fa Mon Sep 17 00:00:00 2001 From: Bertrand Benjamin Date: Fri, 14 Aug 2026 06:21:01 +0200 Subject: [PATCH] feat(stacks): migrate traefik --- .gitignore | 4 ++ stacks/traefik/docker-compose.yml | 42 +++++++++++++++++ stacks/traefik/dynamic/chioggia.opytex.yml | 18 ++++++++ stacks/traefik/dynamic/crowdsec.yml | 18 ++++++++ stacks/traefik/dynamic/opytex.toml | 28 +++++++++++ stacks/traefik/dynamic/raw.toml | 22 +++++++++ stacks/traefik/secrets.env.yaml | 25 ++++++++++ stacks/traefik/traefik.toml | 54 ++++++++++++++++++++++ 8 files changed, 211 insertions(+) create mode 100644 stacks/traefik/docker-compose.yml create mode 100644 stacks/traefik/dynamic/chioggia.opytex.yml create mode 100644 stacks/traefik/dynamic/crowdsec.yml create mode 100644 stacks/traefik/dynamic/opytex.toml create mode 100644 stacks/traefik/dynamic/raw.toml create mode 100644 stacks/traefik/secrets.env.yaml create mode 100644 stacks/traefik/traefik.toml diff --git a/.gitignore b/.gitignore index e6bbab8..501d735 100644 --- a/.gitignore +++ b/.gitignore @@ -64,3 +64,7 @@ terraform.rc *.agekey age-key.txt keys.txt + +# Le stockage ACME de Traefik : clé de compte Let's Encrypt et clés privées de +# tous les certificats. Vit sur le serveur uniquement, jamais dans le dépôt. +acme.json diff --git a/stacks/traefik/docker-compose.yml b/stacks/traefik/docker-compose.yml new file mode 100644 index 0000000..0c651aa --- /dev/null +++ b/stacks/traefik/docker-compose.yml @@ -0,0 +1,42 @@ +services: + traefik: + image: traefik:v3.6 + container_name: traefik2 + restart: always + # Pas de `command:` : Traefik lit /etc/traefik/traefik.toml par défaut. + # Un `--log.level` ici écraserait silencieusement celui du toml. + ports: + - "80:80" + - "443:443" + volumes: + - ./traefik.toml:/etc/traefik/traefik.toml:ro + - ./dynamic:/etc/traefik/dynamic:ro + # Doit exister comme FICHIER avant le premier `up`, sinon Docker crée un + # répertoire à sa place et Traefik redemande tous les certificats. + - ./acme.json:/acme.json + - traefik_log:/var/log/traefik/ + - /var/run/docker.sock:/var/run/docker.sock:ro + secrets: + - crowdsec_lapi_key + networks: + - traefik-proxy + labels: + - traefik.enable=true + - traefik.docker.network=traefik-proxy + - traefik.http.routers.traefik_secure.entrypoints=web-secure + - traefik.http.routers.traefik_secure.rule=Host(`chioggia.opytex.org`) + - traefik.http.routers.traefik_secure.tls.certresolver=letsencrypt + - traefik.http.routers.traefik_secure.service=api@internal + - traefik.http.routers.traefik_secure.middlewares=authentik@file + +secrets: + crowdsec_lapi_key: + environment: CROWDSEC_LAPI_KEY + +volumes: + traefik_log: + external: true + +networks: + traefik-proxy: + external: true diff --git a/stacks/traefik/dynamic/chioggia.opytex.yml b/stacks/traefik/dynamic/chioggia.opytex.yml new file mode 100644 index 0000000..6ff0621 --- /dev/null +++ b/stacks/traefik/dynamic/chioggia.opytex.yml @@ -0,0 +1,18 @@ +http: + middlewares: + authentik: + forwardAuth: + address: http://authentik:9000/outpost.goauthentik.io/auth/traefik + trustForwardHeader: true + authResponseHeaders: + - X-authentik-username + - X-authentik-groups + - X-authentik-email + - X-authentik-name + - X-authentik-uid + - X-authentik-jwt + - X-authentik-meta-jwks + - X-authentik-meta-outpost + - X-authentik-meta-provider + - X-authentik-meta-app + - X-authentik-meta-version diff --git a/stacks/traefik/dynamic/crowdsec.yml b/stacks/traefik/dynamic/crowdsec.yml new file mode 100644 index 0000000..2774d2b --- /dev/null +++ b/stacks/traefik/dynamic/crowdsec.yml @@ -0,0 +1,18 @@ +http: + middlewares: + crowdsec: + plugin: + bouncer: + enabled: true + crowdsecMode: stream + crowdsecLapiScheme: http + crowdsecLapiHost: crowdsec:8080 + # La clé elle-même est dans secrets.env.yaml. Le provider fichier de + # Traefik ne substitue aucune variable d'environnement : c'est ce + # `...File` qui permet de garder ce fichier versionnable en clair. + crowdsecLapiKeyFile: /run/secrets/crowdsec_lapi_key + logLevel: ERROR + # crowdsecAppsecEnabled: true + # crowdsecAppsecHost: crowdsec:7422 + # crowdsecAppsecFailureBlock: false + # crowdsecAppsecUnreachableBlock: false diff --git a/stacks/traefik/dynamic/opytex.toml b/stacks/traefik/dynamic/opytex.toml new file mode 100644 index 0000000..2065523 --- /dev/null +++ b/stacks/traefik/dynamic/opytex.toml @@ -0,0 +1,28 @@ +[http] + + [http.routers] + [http.routers.opytexorg] + entryPoints = ["web-secure"] + service = "opytexorg" + middlewares = ["opytexorgstripprefix@file", "opytexorgindex@file", "opytexerror@file"] + rule = "Host(`opytex.org`)" + [http.routers.opytexorg.tls] + certResolver = "letsencrypt" + [[http.routers.opytexorg.tls.domains]] + main = "opytex.org" + + [http.services] + [http.services.opytexorg.loadBalancer] + [[http.services.opytexorg.loadBalancer.servers]] + url = "http://minio:9000" + + [http.middlewares] + [http.middlewares.opytexorgindex.replacePathRegex] + regex = "^(.*)/$" + replacement = "$1/index.html" + [http.middlewares.opytexorgstripprefix.addPrefix] + prefix = "/opytex.org" + [http.middlewares.opytexerror.errors] + status = ["400-499", "500-599"] + service = "opytexorg@file" + query = "opytex.org/404.html" diff --git a/stacks/traefik/dynamic/raw.toml b/stacks/traefik/dynamic/raw.toml new file mode 100644 index 0000000..940e9c9 --- /dev/null +++ b/stacks/traefik/dynamic/raw.toml @@ -0,0 +1,22 @@ +[http] + + [http.routers] + [http.routers.raw] + entryPoints = ["web-secure"] + service = "raw" + middlewares = ["rawstripprefix@file"] + rule = "Host(`raw.opytex.org`)" + [http.routers.raw.tls] + certResolver = "letsencrypt" + [[http.routers.raw.tls.domains]] + main = "raw.opytex.org" + + [http.services] + [http.services.raw.loadBalancer] + [[http.services.raw.loadBalancer.servers]] + url = "http://minio:9000" + + [http.middlewares] + [http.middlewares.rawstripprefix.addPrefix] + prefix = "/raw" + diff --git a/stacks/traefik/secrets.env.yaml b/stacks/traefik/secrets.env.yaml new file mode 100644 index 0000000..00c8820 --- /dev/null +++ b/stacks/traefik/secrets.env.yaml @@ -0,0 +1,25 @@ +CROWDSEC_LAPI_KEY: ENC[AES256_GCM,data:4gTTw3pdbkmpssdRWroB8sEzibQKE3i+STir1gAh1N5W/f58gi6K8lpF8w==,iv:MUEjf0tTf5S+rg8Yb+7PlxgNqGqOGbLHwc8Gd5UAwpM=,tag:rI9tOy/Qzb3XzJgbZXOa6Q==,type:str] +sops: + age: + - enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBkeDlpb3N3amFuTTcwamFy + a0pSTDhTRExLUUxjM1ZIcmU2Rko3enM1dkZNCm9OZHdxcTBxQnB2RzFEbkNOTEJ6 + WUdTR3NBeXV3KzlnM2N1YkIvbDJJQUUKLS0tIE9ZQzVKbnZES2tRdkhpUjZjQTdP + Qm1EdGR6TitNaTFVS1pyTGhGK08wbGMK9ZwsDE2ih9EI87MNqFuA65XFZBAL2akF + 4ftZM3DDQsvL62DWPYi6t466zg1kH2R2zx/wz9Jil4LKY13vJgz+bQ== + -----END AGE ENCRYPTED FILE----- + recipient: age18m2zl3pwgw97djshzcf4wmhrqxksl4erqse6edkpxpkg5rcz0ujs8u4c7s + - enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBlUS9sZXVCLzIwcE4xdXdx + K2R5eTJnTWs3NHRlbEVsMEFhSnJIdGdpS0ZzCmNmVnVqYnlUS09pK25hcFJySnRn + SS81MHpJYksyaFNUeXFEWWFPQXU2R1EKLS0tIHBnZU1OcGZsSnRCM1hIYWlIZ3hK + UGZhSjVRRldXOUI2d3lZNlM1WE9lb2cK1ByWPZ8taCHED0jQiUkldAehZ7KQYPJx + E20o7wnn45LHx1Km+R91CeNTj7NFIXCpLZrhcXo89knvL4qFNCkT+Q== + -----END AGE ENCRYPTED FILE----- + recipient: age1fh70nny5hzz8a8g9077kgfca0lhlh3wsjf4fjrv4klx5ucdtpdmsazuqdj + lastmodified: "2026-08-14T04:19:11Z" + mac: ENC[AES256_GCM,data:VVPI//syL8fm81WyyIgxLlRdNYdQr9IZT2s5Kf9TygBqFwhqooaVHiYu1uaka3gEpFf0fZkfz3iB7i/vkT+/c6MoXKSUkVrUmT2FaDEZM2WITv3C7+Nh0hrcrksWGBXXsUSBW0ZnFm7WPG/SlYoyqcF+W9oHbA/fa4nCHJY4pBk=,iv:KsPgBCPwgbYf9opXpUgd6DHqf+M2Zuu0oIBdQAA1+p4=,tag:0e9wO7H6LQU5YHjjJsClNQ==,type:str] + unencrypted_suffix: _unencrypted + version: 3.13.3 diff --git a/stacks/traefik/traefik.toml b/stacks/traefik/traefik.toml new file mode 100644 index 0000000..87c130a --- /dev/null +++ b/stacks/traefik/traefik.toml @@ -0,0 +1,54 @@ +[global] + checkNewVersion = true + sendAnonymousUsage = false + +[log] + level = "INFO" + filePath = "/var/log/traefik/traefik.log" + +[entryPoints] + [entryPoints.web] + address = ":80" + [entryPoints.web.http.redirections.entryPoint] + to = "web-secure" + scheme = "https" + + [entryPoints.web-secure] + address = ":443" + +[accessLog] + filePath = "/var/log/traefik/access.log" + bufferingSize = 100 + +[providers] + [providers.file] + directory = "/etc/traefik/dynamic" + + [providers.docker] + endpoint = "unix:///var/run/docker.sock" + watch = true + exposedbydefault = false + +[api] + dashboard = true + debug = false + +# Sans ce bloc, `plugin.bouncer` référencé par dynamic/crowdsec.yml n'existe +# pas : le middleware crowdsec@file devient invalide et TOUS les routeurs qui +# l'utilisent tombent en erreur (404). Traefik ne lit cette configuration qu'au +# démarrage — un conteneur déjà lancé survit à sa disparition, la panne +# n'apparaît qu'à la recréation suivante. C'est arrivé le 2026-08-13. +[experimental] + [experimental.plugins.bouncer] + moduleName = "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin" + version = "v1.4.5" + +[certificatesResolvers.letsencrypt.acme] + email = "benjamin.bertrand@opytex.org" + # Chemin ABSOLU, et monté depuis l'hôte. En relatif, le fichier atterrissait + # dans le répertoire de travail du conteneur : perdu à chaque recréation, + # avec réémission complète des certificats et plafond Let's Encrypt au bout. + storage = "/acme.json" + tlschallenge = true + [certificatesResolvers.letsencrypt.acme.httpChallenge] + entryPoint = "web"