↓ Aller au contenu
Mon homelab de zéro (Partie 13) : DNS, certificats et ingress, enfin un cadenas vert
Photo by Zaqy Al Fattah / Unsplash
  1. Articles/

Mon homelab de zéro (Partie 13) : DNS, certificats et ingress, enfin un cadenas vert

·3375 mots·16 mins·
Sommaire
Mon homelab de zéro - Cet article fait partie d'une série.
Partie 13: Cet article

La partie 12 nous a laissés avec un cluster Kubernetes complet : Talos, Cilium, Longhorn, ArgoCD, trois nœuds Ready et une boucle GitOps qui tourne. Joli. Sauf qu’en pratique, pour ouvrir l’interface d’ArgoCD, il fallait un kubectl port-forward et une adresse IP. Un cluster en place, mais pas franchement utilisable.

L’objectif de cette partie tient en une ligne : taper https://argocd.ktw.ovh dans un navigateur et obtenir un cadenas vert, sans rien exposer sur Internet.

Un mot sur ce qui n’est pas là
#

Si vous avez lu la fin de la partie 12, vous attendiez autre chose. J’y annonçais pour cette partie 13 un programme copieux : Technitium, les certificats, Traefik, mais aussi Authentik pour le SSO, Netbird pour l’accès distant et toute l’observabilité (Prometheus, Grafana, Loki). Je vous dois une explication.

En posant la liste à plat, cette phase avait accumulé tous les reports des précédentes : DNS, ACME, ingress, SSO, VPN, métriques, logs, supervision externe, sauvegardes. Pas une phase, trois. Et surtout, un constat un peu gênant : j’avais construit une infrastructure intéressante mais… qui n’hébergeait encore rien. Tout dérouler d’un bloc, c’était repartir pour des semaines sans jamais servir un vrai service.

J’ai donc redécoupé :

  • ici : DNS, certificats et ingress, le socle minimal pour utiliser ce qui existe déjà
  • ensuite : l’identité et l’accès
  • puis : l’observabilité

Le teaser de la partie 12 était donc trop optimiste.

Quatre décisions avant de commencer
#

Retour à l’apply en local
#

En partie 12, je me félicitais d’appliquer enfin depuis la CI, “plus depuis mon poste”. Ça n’a pas duré. Pendant la construction, le pipeline s’est révélé trop lent : 17 minutes sur un simple téléchargement d’images, sans le moindre retour intermédiaire et un débogage à travers des logs de CI bien moins confortable qu’un terminal.

J’ai donc fait machine arrière : apply en local tant que je construis, pipeline quand le code se stabilise. Le pipeline ne disparaît pas pour autant : le plan en MR reste un test de portabilité, les garde-fous sur les secrets restent en place et le job apply manuel est conservé.

Technitium dans un LXC dédié, hors de Kubernetes
#

Le DNS interne, j’avais choisi Technitium dès la partie 5 pour son API complète et son split-horizon natif. Restait à savoir où le faire tourner. Trois options : un LXC dédié, dans le cluster Kubernetes ou sur Dokploy.

J’ai retenu le LXC dédié (ct:210, sur pve03, en .40) pour une raison simple : les briques dont on a besoin quand ça va mal ne doivent pas dépendre de ce qui peut mal aller. Un DNS dans le cluster crée une dépendance circulaire : si Kubernetes tombe, on perd la résolution de tout, y compris des outils qui servent à diagnostiquer la panne. C’est le même raisonnement que celui que j’avais tenu en partie 5 pour le serveur de sauvegarde et la supervision, prévus hors du cluster.

Et contrairement aux VM Talos de la partie 12, ce conteneur est sous HA. Aucune contradiction : Technitium est un service singleton sans mécanisme de résilience propre, pas un membre etcd qui risquerait de revenir avec une vision périmée du cluster. Le HA Proxmox est exactement le bon outil, avec le patron déjà rodé sur le runner et Dokploy en partie 11.

Un LoadBalancer Cilium plutôt qu’un hostPort
#

Traefik a besoin d’une IP joignable depuis le LAN. Cilium sait attribuer des adresses aux services de type LoadBalancer et les annoncer en ARP (le protocole qui, sur un réseau local, associe une IP à une adresse MAC) : je lui réserve la plage .60 à .69. L’alternative, un hostPort sur les nœuds, aurait fait pointer le DNS vers un nœud précis et donc cassé la résilience qu’on vient de passer une partie entière à construire.

Deux ingress, assumés
#

Dokploy embarque déjà son propre Traefik, qui écoute sur les ports 80 et 443 de sa VM (on l’avait vu en partie 11). Déployer un Traefik dans Kubernetes crée donc deux points d’entrée distincts, un par étage d’orchestration. Ce n’est pas gênant : des IP différentes, des responsabilités séparées. Mais il faut en être conscient au moment de créer les enregistrements DNS.

Étape 1 : Technitium
#

Le conteneur
#

Rien de neuf sous le soleil côté OpenTofu : un LXC, sa réplication vers les deux autres nœuds, sa déclaration HA et sa règle d’affinité. C’est le patron de la partie 11, appliqué une troisième fois :

# tofu/stacks/core/technitium.tf (extrait)
resource "proxmox_virtual_environment_container" "technitium" {
  node_name = var.technitium_node
  vm_id     = var.technitium_vm_id

  unprivileged  = true
  start_on_boot = true

  initialization {
    hostname = "dns01"
    # ... (IP, passerelle, clé SSH)
  }

  # ... (1 cœur, 1 Go de RAM, 8 Go de disque, template Debian)

  lifecycle {
    ignore_changes = [node_name, started]
  }
}

# ... (réplication vers les deux autres nœuds, déclaration HA)

resource "proxmox_virtual_environment_harule" "technitium" {
  rule = "dns-home"
  type = "node-affinity"

  resources = [proxmox_virtual_environment_haresource.technitium.resource_id]
  nodes     = { (var.technitium_node) = 100 }

  strict  = false
  comment = "Managed by OpenTofu"
}

Vous reconnaissez le ignore_changes = [node_name, started] et le strict = false : ce sont les deux leçons de la partie 11, qui s’appliquent désormais sans discussion à tout ce qui passe sous HA.

L’installation
#

Pour installer Technitium, même raisonnement que pour Dokploy en partie 11 : le script officiel, encadré par Ansible, qui ne le relance que si le logiciel est absent. Le rôle teste donc la présence d’un fichier avant d’agir :

# ansible/roles/technitium/tasks/main.yml (extrait)
- name: Check whether Technitium is installed
  ansible.builtin.stat:
    path: /opt/technitium/dns/DnsServerApp.dll
  register: technitium_installed

La zone et les enregistrements, par API
#

Un second rôle s’authentifie ensuite sur l’API de Technitium, puis crée la zone primaire ktw.ovh et ses enregistrements A via le module uri. Tous les paramètres passent dans le corps de la requête plutôt que dans une URL construite à la main :

# ansible/roles/technitium_config/tasks/main.yml (extrait)
- name: Authenticate
  ansible.builtin.uri:
    url: "{{ technitium_config_api_url }}/user/login"
    method: POST
    body_format: form-urlencoded
    body:
      user: "{{ technitium_config_admin_user }}"
      pass: "{{ technitium_config_admin_password }}"
      includeInfo: "false"
    return_content: true
  register: technitium_config_auth
  changed_when: false
  no_log: true
  failed_when: technitium_config_auth.json.status != "ok"

# ... (forwarders, liste et création de la zone)

- name: Add A records
  ansible.builtin.uri:
    url: "{{ technitium_config_api_url }}/zones/records/add"
    method: POST
    body_format: form-urlencoded
    body:
      token: "{{ technitium_config_token }}"
      domain: "{{ item.name }}.{{ technitium_config_zone }}"
      zone: "{{ technitium_config_zone }}"
      type: A
      ttl: "300"
      overwrite: "true"
      ipAddress: "{{ item.ip }}"
    return_content: true
  loop: "{{ technitium_config_records }}"
  register: technitium_config_record_add
  changed_when: true
  failed_when: technitium_config_record_add.json.status != "ok"

Avec body_format: form-urlencoded, c’est Ansible qui se charge de l’encodage : un caractère spécial dans un mot de passe ne peut pas casser la requête, comme il le ferait dans une URL assemblée à la main.

Les enregistrements eux-mêmes sont une simple liste dans les variables du rôle :

# ansible/roles/technitium_config/defaults/main.yml (extrait)
technitium_config_forwarders:
  - 1.1.1.1
  - 9.9.9.9

technitium_config_records:
  - { name: pve01, ip: 192.168.3.11 }
  - { name: pve02, ip: 192.168.3.12 }
  - { name: pve03, ip: 192.168.3.13 }
  - { name: pbs01, ip: 192.168.3.20 }
  - { name: dns01, ip: 192.168.3.40 }
  - { name: runner01, ip: 192.168.3.50 }
  - { name: dokploy, ip: 192.168.3.51 }
  - { name: talos01, ip: 192.168.3.52 }
  - { name: talos02, ip: 192.168.3.53 }
  - { name: talos03, ip: 192.168.3.54 }
  - { name: k8s, ip: 192.168.3.55 }

Le split-horizon, concrètement : une zone primaire locale rend Technitium autoritaire sur ktw.ovh. Il répond lui-même pour tout ce domaine, sans jamais demander à Internet. La conséquence est à assumer : tout nom sous ktw.ovh qui n’est pas déclaré localement renvoie NXDOMAIN, même s’il existe publiquement. Le domaine étant dédié au lab, c’est acceptable, mais c’est une conséquence à garder en tête pour les certificats.

Résultat
#

$ dig @192.168.3.40 pve01.ktw.ovh +short
192.168.3.11
$ dig @192.168.3.40 k8s.ktw.ovh +short
192.168.3.55
$ dig @192.168.3.40 gitlab.com +short
172.65.251.78

Les noms du lab résolus en privé, le reste d’Internet via les forwarders. Premier morceau du socle en place.

Étape 2 : les certificats
#

Le domaine chez OVH, la zone chez Cloudflare
#

En partie 5, j’annonçais des certificats Let’s Encrypt via le challenge ACME DNS-01 sur l’API OVH. Une précision s’impose : le domaine ktw.ovh est bien acheté chez OVH mais sa zone DNS est servie par Cloudflare :

$ dig +short NS ktw.ovh
curt.ns.cloudflare.com.
khloe.ns.cloudflare.com.
note

Registrar et hébergeur DNS sont deux choses différentes. Le registrar, c’est chez qui vous avez acheté le nom. L’hébergeur DNS, c’est qui répond aux requêtes pour ce nom et c’est lui seul qui compte pour un challenge ACME DNS-01 : c’est dans sa zone que l’enregistrement TXT de preuve doit apparaître. Les deux coïncident souvent, mais pas toujours.

C’est donc Cloudflare que cert-manager doit piloter et c’est une bonne nouvelle : cert-manager le prend en charge nativement. Pas de webhook externe à installer (ce qu’OVH aurait demandé), moins de pièces mobiles, un code maintenu par le projet lui-même.

Il faut deux objets : un secret qui porte le token API Cloudflare et un ClusterIssuer, l’émetteur de certificats utilisable depuis n’importe quel namespace :

# tofu/stacks/core/certmanager.tf (extrait)
resource "kubernetes_secret" "cloudflare_token" {
  metadata {
    name      = "cloudflare-api-token"
    namespace = kubernetes_namespace.cert_manager.metadata[0].name
  }

  data = {
    "api-token" = var.cloudflare_api_token
  }

  depends_on = [helm_release.cert_manager]
}

resource "kubernetes_manifest" "letsencrypt" {
  manifest = {
    apiVersion = "cert-manager.io/v1"
    kind       = "ClusterIssuer"
    metadata = {
      name = "letsencrypt"
    }
    spec = {
      acme = {
        email  = var.acme_email
        server = "https://acme-v02.api.letsencrypt.org/directory"
        privateKeySecretRef = {
          name = "letsencrypt-account-key"
        }
        solvers = [{
          dns01 = {
            cloudflare = {
              apiTokenSecretRef = {
                name = kubernetes_secret.cloudflare_token.metadata[0].name
                key  = "api-token"
              }
            }
          }
        }]
      }
    }
  }

  depends_on = [kubernetes_secret.cloudflare_token]
}

Le token Cloudflare suit le même chemin que tous les autres secrets de la série : rangé dans SOPS, exporté en TF_VAR_cloudflare_api_token par tofu/env.sh, jamais écrit en clair dans le code.

Vérifier la propagation auprès de résolveurs publics
#

Avant de demander le certificat à Let’s Encrypt, cert-manager vérifie lui-même que l’enregistrement TXT du challenge est bien visible. Par défaut, il passe pour ça par la résolution DNS du cluster. Or ici, les pods résolvent les noms en ktw.ovh via Technitium : CoreDNS, le DNS interne de Kubernetes, lui délègue la zone pour que les services puissent joindre les machines du lab.

Vous vous souvenez de la phrase de l’étape 1 ? Tout nom sous ktw.ovh non déclaré localement renvoie NXDOMAIN. Technitium ne connaît pas les enregistrements éphémères du challenge, qui n’existent que chez Cloudflare. Sans réglage particulier, cert-manager attendrait donc indéfiniment un TXT qui, de son point de vue, n’existe pas.

La solution : forcer cert-manager à vérifier la propagation auprès de résolveurs publics et uniquement eux :

# tofu/stacks/core/certmanager.tf (extrait)
resource "helm_release" "cert_manager" {
  name       = "cert-manager"
  repository = "https://charts.jetstack.io"
  chart      = "cert-manager"
  version    = var.cert_manager_version
  namespace  = kubernetes_namespace.cert_manager.metadata[0].name

  timeout = 600

  values = [yamlencode({
    crds = { enabled = true }

    dns01RecursiveNameservers     = "1.1.1.1:53,8.8.8.8:53"
    dns01RecursiveNameserversOnly = true
  })]

  depends_on = [helm_release.cilium]
}
note

Split-horizon et DNS-01 : vérifier la propagation depuis l’extérieur. Un DNS interne autoritaire sur le domaine ne voit pas les enregistrements publics du challenge. dns01RecursiveNameserversOnly existe précisément pour ça. Si vous montez la même architecture, posez ce réglage d’emblée.

Résultat : un certificat délivré en 32 secondes.

subject=CN=*.ktw.ovh
issuer=C=US, O=Let's Encrypt, CN=YR2
notAfter=Nov  1 19:46:53 2026 GMT

Un certificat wildcard (*.ktw.ovh), valable 90 jours et renouvelé automatiquement 30 jours avant l’échéance. Et c’est toute la raison d’être de cette chaîne : un wildcard ne peut s’obtenir que par DNS-01, jamais par HTTP-01. C’est aussi ce qui permet d’avoir des certificats valides sans qu’aucun service ne soit joignable depuis Internet, comme promis en partie 5.

Étape 3 : le LoadBalancer Cilium
#

Pour que Traefik ait une adresse sur le LAN, il faut apprendre à Cilium deux choses : quelles adresses il peut distribuer et comment les annoncer. Deux objets Cilium, déclarés dans le même stack :

# tofu/stacks/core/cilium_lb.tf
resource "kubernetes_manifest" "cilium_ip_pool" {
  manifest = {
    apiVersion = "cilium.io/v2alpha1"
    kind       = "CiliumLoadBalancerIPPool"
    metadata = {
      name = "lab-pool"
    }
    spec = {
      blocks = [{
        start = var.lb_pool_start
        stop  = var.lb_pool_stop
      }]
    }
  }

  depends_on = [helm_release.cilium]
}

resource "kubernetes_manifest" "cilium_l2_policy" {
  manifest = {
    apiVersion = "cilium.io/v2alpha1"
    kind       = "CiliumL2AnnouncementPolicy"
    metadata = {
      name = "lab-l2"
    }
    spec = {
      interfaces      = ["eth0"]
      externalIPs     = true
      loadBalancerIPs = true
    }
  }

  depends_on = [helm_release.cilium]
}

Le pool va de 192.168.3.60 à 192.168.3.69 et la politique d’annonce publie ces adresses en ARP sur eth0. Il faut aussi activer la fonctionnalité dans le chart Cilium lui-même. Ces lignes s’ajoutent aux valeurs montrées en partie 12.

# tofu/stacks/core/cilium.tf (ajout aux valeurs Helm)
    l2announcements = {
      enabled = true
    }

    k8sClientRateLimit = {
      qps   = 50
      burst = 200
    }

Le relèvement de k8sClientRateLimit n’est pas décoratif : les annonces L2 reposent sur une élection du nœud annonceur, via des leases Kubernetes et génèrent beaucoup d’appels à l’API.

Redémarrer les pods Cilium
#

Une subtilité au passage : la mise à jour du chart modifie bien le ConfigMap de Cilium (on y trouve enable-l2-announcements: true), mais ne recrée pas les pods qui le lisent. Les annonces L2 ne démarrent donc qu’après un redémarrage :

kubectl -n kube-system rollout restart ds/cilium

Pour tester, un simple nginx exposé en LoadBalancer : il obtient 192.168.3.60, un lease cilium-l2announce-default-nginx-test apparaît sur talos02 (le nœud qui a gagné l’élection et porte l’annonce) et curl répond 200.

note

Modifier une configuration Helm ne redémarre pas forcément les pods concernés. Certains charts posent une annotation de checksum pour forcer le redéploiement quand la configuration change, d’autres non. Quand un changement de configuration ne produit aucun effet, le premier réflexe est de vérifier que les pods ont bien été recréés.

Le ping ne prouve rien
#

Une précision utile : ping 192.168.3.60 ne répond pas et c’est normal. Aucune interface ne porte réellement cette IP. Cilium annonce une entrée ARP qui attire le trafic vers un nœud, puis son load balancer eBPF ne traite que le TCP à destination des ports exposés. L’ICMP n’est pas de son ressort.

note

Ne diagnostiquez jamais un service LoadBalancer L2 au ping. C’est un comportement commun à ce type de load balancer (MetalLB fait pareil). Le seul test qui vaille, c’est une connexion sur le port réel.

Étape 4 : Traefik
#

Un point d’entrée unique sur .60, le certificat wildcard servi par défaut et une redirection HTTP vers HTTPS systématique. D’abord le certificat :

# tofu/stacks/core/traefik.tf (extrait)
resource "kubernetes_manifest" "traefik_wildcard" {
  manifest = {
    apiVersion = "cert-manager.io/v1"
    kind       = "Certificate"
    metadata = {
      name      = "wildcard"
      namespace = kubernetes_namespace.traefik.metadata[0].name
    }
    spec = {
      secretName = "wildcard-tls"
      issuerRef = {
        name = "letsencrypt"
        kind = "ClusterIssuer"
      }
      commonName = "*.${var.lab_domain}"
      dnsNames = [
        var.lab_domain,
        "*.${var.lab_domain}",
      ]
    }
  }

  depends_on = [kubernetes_manifest.letsencrypt]
}

Les secrets Kubernetes vivent dans un namespace. Plutôt que de copier à la main le secret du certificat obtenu à l’étape 2, Traefik déclare son propre Certificate, que cert-manager émet directement dans le namespace traefik. Puis le chart :

# tofu/stacks/core/traefik.tf (extrait)
resource "helm_release" "traefik" {
  name       = "traefik"
  repository = "https://traefik.github.io/charts"
  chart      = "traefik"
  version    = var.traefik_version
  namespace  = kubernetes_namespace.traefik.metadata[0].name

  timeout = 600

  values = [yamlencode({
    service = {
      type = "LoadBalancer"
      annotations = {
        "lbipam.cilium.io/ips" = var.traefik_ip
      }
    }

    ports = {
      web = {
        http = {
          redirections = {
            entryPoint = {
              to     = "websecure"
              scheme = "https"
            }
          }
        }
      }
    }

    tlsStore = {
      default = {
        defaultCertificate = {
          secretName = "wildcard-tls"
        }
      }
    }

    # ... (dashboard désactivé, providers CRD et Ingress activés)
  })]

  depends_on = [
    kubernetes_manifest.traefik_wildcard,
    kubernetes_manifest.cilium_ip_pool,
  ]
}

L’annotation lbipam.cilium.io/ips fige l’adresse sur 192.168.3.60. Sans elle, Cilium en piocherait une au hasard dans la plage et le DNS pointerait dans le vide au premier redéploiement.

Deux subtilités du chart dans ces values : la redirection HTTP vers HTTPS se déclare sous web.http.redirections.entryPoint et le TLS de websecure est activé par défaut, inutile de le demander. Le chart valide d’ailleurs ses values contre un schéma JSON : une propriété mal placée est refusée avant tout déploiement, avec son nom exact.

Le wildcard DNS
#

Côté Technitium, une seule ligne ajoutée à la liste de l’étape 1 :

# ansible/roles/technitium_config/defaults/main.yml (ajout)
  - { name: "*", ip: 192.168.3.60 }

Douze enregistrements au total et c’est celui-ci qui change tout au quotidien : ajouter un service Kubernetes ne demande plus aucune modification DNS. On déclare la route, le nom résout tout seul. Et les deux ingress de tout à l’heure cohabitent sans heurt : dokploy.ktw.ovh a son propre enregistrement explicite vers .51, qui l’emporte sur le wildcard.

Les routes
#

Trois services à exposer pour commencer : ArgoCD, l’interface de Longhorn et Hubble. Une IngressRoute Traefik pour chacun, générée depuis une seule liste :

# tofu/stacks/core/ingress.tf
locals {
  ingress_routes = {
    argocd = {
      namespace = "argocd"
      service   = "argo-cd-argocd-server"
      port      = 80
    }
    longhorn = {
      namespace = "longhorn-system"
      service   = "longhorn-frontend"
      port      = 80
    }
    hubble = {
      namespace = "kube-system"
      service   = "hubble-ui"
      port      = 80
    }
  }
}

resource "kubernetes_manifest" "ingress_routes" {
  for_each = local.ingress_routes

  manifest = {
    apiVersion = "traefik.io/v1alpha1"
    kind       = "IngressRoute"
    metadata = {
      name      = each.key
      namespace = each.value.namespace
    }
    spec = {
      entryPoints = ["websecure"]
      routes = [{
        match = "Host(`${each.key}.${var.lab_domain}`)"
        kind  = "Rule"
        services = [{
          name = each.value.service
          port = each.value.port
        }]
      }]
      tls = {}
    }
  }

  depends_on = [helm_release.traefik]
}

Le tls = {} vide n’est pas un oubli : il indique à Traefik de terminer le TLS avec le certificat par défaut du tlsStore, donc le wildcard. C’est aussi là que se referme une boucle ouverte en partie 12 : ArgoCD tournait en server.insecure, “parce que le TLS sera terminé par l’ingress”. Nous y sommes : c’est Traefik qui présente le certificat et ArgoCD reste en HTTP derrière lui, sur le port 80 de son service. Même chose pour Hubble, activé en partie 12 et enfin accessible autrement que par un port-forward.

Le test final
#

curl -s -o /dev/null -w '%{http_code}\n' https://argocd.ktw.ovh

Notez l’absence de -k. Sans cette option, curl refuse tout certificat qu’il ne peut pas valider. S’il accepte la connexion, c’est que toute la chaîne fonctionne : le DNS résout le nom vers .60, Cilium annonce l’adresse, Traefik répond et présente un certificat Let’s Encrypt valide pour ce nom. C’est le vrai test de bout en bout.

Puis, pour le plaisir, dans le navigateur : https://argocd.ktw.ovh, cadenas vert, aucun avertissement. Objectif de la partie atteint.

L’état des lieux après cette phase
#

ÉlémentÉtat
Technitium (ct:210)pve03, HA + affinité dns-home, split-horizon sur ktw.ovh
Enregistrements DNS12 dont le wildcard, gérés par API depuis Ansible
cert-managerDNS-01 via Cloudflare (support natif), résolveurs publics forcés
Certificat *.ktw.ovhLet’s Encrypt, 90 jours, renouvellement automatique
Cilium LoadBalancerplage .60 à .69, annonces L2 sur eth0
Traefik.60, wildcard par défaut, HTTP redirigé vers HTTPS
Services exposésargocd, longhorn, hubble en HTTPS

Les services du cluster se consultent désormais avec un nom et un cadenas, plus avec une IP et un port-forward.

Les points encore ouverts
#

  • Mon poste résout tout via Technitium, configuré en dur. Si le conteneur tombe (le temps que le HA le relance ailleurs), je perds toute résolution, Internet compris. Deux pistes : que le DHCP du réseau distribue Technitium en premier et un résolveur public en second, ou un second Technitium en réplica.
  • Un compte local de plus. Le mot de passe administrateur de Technitium vit dans SOPS, en attendant qu’un SSO le remplace.
  • Une dette mineure d’idempotence. Les tâches Add A records ont changed_when: true en dur et rapportent donc un changement à chaque passage. Sans conséquence réelle, l’API travaillant en overwrite=true, mais ce n’est pas idempotent au sens strict.

Et maintenant ?
#

Des noms qui résolvent et des certificats valides : c’est exactement ce qui manquait pour la suite. Mais d’abord, une pause côté matériel : la prochaine partie quitte le code pour le plastique, avec le rack 10 pouces que j’ai imprimé en 3D pour ranger tout ce petit monde. Ensuite viendront l’identité et l’accès : Authentik pour le SSO (Single Sign-On, une authentification unique pour tous les services) en OIDC, puis Netbird auto-hébergé pour l’accès distant, avec la question de poule et d’œuf évoquée en partie 12 à trancher enfin. Cette fois, je me garderai bien de promettre davantage.

À bientôt !

Kentrow
Auteur
Kentrow
Partage de notes et astuces IT : réseaux, serveurs, DevOps, sécurité, homelab et plus encore.
Mon homelab de zéro - Cet article fait partie d'une série.
Partie 13: Cet article

Articles connexes