Logo
Overview
Un Talos européen de qualité - Part V - Ingress

Un Talos européen de qualité - Part V - Ingress

October 31, 2025
18 min read
part-05

Objectif 🎯

À la fin de la section précédente, nous sommes arrivés à un kube avec tous les composants critiques de base installés. Il est temps de rendre notre cluster accessible sous 2 points d’entrées :

  • Privée pour les outils internes, uniquement accessible à travers le réseau Tailnet.
  • Publique pour les services web à exposer sur internet, uniquement accessibles via le Load Balancer Hetzner et sécurisée via une solution WAF interne sans nécessairement dépendre d’une solution classique externe renforcée telle que Cloudflare.

Architecture

Comme nous utilisons Hcloud, nous allons utiliser le LoadBalancer natif de Hetzner pour exposer notre Ingress Controller. Grâce à l’intégration cloud opéré par HCCM installé lors de la section 2 de ce guide, nous allons pouvoir automatiser la création et le management des LB Hetzner physique directement au niveau des annotations du service qui servira de LoadBalancer. Le but est d’implémenter l’architecture classique suivante :

ingress-public

Quant aux outils internes, nous allons les exposer via un Ingress interne, accessible uniquement depuis le réseau Tailnet.

Il est donc essentiel d’avoir 2 services bien distincts, un pour l’Ingress public et un pour l’Ingress privé. Les règles d’accès et typologie réseau seront en effet très différentes entre les 2, l’un nécessitant une exposition publique et nécessitant des protections particulières et l’autre étant restreint au réseau interne.

Certificate Issuer

Avant d’attaquer les hostilités avec Traefik, il faut nous débarrasser de la problématique des certificats TLS. Le plus flexible est de générer des wildcards via challenge DNS-01. Du fait de l’utilisation des DNS Scaleway dans ce guide, nous installerons le webhook en charge d’implémenter ce challenge sur les DNS Scaleway. À adapter selon votre propre DNS parmi la myriade de choix entre les providers, ou au pire des cas, implémenter le. Enfin, nous définirons le ClusterIssuer à utiliser par défaut lors de la génération de notre futur certificat.

clusters/dev-kube/module-ingress.tf
module "kube_ingress" {
source = "../../modules/kube/ingress"
scw_dns_access_key = var.scw_dns_username
scw_dns_secret_key = var.scw_dns_password
acme_email = "me@ohmytalos.io"
}
Explanation

En plus du traditionnel email pour l’ACME, nous aurons besoin d’avoir les variables scw_dns_access_key et scw_dns_secret_key correctement renseignées à générer depuis l’interface Scaleway. Ils sont indispensables pour l’accès en écriture à l’API DNS pour créer les entrées nécessaires à la résolution du challenge DNS-01.

Traefik

clusters/dev-kube/module-ingress.tf
module "kube_ingress" {
// ...
crowdsec_bouncer_lapi_key = var.crowdsec_bouncer_lapi_key
traefik_http_basic_auth_username = var.traefik_internal_basic_auth_username
traefik_http_basic_auth_password = var.traefik_internal_basic_auth_password
traefik_service_annotations = {
for key, value in {
name = "${local.cluster_name}-traefik"
type = "lb11"
location = "nbg1"
use-private-ip = "true"
private-ipv4 = "10.0.1.100"
uses-proxyprotocol = "true"
health-check-interval = "15s"
health-check-timeout = "10s"
health-check-retries = "3"
} :
"load-balancer.hetzner.cloud/${key}" => value
}
common_name = "ohmytalos.io"
dns_names = [
"ohmytalos.io",
"*.ohmytalos.io",
"*.dev.ohmytalos.io"
]
internal_domain = local.internal_domain
}
Explanation

Bien que les services internes soient déjà prévus d’être protégés via le réseau Tailnet, il reste indispensable de leur rajouter un middleware de protection HTTP basic auth. Il n’est pas question de laisser l’accès au dashboard longhorn ouvert même sur du réseau interne. Nous rajoutons donc les variables traefik_http_basic_auth_username et traefik_http_basic_auth_password pour définir les identifiants d’accès.

Nous prévoyons également d’installer le plugin CrowdSec bouncer pour Traefik, qui nous permettra de protéger l’Ingress public contre les attaques web courantes. Un token crowdsec_bouncer_lapi_key sera nécessaire pour que le plugin puisse s’authentifier auprès de l’API locale de CrowdSec. Générer le via openssl rand -hex 10 et stocker le dans votre vault.

Nous aurons besoin également de définir les annotations du service de type LoadBalancer pour que le LB Hetzner soit créé avec les bonnes options, grâce au HCCM déployé précédemment. L’utilisation du paramètre uses-proxyprotocol est indispensable pour que Traefik puisse récupérer la bonne IP source du client, au lieu de l’IP du LB Hetzner, permettant le bon fonctionnement du futur WAF. Nous utiliserons le protocole TCP par défaut, et donc du TLS Passthrough au niveau du port 443, la responsabilité de la terminaison TLS étant déléguée à Traefik.

Enfin nous définissons les common_name et dns_names pour générer 2 certificats TLS wildcard via le ClusterIssuer Let’s Encrypt que nous avons défini préalablement, un pour l’interne et un pour l’externe.

De très loin la configuration la plus complexe jusqu’ici, lancer la commande terraform apply devrait vous déployer un Traefik avec son certificat SSL.

Avant de continuer, assurez-vous que le certificat est bien généré et en status Ready via la commande cmctl status certificate default-certificate -n traefik. Vous pouvez vérifier les challenges en cours via la commande k get challenges -n traefik. Cette opération peut prendre plusieurs minutes.

Enfin, vérifier que le load balancer Hetzner est bien créé et que le service LoadBalancer de Traefik possède une IP publique attribuée via kgs -n traefik. Cela devrait afficher quelque chose comme suit avec les 2 services, un public et un privé :

NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
traefik LoadBalancer 10.43.142.148 10.0.1.100,2a01:4f8:1c1f:7ffa::1,91.98.5.26 22:30310/TCP,80:32093/TCP,443:32736/TCP 17d
traefik-internal ClusterIP 10.43.245.78 <none> 443/TCP 17d

Si EXTERNAL-IP est en Pending, alors quelque chose coince au niveau de la création du load balancer physique.

Hcloud LB

Vérifier le status des 9 services (3x3) :

Hcloud LB Overview

Vous pouvez dès à présent enregistrer les entrées DNS suivantes :

@ 3600 IN A <public_ipv4>
@ 3600 IN AAAA <public_ipv6>
dev 3600 IN A <private_ipv4_tailnet_control_plane_nbg1>
dev 3600 IN A <private_ipv4_tailnet_control_plane_fsn1>
dev 3600 IN A <private_ipv4_tailnet_control_plane_hel1>
* 3600 IN CNAME ohmytalos.io.
*.dev 3600 IN CNAME dev.ohmytalos.io.

Après un certain temps de propagation, aller sur https://test.ohmytalos.io/ pour tomber sur la 404 classique de Traefik avec le certificat valide.

HAProxy

Voilà un bon gros morceau de fait. Il reste maintenant à accéder à nos services internes de manière sécurisée via Tailnet. Pour rappel, seuls les control planes sont branchés sur ce réseau privé, il est donc logique d’accéder à nos services au travers d’eux. L’idée est donc de mettre en place un HAProxy en mode TCP 443 (TLS Passthrough) sur chaque control plane, qui fera office de reverse proxy TCP pour router les connexions vers le service traefik-internal du cluster Kubernetes, la partie certificat *.dev.ohmytalos.io étant déjà réglé à l’étape précédente. Le schéma suivant récapitulatif :

ingress-private

modules/kube/ingress/haproxy.tf
resource "kubernetes_namespace_v1" "haproxy" {
metadata {
name = "haproxy"
labels = {
"pod-security.kubernetes.io/enforce" = "privileged"
}
}
}
resource "helm_release" "haproxy" {
repository = "https://haproxytech.github.io/helm-charts"
chart = "haproxy"
version = "1.27.0"
name = "haproxy"
namespace = kubernetes_namespace_v1.haproxy.metadata[0].name
max_history = 2
set = [
{
name = "kind"
value = "DaemonSet"
},
{
name = "daemonset.useHostNetwork"
value = "true"
},
{
name = "daemonset.useHostPort"
value = "true"
},
{
name = "dnsPolicy"
value = "ClusterFirstWithHostNet"
},
{
name = "tolerations[0].key"
value = "node-role.kubernetes.io/control-plane"
},
{
name = "tolerations[0].operator"
value = "Exists"
},
{
name = "nodeSelector.node-role\\.kubernetes\\.io/control-plane"
value = ""
},
{
name = "config"
value = <<EOF
global
log stdout format raw local0
maxconn 1024
defaults
log global
timeout client 60s
timeout connect 60s
timeout server 60s
frontend traefik_in
bind :443
default_backend traefik_backend
backend traefik_backend
server traefik-internal traefik-internal.traefik:443 check send-proxy-v2
EOF
}
]
}
Explanation

Rien de bien particulier ici, nous déployons HAProxy en DaemonSet sur les control planes uniquement, avec le hostNetwork et hostPort activé pour binder le port 443 de chaque nœud. Le chart est configuré pour router toutes les connexions entrantes sur le port 443 vers le service traefik-internal du namespace traefik, en utilisant le mode send-proxy-v2 pour que Traefik puisse récupérer la bonne IP source du client et traverser le middleware de protection d’ips internes tailnet.

Déployer le tout avec terraform apply, et vous devriez être capable d’accéder au dashboard Traefik interne sur https://traefik.dev.ohmytalos.io/ via le réseau Tailnet, le tout protégé par le middleware HTTP basic auth. Assurez-vous d’avoir bien enregistré les IPs internes tailnet sur votre DNS pour tous les sous-domaines *.dev.

Traefik dashboard

Vous devriez également pouvoir accéder aux dashboards de Hubble UI et Longhorn UI, respactivement sur hubble.dev.ohmytalos.io et longhorn.dev.ohmytalos.io via le réseau Tailnet.

CrowdSec

Il ne reste plus que notre WAF à mettre en place pour un truc pro. CrowdSec sera la solution privilégiée pour protéger l’Ingress public. Il est composé à la fois d’un véritable WAF AppSec qui bloque en temps réel la plupart des attaques courantes dont le top 10 OWASP, mais aussi d’un analyseur comportemental par extraction des données de logs.

ingress-waf

Diagramme de séquence en cas de requête malveillante :

ingress-waf-malicious

Et dans le cadre d’une requête légitime :

ingress-waf-not-malicious

clusters/dev-kube/terraform.tf
terraform {
required_providers {
hcloud = {
source = "hetznercloud/hcloud"
version = ">= 1.43.0"
}
}
// ...
}
// ...
provider "hcloud" {
token = var.hcloud_token
}
clusters/dev-kube/module-ingress.tf
data "hcloud_servers" "workers" {
with_selector = "type=worker"
}
data "hcloud_servers" "control_planes" {
with_selector = "type=controlplane"
}
module "kube_ingress" {
// ...
crowdsec_enroll_key = var.crowdsec_enroll_key
crowdsec_enroll_instance_name = local.cluster_name
crowdsec_whitelist_ips = concat(
[for s in data.hcloud_servers.control_planes.servers : s.ipv4_address],
[for s in data.hcloud_servers.workers.servers : s.ipv4_address],
[for s in data.hcloud_servers.control_planes.servers : s.ipv6_address],
[for s in data.hcloud_servers.workers.servers : s.ipv6_address]
)
crowdsec_whitelist_rule_ids = [
911100,
920420,
920450,
]
}
Explanation

Vous pouvez récupérer votre clé d’enrôlement depuis l’interface web de CrowdSec Cloud.

Petite particularité ici, on whiteliste les IPs publiques de tous les nœuds du cluster (control planes et workers) pour des raisons évidentes. Pour cela, n’oubliez pas de déclarer le provider hcloud afin de récupérer les IPs depuis l’API Hetzner.

Vu que l’on activera le WAF crs en mode bloquant, il faut anticiper les faux positifs en prévoyant de whitelister facilement les règles modsecurity.

Comme d’habitude, un petit coup de terraform apply pour lancer le déploiement. Vérifier les logs des agents via kl -n crowdsec ds/crowdsec-agent, afin de vous assurer que les logs de Traefik sont bien ingérés. Côté longhorn, 2 nouveaux volumes devraient être créés pour CrowdSec.

Pour vérifier le bon fonctionnement du WAF, tester rapidement avec https://test.ohmytalos.io/.env. Vous devriez avoir un retour 403. Grâce à la collection CRS, les failles top 10 OWASP les plus communes telles que celles basées sur XSS et injection SQL sont également bloquées.

Terminal window
# XSS -> 403
curl "https://test.ohmytalos.io/?<script>alert(1)</script>" -v
# SQL injection -> 403
curl "https://test.ohmytalos.io/?username=1'%20or%20'1'%20=%20'1&amp;password=1'%20or%20'1'%20=%20'1" -v

Conclusion

Voilà pour la partie Ingress, qui n’est pas si triviale que ça quand il s’agit d’avoir quelque chose un minimum sérieux. Vous avez désormais un Ingress public robuste, sécurisé et prêt à être pleinement monitoré, ainsi qu’un accès privé aux services internes via Tailnet. Le tout avec des certificats TLS valides et renouvelés automatiquement.

Pas mal non ? Il faut maintenant s’occuper des backups et de la mise en place des clusters de base de données pour nos futures applications, en nous appuyant sur les opérateurs installés lors de la section précédente. Suite à la prochaine section.