hoortrend.com

Documentation · version du 21 octobre 2026

Documentation · as of 21 October 2026

La documentation du lab

The lab documentation

Un petit cluster Kubernetes à deux nœuds, quelques services qui tournent à côté, et un seul point d'entrée HTTPS. Cette page décrit chaque pièce, pourquoi elle est là, et comment l'exploiter.

A small two-node Kubernetes cluster, a few services running beside it, and a single HTTPS entry point. This page describes each piece, why it is there, and how to operate it.

Vue d'ensembleOverview

Le cluster est installé avec Kubespray (Kubernetes 1.35) sur deux machines Debian :

  • master1 : le plan de contrôle (API, etcd, scheduler). Il est protégé par un taint, donc aucune charge applicative n'y tourne.
  • worker1 : 4 vCPU, 16 Go de RAM. Il exécute toutes les applications, le contrôleur d'ingress, et héberge aussi deux services installés directement sur la machine : GitLab et Zabbix.

Les briques de base : Calico pour le réseau, ingress-nginx pour l'entrée HTTP(S), cert-manager pour les certificats, kube-prometheus-stack pour la supervision, Rancher pour la gestion, et le registry de GitLab pour les images. Les versions et le rôle de chaque brique sont sur la page Stack du portfolio.

The cluster is installed with Kubespray (Kubernetes 1.35) on two Debian machines:

  • master1: the control plane (API, etcd, scheduler). It carries a taint, so no application workload runs there.
  • worker1: 4 vCPU, 16 GB RAM. It runs every application and the ingress controller, and also hosts two services installed directly on the machine: GitLab and Zabbix.

The building blocks: Calico for networking, ingress-nginx for HTTP(S) entry, cert-manager for certificates, kube-prometheus-stack for monitoring, Rancher for management, and the GitLab registry for images. Versions and the role of each piece are on the lab's Stack page.

Comment le cluster a été montéHow the cluster was built

Cinq étapes, dans cet ordre, chacune avec l'outil adapté : Kubespray pour Kubernetes lui-même, Helm pour les briques communes du cluster, des paquets sur la machine pour GitLab et Zabbix, et des manifestes versionnés plus la CI pour les applications.

  1. Kubernetes, avec Kubespray (4 octobre 2026). Kubespray, un ensemble de playbooks Ansible, installe Kubernetes 1.35.4 sur les deux machines Debian 11 à partir d'un inventaire : le runtime containerd 2.2, etcd 3.6 (service système sur master1), le plan de contrôle sur master1 (via kubeadm), le réseau Calico 3.31, kube-proxy en mode IPVS, CoreDNS et un cache DNS sur chaque nœud (NodeLocal DNS).
  2. Les modules de base, avec Kubespray (5 octobre). Le même outil ajoute metrics-server (CPU et mémoire des pods), local-path-provisioner (volumes persistants sur le disque du nœud) et cert-manager 1.15 (certificats). Un émetteur Let's Encrypt (ClusterIssuer) est ensuite créé avec kubectl apply.
  3. Les briques du cluster, avec Helm (5 et 12 octobre). Trois charts, chacun dans son namespace et à une version figée (tableau ci-dessous). Les réglages de la supervision sont versionnés dans le dépôt infra et appliqués avec helm upgrade --reuse-values -f.
  4. Les services de la machine (16 octobre). GitLab 17.9 (Omnibus) et Zabbix 6.4 sont installés directement sur worker1 et publiés par l'ingress (voir Services hors Kubernetes).
  5. Les applications, avec des manifestes et la CI (du 6 au 19 octobre). Chaque application a ses manifestes dans son dépôt (k8s/), appliqués une première fois avec kubectl apply. Ensuite, le runner GitLab, installé dans le cluster le 17 octobre, construit, analyse et déploie chaque nouvelle version (voir Construire et déployer).
  6. GitOps avec Argo CD (21 octobre). Argo CD, installé avec Helm, applique lui-même les manifestes de demo-app depuis Git et annule tout écart ; le pipeline garde le choix de l'image (voir GitOps avec Argo CD).

Five steps, in this order, each with the right tool: Kubespray for Kubernetes itself, Helm for the cluster's shared building blocks, packages on the host for GitLab and Zabbix, and versioned manifests plus CI for the applications.

  1. Kubernetes, with Kubespray (4 October 2026). Kubespray, a set of Ansible playbooks, installs Kubernetes 1.35.4 on the two Debian 11 machines from an inventory: the containerd 2.2 runtime, etcd 3.6 (a system service on master1), the control plane on master1 (through kubeadm), Calico 3.31 networking, kube-proxy in IPVS mode, CoreDNS and a DNS cache on each node (NodeLocal DNS).
  2. The base add-ons, with Kubespray (5 October). The same tool adds metrics-server (pod CPU and memory), local-path-provisioner (persistent volumes on the node's disk) and cert-manager 1.15 (certificates). A Let's Encrypt issuer (ClusterIssuer) is then created with kubectl apply.
  3. The cluster's building blocks, with Helm (5 and 12 October). Three charts, each in its own namespace and at a pinned version (table below). The monitoring settings are versioned in the infra repository and applied with helm upgrade --reuse-values -f.
  4. The host services (16 October). GitLab 17.9 (Omnibus) and Zabbix 6.4 are installed directly on worker1 and published through the ingress (see Services outside Kubernetes).
  5. The applications, with manifests and CI (6 to 19 October). Each application keeps its manifests in its repository (k8s/), applied once with kubectl apply. After that, the GitLab runner, installed in the cluster on 17 October, builds, scans and deploys every new version (see Build and deploy).
  6. GitOps with Argo CD (21 October). Argo CD, installed with Helm, applies demo-app's manifests itself from Git and reverts any drift; the pipeline keeps choosing the image (see GitOps with Argo CD).
Release HelmHelm releaseChartCe qu'elle installeWhat it installsInstallée leInstalled on
ingress-nginxingress-nginx 4.15.1Le contrôleur d'entrée HTTP(S) : un DaemonSet sur worker1, branché directement sur les ports 80 et 443 de la machine (hostNetwork), classe d'Ingress par défaut.The HTTP(S) entry controller: a DaemonSet on worker1, bound directly to the host's ports 80 and 443 (hostNetwork), default Ingress class.5 oct. 20265 Oct 2026
monitoringkube-prometheus-stack 91.9.0Prometheus et son opérateur, Alertmanager, Grafana, node-exporter, kube-state-metrics et les règles d'alerte. Six mises à jour depuis, toutes par des fichiers de valeurs versionnés.Prometheus and its operator, Alertmanager, Grafana, node-exporter, kube-state-metrics and the alert rules. Six upgrades since, all through versioned values files.5 oct. 20265 Oct 2026
rancherrancher 2.15.2La console de gestion du cluster, un réplica, certificat Let's Encrypt. Rancher installe lui-même Fleet et Rancher Turtles (quatre releases de plus).The cluster management console, one replica, Let's Encrypt certificate. Rancher itself installs Fleet and Rancher Turtles (four more releases).12 oct. 202612 Oct 2026
argocdargo-cd 10.10.5Argo CD v3.5.5, en GitOps : il synchronise demo-app depuis son dépôt (correction automatique de tout écart fait à la main ; l'image reste posée par le pipeline). Accès administrateur seulement ; ni Dex ni notifications, un exemplaire de chaque composant.Argo CD v3.5.5, for GitOps: it syncs demo-app from its repository (any change made by hand is reverted; the image is still set by the pipeline). Admin access only; no Dex, no notifications, one copy of each component.21 oct. 202621 Oct 2026

Pour le constater sur le cluster :

To see it on the cluster:

kubectl get nodes -o wide     # nœuds, version, runtime / nodes, version, runtime
helm list -A                  # releases Helm / Helm releases
ls /etc/kubernetes/addons/    # modules Kubespray / Kubespray add-ons (master1)

Pas encore dans Git : l'inventaire Kubespray et les valeurs Helm d'ingress-nginx et de Rancher. Les y mettre est la prochaine étape pour pouvoir reconstruire le cluster à l'identique depuis les dépôts.

Not in Git yet: the Kubespray inventory and the Helm values of ingress-nginx and Rancher. Putting them there is the next step towards rebuilding the cluster identically from the repositories.

Domaines et servicesDomains and services

Tous les noms pointent vers l'IP publique du worker. Aucun port n'est à saisir : tout passe par 443.

Every name points to the worker's public IP. No port to type: everything goes through 443.

DomaineCe que c'estWhat it isOù ça tourneWhere it runs
hoortrend.comCette page d'accueil, sa documentation et la page « Comment ça marche »This home page, its documentation and the “How it works” pageKubernetes · ns landing · 2 pods nginx
lab.hoortrend.comLe lab en direct (FastAPI + JavaScript) : architecture, état du cluster, observabilité, manifestes, bac à sable, notifications. Projet personnel.The live lab (FastAPI + JavaScript): architecture, cluster state, observability, manifests, sandbox, notifications. Personal project.Kubernetes · ns lab · 2 pods lab-web + 2 pods notify-api
portfolio.ilyes-litim.frLe portfolio professionnel et sa page de contactThe professional portfolio and its contact pageKubernetes · ns portfolio · 2 pods portfolio-web + 2 pods contact-api
grafanaTableaux de bord PrometheusPrometheus dashboardsKubernetes · ns monitoring
rancherConsole de gestion du clusterCluster management consoleKubernetes · ns cattle-system
argocdArgo CD : synchronisation GitOps de demo-app (administrateur seulement)Argo CD: GitOps sync of demo-app (admin only)Kubernetes · ns argocd
gitlabForge Git et CI/CD (GitLab 17.9, installation Omnibus)Git forge and CI/CD (GitLab 17.9, Omnibus install)Machine worker1 · via upstreamworker1 host · via upstream
registryRegistre d'images : toutes les images du labContainer registry: every image of the labMachine worker1 · via upstreamworker1 host · via upstream
auth, notes, board, apiLab d'authentification : serveur OAuth 2.0 / OIDC, deux clients, une APIAuthentication lab: OAuth 2.0 / OIDC server, two clients, an APIKubernetes · ns auth-lab
insideLe playground du lab vu de l'intérieur : ce que Kubernetes fait, en direct et chronométréThe lab's playground from the inside: what Kubernetes does, live and timedKubernetes · ns inside · 1 pod
demoApplication de démonstration : montre qu'une application déployée par la CI fonctionne et répond (pod, nœud, version)Demonstration application: shows that an application deployed by the CI works and answers (pod, node, version)Kubernetes · ns demo-app · 2 pods
zabbixSupervision Zabbix 6.4Zabbix 6.4 monitoringMachine worker1 · via upstreamworker1 host · via upstream

Le www.hoortrend.com n'existe pas : seul le domaine nu est configuré.

www.hoortrend.com does not exist: only the bare domain is configured.

Le portfolio a son propre domaine, ilyes-litim.fr : son enregistrement A pointe vers le même worker et son certificat Let's Encrypt est obtenu comme les autres. L'ancien nom portfolio.hoortrend.com redirige de façon permanente vers le nouveau.

The portfolio has its own domain, ilyes-litim.fr: its A record points to the same worker and its Let's Encrypt certificate is obtained like the others. The old name portfolio.hoortrend.com permanently redirects to the new one.

Le chemin d'une requêteRequest path

DNS, ingress-nginx (qui termine le TLS et choisit la destination d'après le nom d'hôte), Service, puis pod. Pour les services hors cluster, l'ingress envoie directement vers la machine (section suivante). Le détail, avec un schéma et un test en direct, est sur la page Comment ça marche.

DNS, ingress-nginx (which terminates TLS and picks the destination from the host name), Service, then pod. For services outside the cluster, the ingress sends straight to the host (next section). The detail, with a diagram and a live test, is on the How it works page.

Le formulaire de contactThe contact form

La page /contact du portfolio envoie le message à POST /api/contact, sur le même domaine. L'ingress du portfolio route ce seul chemin vers un petit service Python (contact-api) qui valide les champs puis remet le message directement au serveur de messagerie de ma boîte (celui de hoortrend.com, en STARTTLS avec certificat vérifié), sans compte SMTP ni mot de passe. Le message n'est stocké nulle part.

L'adresse d'envoi, contact@ilyes-litim.fr, passe le contrôle SPF : l'enregistrement SPF de ilyes-litim.fr autorise l'adresse de worker1, celle que voit le serveur de réception. Aucun secret n'est donc nécessaire. Le service reste séparé du site exprès : il reçoit les données de n'importe quel visiteur et n'a aucun jeton Kubernetes, alors que portfolio-web parle à l'API Kubernetes.

Protections : champ piège contre les robots, vérification de l'origine, limite de débit (par visiteur et pour tout le site), taille maximale, refus des retours à la ligne dans les en-têtes (injection), et un journal qui ne contient jamais le nom, l'adresse ni le message.

The portfolio's /contact page posts the message to POST /api/contact on the same domain. The portfolio's ingress routes only that path to a small Python service (contact-api) which validates the fields and hands the message straight to my mailbox's mail server (hoortrend.com's, over STARTTLS with a checked certificate), with no SMTP account or password. The message is stored nowhere.

The sending address, contact@ilyes-litim.fr, passes the SPF check: the SPF record of ilyes-litim.fr allows worker1's address, the one the receiving server sees. So no secret is needed. The service stays separate from the site on purpose: it takes input from any visitor and has no Kubernetes token, while portfolio-web talks to the Kubernetes API.

Protections: a honeypot field against robots, origin check, rate limits (per visitor and site-wide), a size cap, rejection of line breaks in headers (injection), and a log that never contains the name, address or message.

Services hors KubernetesServices outside Kubernetes

GitLab et Zabbix sont installés directement sur worker1. Ils ne peuvent pas écouter sur 80 et 443, déjà pris par l'ingress. Chacun écoute donc sur un port interne, lié à une adresse privée de la boucle locale (jamais à l'adresse publique), et l'ingress joue le rôle de reverse proxy avec upstream :

GitLab and Zabbix are installed directly on worker1. They cannot listen on 80 and 443, which the ingress already owns. Each one listens on an internal port, bound to a private loopback address (never the public one), and the ingress acts as a reverse proxy with an upstream:

ServicePort interneInternal portParticularitéNote
GitLab8929external_url sans port, nginx Omnibus en listen_port 8929external_url without a port, Omnibus nginx on listen_port 8929
Registry5443Même principe pour registry_nginxSame principle for registry_nginx
Zabbix8444Site nginx dédié + PHP-FPM ; le serveur Zabbix garde le port 10051 pour les agentsDedicated nginx site + PHP-FPM; the Zabbix server keeps port 10051 for agents

Dans Kubernetes, chaque service externe est un Service sans sélecteur, dont l'Endpoints pointe vers la machine. L'Ingress l'utilise ensuite comme n'importe quel autre :

In Kubernetes, each external service is a selector-less Service whose Endpoints points at the host. The Ingress then uses it like any other:

# namespace external-hosts
apiVersion: v1
kind: Service
metadata: {name: gitlab, namespace: external-hosts}
spec: {ports: [{name: https, port: 8929, targetPort: 8929}]}
---
kind: Endpoints
metadata: {name: gitlab, namespace: external-hosts}
subsets: [{addresses: [{ip: 10.255.255.1}], ports: [{name: https, port: 8929}]}]
---
kind: Ingress
metadata:
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
    nginx.ingress.kubernetes.io/backend-protocol: "HTTPS"
    nginx.ingress.kubernetes.io/proxy-body-size: "0"      # pas de limite sur les push Git
    nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"

Ces ports ne sont donc pas joignables depuis Internet : seul l'ingress, qui tourne sur la même machine, peut les atteindre. L'adresse est créée au démarrage par une unité systemd.

These ports are therefore not reachable from the Internet: only the ingress, running on the same machine, can reach them. The address is created at boot by a systemd unit.

Ajouter un nouveau service de ce type : un Service + Endpoints + Ingress, rien d'autre. Le certificat est demandé tout seul.

Adding another service of this kind takes a Service + Endpoints + Ingress, nothing else. The certificate is requested automatically.

Certificats TLSTLS certificates

Un seul ClusterIssuer, letsencrypt-prod, utilise le challenge HTTP-01. L'annotation cert-manager.io/cluster-issuer sur un Ingress suffit : cert-manager crée le certificat, le stocke dans le Secret nommé par tls.secretName, et le renouvelle avant l'expiration (environ 30 jours avant).

Le certificat vu par les visiteurs est toujours celui de l'ingress. Les services de la machine ont leur propre certificat interne, qui n'est utilisé que pour la liaison entre l'ingress et eux : l'ingress ne le valide pas.

A single ClusterIssuer, letsencrypt-prod, uses the HTTP-01 challenge. The cert-manager.io/cluster-issuer annotation on an Ingress is enough: cert-manager creates the certificate, stores it in the Secret named by tls.secretName, and renews it before expiry (about 30 days ahead).

The certificate visitors see is always the ingress one. The host services have their own internal certificate, used only for the link between the ingress and them: the ingress does not validate it.

kubectl get certificate -A
kubectl -n external-hosts describe certificate gitlab-hoortrend-com-tls

Construire et déployerBuild and deploy

Tout le code vit dans GitLab : le groupe hoortrend (landing, lab, demo-app, inside, sre-demo, infra, auth-lab) et, à part, le groupe ilyes-litim pour le portfolio. Depuis le 19 octobre 2026, seul demo-app est public ; les autres dépôts sont privés. Chaque git push sur main lance un pipeline, et les images sont publiées dans le registry de GitLab.

  1. validate : syntaxe du code, des manifestes et des fichiers référencés par les pages.
  2. build : l'image est construite avec Kaniko dans un pod, sans accès privilégié, puis publiée sous le tag du commit.
  3. scan : Trivy analyse l'image publiée ; une vulnérabilité HIGH ou CRITICAL corrigeable bloque la suite. Dans chaque projet qui construit une image depuis le 19 octobre 2026.
  4. promote : seulement ensuite, l'image reçoit le tag :latest (pour l'image kubectl d'infra, aussi son numéro de version). Les manifestes désignent ce tag ; un contrôle de la CI refuse un tag figé.
  5. deploy : kubectl set image sur le Deployment, puis attente de la fin du déploiement progressif. Pour demo-app, le reste des manifestes est appliqué par Argo CD (voir GitOps avec Argo CD).
  6. verify : un test de fumée interroge le site public.

Sur une merge request, seules les étapes de validation et de build tournent, sans publier ni déployer, et la merge request ne peut être fusionnée que si ce pipeline a réussi (réglage actif sur les six projets depuis le 19 octobre 2026). Avec maxUnavailable: 0, un nouveau pod doit être prêt avant qu'un ancien soit arrêté : le site reste disponible pendant la mise à jour.

Les jobs s'exécutent dans un runner Kubernetes. Son compte de service ne peut changer que l'image d'un seul Deployment nommé par namespace : la CI n'a pas de droits d'administration du cluster. Les pods tirent leurs images avec un jeton en lecture seule.

All code lives in GitLab: the hoortrend group (landing, demo-app, inside, sre-demo, infra, auth-lab) and, separately, the ilyes-litim group for the portfolio. Since 19 October 2026, only demo-app is public; the other repositories are private. Every git push to main starts a pipeline, and images are published to the GitLab registry.

  1. validate: syntax of the code, manifests and the files pages reference.
  2. build: the image is built with Kaniko inside a pod, without privileged access, then published under the commit tag.
  3. scan: Trivy scans the published image; a fixable HIGH or CRITICAL vulnerability stops the rest. In every project that builds an image since 19 October 2026.
  4. promote: only then does the image get the :latest tag (infra's kubectl image also gets its version number). Manifests name that tag; a CI check refuses a frozen tag.
  5. deploy: kubectl set image on the Deployment, then wait for the rolling update to finish. For demo-app, the rest of the manifests is applied by Argo CD (see GitOps with Argo CD).
  6. verify: a smoke test queries the public site.

On a merge request, only validate and build run, without publishing or deploying, and the merge request can only be merged if that pipeline passed (setting on in all six projects since 19 October 2026). With maxUnavailable: 0, a new pod must be ready before an old one is stopped: the site stays up during the update.

Jobs run in a Kubernetes runner. Its service account can only change the image of one named Deployment per namespace: CI has no cluster-admin rights. Pods pull their images with a read-only token.

git push origin main                                  # lance le pipeline / starts the pipeline
kubectl -n landing rollout status deploy/landing-web
kubectl -n landing rollout undo deploy/landing-web   # retour arrière / rollback

GitOps avec Argo CDGitOps with Argo CD

Jusqu'au 20 octobre 2026, les manifestes de chaque application étaient appliqués à la main avec kubectl apply, puis le pipeline ne changeait plus que l'image. Rien ne garantissait que le cluster restait identique au dépôt : une modification faite à la main y restait. Depuis le 21 octobre, Argo CD fait de Git la seule source de vérité pour demo-app : ce qui est dans k8s/ sur main est ce qui tourne.

Le déploiement

Argo CD est installé avec Helm (chart argo/argo-cd 10.10.5, Argo CD v3.5.5) dans le namespace argocd, à partir d'un fichier de valeurs versionné dans le dépôt infra. Choix faits pour le lab :

  • Accès administrateur seulement sur argocd.hoortrend.com : aucun accès anonyme, pas de SSO (Dex désactivé), le compte local admin. Son mot de passe est tiré au hasard ; le cluster n'en garde que l'empreinte bcrypt.
  • TLS à l'entrée : ingress-nginx termine le HTTPS avec un certificat Let's Encrypt de cert-manager, et parle en HTTP à Argo CD dans le cluster (server.insecure). Limites de débit sur l'Ingress.
  • Version allégée : ni notifications, ni contrôleur ApplicationSet, un exemplaire de chaque composant, des limites de ressources, sur worker1 (master1 manque de mémoire). Environ 300 Mio en tout.
helm upgrade --install argocd argo/argo-cd --version 10.10.5 \
  -n argocd --create-namespace -f argocd/values.yaml    # dépôt infra
kubectl apply -f argocd/demo-app.yaml                  # projet + application
kubectl -n argocd get applications                     # SYNC STATUS Synced, HEALTH Healthy

Comment il fonctionne

  1. repo-server clone le dépôt public de demo-app et produit les manifestes du dossier k8s/ à la dernière révision de main.
  2. application-controller compare cet état voulu à l'état réel du cluster, objet par objet. Il le fait toutes les deux à trois minutes (120 s plus un décalage aléatoire de 60 s au plus), et aussitôt qu'un objet suivi change dans le cluster.
  3. S'il y a un écart, il synchronise : il applique les manifestes (automated sync). Un objet retiré du dépôt est supprimé du cluster (prune) ; un objet modifié à la main est remis comme dans le dépôt (self-heal).
  4. Il évalue enfin la santé de chaque objet : un Deployment est « Healthy » quand tous ses pods sont à jour et prêts.
  5. server sert l'interface et l'API (réservées à l'administrateur) ; redis sert de cache aux autres composants.

L'application est rangée dans un projet Argo CD (AppProject) qui limite ce qu'elle peut faire : seulement le dépôt de demo-app, seulement le namespace demo-app, et seulement les types d'objets qu'il contient (Namespace, Deployment, Service, PodDisruptionBudget, Ingress, NetworkPolicy). Un manifeste qui viserait un autre namespace ou créerait un rôle serait refusé.

Argo CD et le pipeline : qui fait quoi

ÉtapeQuiComment
Tests, build, analyse de l'imagePipeline GitLabKaniko, puis Trivy, qui bloque sur une vulnérabilité HIGH ou CRITICAL corrigeable.
Choix de l'image en productionPipeline GitLabkubectl set image avec le tag du commit, après l'analyse. Argo CD ignore ce seul champ (ignoreDifferences, RespectIgnoreDifferences) : les deux ne se battent pas.
Tout le reste des manifestesArgo CDRéplicas, variables, sondes, Service, Ingress, NetworkPolicy… appliqués depuis k8s/ sur main en deux à trois minutes ; tout écart fait à la main est annulé.

Vérifié le 21 octobre 2026 : passer demo-app à 3 réplicas à la main a été annulé en 5 secondes (retour à 2). Un vrai pipeline a ensuite déployé la version 226f67fb : l'application est restée « Synced » et « Healthy », avec cette image.

Pas encore : les autres applications. Leurs dépôts sont privés (Argo CD aurait besoin d'un accès en lecture à GitLab), et le bac à sable du lab est modifié en direct par les visiteurs : il faudrait dire à Argo CD de laisser ces changements.

Until 20 October 2026, each application's manifests were applied by hand with kubectl apply, and the pipeline then only changed the image. Nothing guaranteed that the cluster stayed identical to the repository: a change made by hand stayed there. Since 21 October, Argo CD makes Git the single source of truth for demo-app: what is in k8s/ on main is what runs.

The deployment

Argo CD is installed with Helm (chart argo/argo-cd 10.10.5, Argo CD v3.5.5) in the argocd namespace, from a values file versioned in the infra repository. Choices made for the lab:

  • Admin access only on argocd.hoortrend.com: no anonymous access, no SSO (Dex off), the local admin account. Its password is random; the cluster only keeps its bcrypt hash.
  • TLS at the edge: ingress-nginx terminates HTTPS with a Let's Encrypt certificate from cert-manager, and talks plain HTTP to Argo CD inside the cluster (server.insecure). Rate limits on the Ingress.
  • Pared down: no notifications, no ApplicationSet controller, one copy of each component, resource limits, on worker1 (master1 is short of memory). About 300 MiB in all.
helm upgrade --install argocd argo/argo-cd --version 10.10.5 \
  -n argocd --create-namespace -f argocd/values.yaml    # infra repository
kubectl apply -f argocd/demo-app.yaml                  # project + application
kubectl -n argocd get applications                     # SYNC STATUS Synced, HEALTH Healthy

How it works

  1. repo-server clones demo-app's public repository and renders the manifests of the k8s/ folder at the latest revision of main.
  2. application-controller compares this desired state with the live state of the cluster, object by object. It does so every two to three minutes (120 s plus a random delay of up to 60 s), and as soon as a tracked object changes in the cluster.
  3. If they differ, it syncs: it applies the manifests (automated sync). An object removed from the repository is deleted from the cluster (prune); an object changed by hand is put back as in the repository (self-heal).
  4. Finally it assesses the health of each object: a Deployment is "Healthy" when all its pods are up to date and ready.
  5. server serves the UI and the API (admin only); redis is the other components' cache.

The application belongs to an Argo CD project (AppProject) that limits what it may do: only demo-app's repository, only the demo-app namespace, and only the kinds of objects it contains (Namespace, Deployment, Service, PodDisruptionBudget, Ingress, NetworkPolicy). A manifest aimed at another namespace, or creating a role, would be refused.

Argo CD and the pipeline: who does what

StepWhoHow
Tests, build, image scanGitLab pipelineKaniko, then Trivy, which blocks on a fixable HIGH or CRITICAL vulnerability.
Which image runs in productionGitLab pipelinekubectl set image with the commit tag, after the scan. Argo CD ignores that one field (ignoreDifferences, RespectIgnoreDifferences): the two never fight.
The rest of the manifestsArgo CDReplicas, variables, probes, Service, Ingress, NetworkPolicy… applied from k8s/ on main within two to three minutes; any change made by hand is reverted.

Checked on 21 October 2026: scaling demo-app to 3 replicas by hand was reverted within 5 seconds (back to 2). A real pipeline then deployed version 226f67fb: the application stayed "Synced" and "Healthy", with that image.

Not yet: the other applications. Their repositories are private (Argo CD would need read access to GitLab), and the lab's sandbox is changed live by visitors: Argo CD would have to be told to leave those changes alone.

Fichiers de configurationConfiguration files

Ces fichiers sont copiés tels quels dans l'image au moment du build : ce sont exactement ceux qui font tourner la page d'accueil.

These files are copied as-is into the image at build time: they are exactly what runs the home page.


    
  • cert-manager.io/cluster-issuer demande le certificat Let's Encrypt, émis et renouvelé automatiquement.
  • cert-manager.io/cluster-issuer requests the Let's Encrypt certificate, issued and renewed automatically.
  • ssl-redirect renvoie tout le trafic HTTP vers HTTPS.
  • ssl-redirect sends all HTTP traffic to HTTPS.
  • limit-rps limite le débit par client pour protéger la page.
  • limit-rps rate-limits each client to protect the page.
  • La règle envoie hoortrend.com vers le Service, sans exposer les pods.
  • The rule routes hoortrend.com to the Service without exposing the pods.

Workflow de chaque projetEach project's workflow

Tous les projets suivent le même chemin, du poste du développeur jusqu'au site public. Choisis un projet, puis rejoue son pipeline : les étapes, les jobs et les images sont les vrais, et le .gitlab-ci.yml exact est affiché dessous.

Every project follows the same path, from the developer's machine to the public site. Pick a project, then replay its pipeline: the stages, jobs and images are the real ones, and the exact .gitlab-ci.yml is shown below.

  1. Branche. Le changement est fait sur une branche, jamais directement sur main.
  2. Branch. The change is made on a branch, never directly on main.
  3. Merge request. Le pipeline valide et construit l'image sans la publier : on sait que ça passe avant de fusionner.
  4. Merge request. The pipeline validates and builds the image without publishing it: we know it passes before merging.
  5. Fusion dans main. Le pipeline complet démarre : validation, construction, publication, déploiement.
  6. Merge into main. The full pipeline starts: validate, build, publish, deploy.
  7. Déploiement progressif. Kubernetes remplace les pods un par un, et ne coupe un ancien pod qu'une fois le nouveau prêt.
  8. Rolling update. Kubernetes replaces pods one by one, and only stops an old pod once the new one is ready.
  9. Vérification. Un test de fumée interroge le site public. En cas de problème : rollout undo, retour à la version précédente en quelques secondes.
  10. Verification. A smoke test queries the public site. If something is wrong: rollout undo, back to the previous version in seconds.


  

Le code affiché est une copie du fichier du dépôt, mise à jour avec cette page. Les durées sont l'ordre de grandeur mesuré le 17 octobre 2026 (démarrage du pod du job compris).

The code shown is a copy of the repository file, updated along with this page. Durations are the order of magnitude measured on 17 October 2026 (job pod start-up included).

SécuritéSecurity

Dans les pods

  • Les namespaces landing, playground, auth-lab, inside et demo-app imposent Pod Security « restricted » : le cluster refuse un pod non conforme. Seul gitlab-runner est en « baseline » (le build d'images).
  • Utilisateur non-root, système de fichiers en lecture seule, toutes les capabilities supprimées, profil seccomp.
  • Aucun jeton d'API Kubernetes monté dans les pods qui n'en ont pas besoin.
  • Limites de CPU et de mémoire sur chaque conteneur.

In the pods

  • The landing, playground, auth-lab, inside and demo-app namespaces enforce Pod Security “restricted”: the cluster rejects a non-compliant pod. Only gitlab-runner is “baseline” (image builds).
  • Non-root user, read-only file system, all capabilities dropped, seccomp profile.
  • No Kubernetes API token mounted in pods that do not need one.
  • CPU and memory limits on every container.

Sur le réseau

  • HTTPS partout, redirection automatique depuis HTTP, HSTS.
  • En-têtes de sécurité et Content-Security-Policy stricte sur chaque page.
  • Limitation de débit par client sur l'ingress.
  • Le bac à sable du portfolio est isolé par une NetworkPolicy : ses pods ne peuvent joindre que le strict nécessaire.
  • inside.hoortrend.com ne peut sortir que vers l'API Kubernetes, Prometheus, le port 8080 du bac à sable (sa sonde) et le DNS ; son compte de service n'a que get, list et watch.
  • Pare-feu sur worker1 (depuis le 19 octobre 2026) : sur l'interface publique, seuls SSH, HTTP, HTTPS, OpenVPN et le trafic venant de master1 passent, en IPv4 et en IPv6. Le kubelet, le webhook d'admission, les exporters et les agents de supervision ne répondent plus à Internet. master1 n'a pas encore le sien.

On the network

  • HTTPS everywhere, automatic redirect from HTTP, HSTS.
  • Security headers and a strict Content-Security-Policy on every page.
  • Per-client rate limiting at the ingress.
  • The portfolio sandbox is isolated by a NetworkPolicy: its pods can only reach what they strictly need.
  • inside.hoortrend.com may only reach the Kubernetes API, Prometheus, the sandbox's port 8080 (its probe) and DNS; its service account only has get, list and watch.
  • Firewall on worker1 (since 19 October 2026): on the public interface, only SSH, HTTP, HTTPS, OpenVPN and traffic from master1 get through, over IPv4 and IPv6. The kubelet, the admission webhook, the exporters and the monitoring agents no longer answer the Internet. master1 does not have one yet.

Dans l'API du portfolio

  • Compte de service en lecture seule ; les actions du bac à sable ne visent que son propre namespace, avec une liste d'actions autorisées.
  • Le relais Slack refuse les destinations qui ne sont pas Slack (protection SSRF), limite le débit, et ne stocke ni ne journalise jamais l'URL du webhook.
  • Les notifications par e-mail ne partent qu'à une adresse confirmée par un code à 6 chiffres. Le droit d'envoi est un jeton signé (HMAC) valable 12 heures : rien n'est stocké. Les envois sont limités par adresse et par visiteur, et un serveur de messagerie à adresse privée est refusé.
  • CORS ouvert uniquement à cette origine, pour deux routes en lecture.

Dans le pipeline

  • gitleaks parcourt tout l'historique Git à chaque pipeline : un secret commité, même supprimé ensuite, le fait échouer.
  • Trivy analyse les images avant leur déploiement, dans chaque pipeline qui en construit une. Premier résultat, le 18 octobre 2026 : trois vulnérabilités HIGH dans starlette (dépendance de FastAPI), corrigées avant la mise en place du blocage. Le 19 octobre, trois vulnérabilités HIGH dans le starlette d'alert-webhook, trouvées avant d'activer son scan, ont été corrigées de la même façon (mise à jour de FastAPI).
  • Les exceptions acceptées vont dans .trivyignore, avec leur raison et une date d'expiration : passé cette date, le scan échoue de nouveau. Une seule aujourd'hui : cinq identifiants de l'image kubectl, que la dernière version publiée de kubectl ne corrige pas encore, acceptés jusqu'au 30 novembre 2026 (le cinquième, une faille de la bibliothèque standard de Go, ajouté le 21 octobre). En passant de kubectl 1.35.4 à 1.35.9, cette image est passée de 26 vulnérabilités HIGH corrigeables à ces quatre identifiants.
  • auth-lab a les mêmes contrôles, plus 33 tests qui vérifient chaque attaque que son serveur doit refuser ; inside aussi, avec des tests qui rejouent un enregistrement réel du bac à sable et une vérification que son RBAC reste en lecture seule ; demo-app avec des tests HTTP et une vérification que son manifeste désigne :latest.
  • Les images nginx (hoortrend.com, board) partent d'une base récente, mise à jour au build (apk upgrade) : l'ancienne base portait 53 vulnérabilités HIGH ou CRITICAL.

In the portfolio API

  • Read-only service account; sandbox actions only target their own namespace, from an allow-list of actions.
  • The Slack relay rejects any non-Slack destination (SSRF protection), rate-limits, and never stores or logs the webhook URL.
  • E-mail notifications only go to an address confirmed with a 6-digit code. The right to send is a signed token (HMAC) valid for 12 hours: nothing is stored. Sends are limited per address and per visitor, and a mail server on a private address is refused.
  • CORS opened only to this origin, for two read routes.

In the pipeline

  • gitleaks walks the whole Git history on every pipeline: a committed secret, even removed later, makes it fail.
  • Trivy scans the images before they are deployed, in every pipeline that builds one. First result, on 18 October 2026: three HIGH vulnerabilities in starlette (a FastAPI dependency), fixed before the gate was switched on. On 19 October, three HIGH vulnerabilities in alert-webhook's starlette, found before its scan was switched on, were fixed the same way (FastAPI upgrade).
  • Accepted exceptions go in .trivyignore, with their reason and an expiry date: past that date, the scan fails again. Only one today: five IDs of the kubectl image that the latest published kubectl does not fix yet, accepted until 30 November 2026 (the fifth, a flaw in Go's standard library, added on 21 October). Moving from kubectl 1.35.4 to 1.35.9 took that image from 26 fixable HIGH vulnerabilities down to those four IDs.
  • auth-lab has the same checks, plus 33 tests covering every attack its server must refuse; so does inside, with tests replaying a real recording of the sandbox and a check that its RBAC stays read-only; demo-app with HTTP tests and a check that its manifest names :latest.
  • The nginx images (hoortrend.com, board) start from a recent base, updated at build time (apk upgrade): the old base carried 53 HIGH or CRITICAL vulnerabilities.

Identité et SSOIdentity and SSO

Le lab a son propre fournisseur d'identité, auth.hoortrend.com : un serveur OAuth 2.0 / OpenID Connect écrit à la main pour une formation. Flux authorization code avec PKCE obligatoire, jetons JWT signés RS256 (clés publiées en JWKS, rotation sans coupure), refresh tokens à rotation, double authentification TOTP compatible Google Authenticator, et une session SSO partagée par quatre applications :

The lab has its own identity provider, auth.hoortrend.com: an OAuth 2.0 / OpenID Connect server written by hand for a course. Authorization code flow with mandatory PKCE, RS256-signed JWTs (keys published as a JWKS, rotation without downtime), rotating refresh tokens, TOTP two-factor compatible with Google Authenticator, and one SSO session shared by four applications:

ApplicationType de clientClient typeCe que reçoit un compte SSOWhat an SSO account gets
notesconfidentiel : les jetons restent sur son serveurconfidential: tokens stay on its serverses propres notes, en lecture et écritureits own notes, read and write
boardpublic : application monopage, PKCE seulpublic: single-page app, PKCE onlylecture seule des notes ; une écriture est refusée (403)read-only access to the notes; a write is refused (403)
Grafanaconfidentielconfidentialrôle Viewer, réimposé à chaque connexion, jamais administrateurViewer role, enforced at every login, never administrator
GitLabconfidentielconfidentialcompte externe : les projets publics (demo-app seulement depuis le 19 octobre), comme un visiteurexternal account: the public projects (only demo-app since 19 October), like a visitor

Ce qui empêche de prendre le compte de quelqu'un d'autre :

  • un compte SSO n'est jamais rattaché à un compte existant de Grafana ou de GitLab, ni par son nom ni par son e-mail : il est reconnu par son sub, tiré au hasard à la création ;
  • le serveur ne collecte aucune adresse e-mail. Il fournit une adresse technique en .invalid (domaine réservé par la RFC 6761), unique, qui ne peut correspondre à aucun compte existant ;
  • les noms admin, root, ilyes… sont refusés à l'inscription, pour qu'un visiteur ne puisse pas se faire passer pour l'administrateur ;
  • les comptes de démo sont supprimés après 24 h. Les comptes administrateurs de Grafana et de GitLab gardent leur mot de passe et ne sont pas liés au SSO.

Vérifié le 19 octobre 2026 avec un navigateur piloté, en production : une seule saisie du mot de passe et du code TOTP (sur notes), puis Grafana et GitLab ne demandent que le consentement. Un compte SSO nommé admin a obtenu un compte Grafana distinct, en Viewer ; un compte nommé root est devenu root1 sur GitLab, externe. Dans GitLab, un compte SSO voit les mêmes projets qu'un visiteur ; ouvrir une issue, commenter une merge request et demander l'accès ont été fermés (403). Restent possibles, faute de rôle « lecture seule » pour un compte connecté dans GitLab Free : commenter un commit, mettre une étoile.

Code, tests et formation en 16 chapitres : projet GitLab hoortrend/auth-lab (privé). Point d'entrée technique : /.well-known/openid-configuration.

What stops anyone from taking someone else's account:

  • an SSO account is never linked to an existing Grafana or GitLab account, neither by name nor by e-mail: it is recognised by its sub, drawn at random when it is created;
  • the server collects no e-mail address. It hands out a technical .invalid address (a domain reserved by RFC 6761), unique, that cannot match any existing account;
  • the names admin, root, ilyes… are refused at sign-up, so a visitor cannot pass for the administrator;
  • demo accounts are deleted after 24 h. The Grafana and GitLab administrator accounts keep their password and are not linked to the SSO.

Checked on 19 October 2026 with a driven browser, in production: a single password and TOTP entry (on notes), then Grafana and GitLab only ask for consent. An SSO account named admin got a separate Grafana account, as Viewer; an account named root became root1 on GitLab, external. In GitLab, an SSO account sees the same projects as a visitor; opening an issue, commenting a merge request and requesting access were closed (403). Still possible, since GitLab Free has no read-only role for a signed-in account: commenting a commit, starring a project.

Code, tests and a 16-chapter course: GitLab project hoortrend/auth-lab (private). Technical entry point: /.well-known/openid-configuration.

SupervisionMonitoring

Deux outils complémentaires :

  • Prometheus + Alertmanager + Grafana (kube-prometheus-stack) pour Kubernetes : nœuds, pods, API. Les alertes sont aussi lues par le lab, qui les affiche en direct et peut les relayer vers Slack ou par e-mail.
  • Zabbix pour la machine worker1 et ses services. Les agents parlent au serveur sur le port 10051.

Règles d'alerte : celles fournies par kube-prometheus-stack, plus des règles écrites pour le lab (nœud injoignable, CPU et mémoire des nœuds, pod en boucle de redémarrage, sauvegardes). Les alertes sont visibles dans Alertmanager, Grafana et sur la page Observabilité du lab, et depuis le 19 octobre 2026 Alertmanager les envoie par mail au propriétaire du lab, pour tous les namespaces : une alerte critical tout de suite puis toutes les 4 h tant qu'elle dure, les autres toutes les 24 h, et un mail à la fin. Restent sans envoi : Watchdog (toujours active, elle prouve que la chaîne fonctionne), les alertes info, et KubeCPUOvercommit / KubeMemoryOvercommit, vraies par construction avec un seul worker. Le mail part directement vers le serveur de messagerie du domaine, sans compte SMTP.

kube-proxy n'est plus collecté : Kubespray lie ses métriques à 127.0.0.1, sa cible était en panne depuis l'installation et ses deux alertes sonnaient sans fin.

La sauvegarde nocturne publie l'heure de son dernier succès, lue par node-exporter (collecteur textfile, actif depuis le 19 octobre 2026) : LabBackupStale se déclenche après 30 h sans sauvegarde réussie.

Two complementary tools:

  • Prometheus + Alertmanager + Grafana (kube-prometheus-stack) for Kubernetes: nodes, pods, the API. Alerts are also read by the lab, which shows them live and can relay them to Slack or by e-mail.
  • Zabbix for the worker machine and its services. Agents talk to the server on port 10051.

Alert rules: the ones shipped with kube-prometheus-stack, plus rules written for the lab (node down, node CPU and memory, crash-looping pod, backups). Alerts are visible in Alertmanager, Grafana and on the lab's Observability page, and since 19 October 2026 Alertmanager sends them by mail to the lab owner, for every namespace: a critical alert at once then every 4 h while it lasts, the others every 24 h, and a mail when it ends. Not sent: Watchdog (always firing, it proves the chain works), info alerts, and KubeCPUOvercommit / KubeMemoryOvercommit, true by construction with a single worker. The mail goes straight to the domain's mail server, with no SMTP account.

kube-proxy is no longer scraped: Kubespray binds its metrics to 127.0.0.1, its target had been down since the install and its two alerts fired endlessly.

The nightly backup publishes the time of its last success, read by node-exporter (textfile collector, active since 19 October 2026): LabBackupStale fires after 30 h without a successful backup.

Sauvegarde et repriseBackup and recovery

Un timer systemd sur master1 lance chaque nuit un script versionné dans le dépôt infra. Chaque machine garde une copie des données de l'autre : perdre un nœud ne fait pas perdre ses sauvegardes.

A systemd timer on master1 runs a script, versioned in the infra repository, every night. Each machine keeps a copy of the other one's data: losing a node does not lose its backups.

DonnéeDataMéthodeMethodCopie gardée surCopy kept on
État du clusterCluster statesnapshot etcd + PKI du clusteretcd snapshot + cluster PKImaster1 + worker1
GitLabgitlab-backup (base, dépôts, artefacts) + gitlab-secrets.jsongitlab-backup (database, repositories, artifacts) + gitlab-secrets.jsonmaster1
Zabbixpg_dumpmaster1
  • Rétention : 7 copies de chaque. RPO : 24 h (fréquence quotidienne). RTO : non défini, aucune restauration complète n'a encore été chronométrée.
  • Vérifié le 18 octobre 2026 : snapshot etcd restauré dans un dossier jetable (1 804 clés), archive GitLab complète (base + 43 dépôts), dump Zabbix lisible (186 tables).
  • Pas sauvegardé, volontairement : le registre d'images (chaque image se reconstruit en relançant son pipeline) et les métriques Prometheus (7 jours de rétention).
  • Limite : les deux machines sont chez le même hébergeur, ce n'est pas une copie hors site.
  • Retention: 7 copies of each. RPO: 24 h (daily schedule). RTO: not defined, no full restore has been timed yet.
  • Checked on 18 October 2026: etcd snapshot restored into a scratch directory (1,804 keys), complete GitLab archive (database + 43 repositories), readable Zabbix dump (186 tables).
  • Not backed up, on purpose: the image registry (each image is rebuilt by re-running its pipeline) and Prometheus metrics (7-day retention).
  • Limit: both machines are at the same provider, so this is not an off-site copy.
systemctl list-timers lab-backup.timer
journalctl -u lab-backup --since yesterday
sudo etcdutl snapshot status /var/backups/lab/etcd/etcd-<date>.db -w table

ProcéduresRunbook

Un site ne répond plus

A site stopped answering

kubectl get nodes
kubectl get pods -A | grep -v Running
kubectl -n ingress-nginx logs ds/ingress-nginx-controller --tail=50
kubectl get ingress -A

GitLab ou Zabbix : 502 ou 504

GitLab or Zabbix: 502 or 504

L'ingress est vivant, c'est l'upstream sur la machine qui ne répond pas. Sur worker1 :

The ingress is alive; the upstream on the host is not answering. On worker1:

sudo gitlab-ctl status                 # GitLab : tous les services en "run"
sudo systemctl status nginx php7.4-fpm zabbix-server
ss -ltn | grep -E ':(8929|5443|8444) '

Après un redémarrage de GitLab, la page « Waiting for GitLab to boot » (502) est normale pendant une à deux minutes.

After a GitLab restart, the “Waiting for GitLab to boot” page (502) is normal for a minute or two.

Un certificat ne se renouvelle pas

A certificate does not renew

kubectl get certificate,order,challenge -A
kubectl -n cert-manager logs deploy/cert-manager --tail=50

Cause fréquente : le port 80 ne mène plus à l'ingress, ou l'enregistrement DNS ne pointe plus vers le worker.

Common cause: port 80 no longer reaches the ingress, or the DNS record no longer points to the worker.

Revenir à la version précédente

Roll back to the previous version

Les images sont taguées par commit, donc l'ancienne version est toujours dans le registre. rollout undo remet le ReplicaSet précédent, avec le même déploiement progressif sans coupure. Le pipeline suivant sur main redéploiera le dernier commit : corriger ou annuler le commit fautif par une merge request.

Images are tagged by commit, so the previous version is still in the registry. rollout undo brings back the previous ReplicaSet, with the same zero-downtime rolling update. The next pipeline on main will redeploy the latest commit: fix or revert the faulty commit through a merge request.

kubectl -n portfolio rollout history deploy/portfolio-web
kubectl -n portfolio rollout undo deploy/portfolio-web
kubectl -n portfolio rollout status deploy/portfolio-web

Un pod redémarre en boucle ou est tué (OOMKilled)

A pod restarts in a loop or gets killed (OOMKilled)

kubectl get pods -A | awk '$5 > 0'                 # colonne RESTARTS / RESTARTS column
kubectl -n <ns> describe pod <pod> | grep -A3 'Last State'
kubectl top pod -n <ns> --containers

Reason: OOMKilled : le conteneur a dépassé sa limite mémoire. Comparer avec la consommation réelle avant de remonter la limite, et vérifier qu'il reste de la place sur le nœud (kubectl describe node, section Allocated resources).

Reason: OOMKilled: the container went past its memory limit. Compare with real usage before raising the limit, and check there is room left on the node (kubectl describe node, Allocated resources).

La connexion SSO échoue (Grafana, GitLab, notes, board)

SSO sign-in fails (Grafana, GitLab, notes, board)

curl -s https://auth.hoortrend.com/.well-known/openid-configuration | head -c 200
kubectl -n auth-lab get pods
kubectl -n auth-lab logs deploy/authserver --tail=50          # codes émis, refus, révocations / codes issued, refusals
kubectl -n monitoring logs deploy/monitoring-grafana -c grafana | grep -i oauth | tail
sudo grep -h openid_connect /var/log/gitlab/gitlab-rails/application_json.log | tail   # sur worker1 / on worker1
  • invalid_client : le secret d'un client a changé d'un côté seulement (l'application et l'empreinte gardée par le serveur doivent changer ensemble).
  • Page « redirect_uri non enregistrée » : l'application envoie une adresse de retour différente de celle enregistrée (pour Grafana, elle dépend de son root_url).
  • GitLab, « Invalid 'state' » : un onglet est resté ouvert sur une ancienne tentative ; recommencer la connexion.
  • Changer la clé qui signe les jetons : scripts/rotate-signing-key.sh (dépôt auth-lab), puis --prune 15 minutes plus tard.
  • invalid_client: a client secret changed on one side only (the application and the hash kept by the server must change together).
  • “redirect_uri non enregistrée” page: the application sends a return address other than the registered one (for Grafana it depends on its root_url).
  • GitLab, “Invalid 'state'”: a tab stayed open on an older attempt; start the sign-in again.
  • Changing the key that signs tokens: scripts/rotate-signing-key.sh (auth-lab repository), then --prune 15 minutes later.

Exposer un nouveau sous-domaine

Expose a new subdomain

  1. Créer l'enregistrement DNS A vers l'IP du worker.
  2. Create the DNS A record to the worker's IP.
  3. Écrire un Ingress avec l'annotation cert-manager.io/cluster-issuer: letsencrypt-prod et un bloc tls.
  4. Write an Ingress with the cert-manager.io/cluster-issuer: letsencrypt-prod annotation and a tls block.
  5. Vérifier : kubectl get certificate passe à True en quelques secondes.
  6. Check: kubectl get certificate turns True within seconds.

Une alerte arrive par mail

An alert arrives by mail

  1. Lire l'alerte dans Alertmanager ou sur la page Observabilité : son nom, son namespace et sa description disent quoi regarder.
  2. Read the alert in Alertmanager or on the Observability page: its name, namespace and description say what to look at.
  3. Si elle est attendue (une maintenance, un test), la faire taire pour une durée limitée avec amtool silence add et un commentaire ; si elle est vraie par construction, l'ajouter aux routes sans envoi de sre-alert-routing, par une MR.
  4. If it is expected (maintenance, a test), silence it for a limited time with amtool silence add and a comment; if it is true by construction, add it to the no-send routes of sre-alert-routing, through a merge request.
  5. Tester la chaîne : une alerte factice envoyée avec amtool alert add arrive en mail environ 30 s plus tard.
  6. Test the chain: a fake alert sent with amtool alert add arrives by mail about 30 s later.

Un service doit être joignable depuis Internet sur worker1

A service must be reachable from the Internet on worker1

  1. Préférer l'ingress : un sous-domaine en HTTPS ne demande aucun port de plus.
  2. Prefer the ingress: an HTTPS subdomain needs no extra port.
  3. Sinon, ajouter le port à TCP_OPEN ou UDP_OPEN dans le script du pare-feu (dépôt infra), par une MR, puis le réinstaller et lancer sudo systemctl restart hoortrend-firewall.
  4. Otherwise, add the port to TCP_OPEN or UDP_OPEN in the firewall script (infra repository), through a merge request, then reinstall it and run sudo systemctl restart hoortrend-firewall.
  5. Vérifier : sudo hoortrend-firewall status montre le compteur de la nouvelle règle qui monte.
  6. Check: sudo hoortrend-firewall status shows the new rule's counter going up.

Compétences et preuvesSkills and evidence

Chaque ligne renvoie à ce qui le prouve : un fichier du seul dépôt public (demo-app), une section de cette page, ou le cluster en direct. Les autres dépôts sont privés depuis le 19 octobre.

Each row links to what proves it: a file of the only public repository (demo-app), a section of this page, or the live cluster. The other repositories are private since 19 October.

DomaineAreaCe qui est faitWhat is donePreuveEvidence
CI/CDPipelines GitLab validate → build (Kaniko) → scan (Trivy, bloquant) → promote → deploy → verify dans chaque projet qui construit une image ; merge requests sans publication, fusionnées seulement si le pipeline réussit ; déploiements sérialisés par resource_group ; demo-app synchronisé par Argo CD (GitOps, détails)GitLab pipelines validate → build (Kaniko) → scan (Trivy, blocking) → promote → deploy → verify in every project that builds an image; merge requests without publishing, merged only when the pipeline passes; deployments serialised by resource_group; demo-app synced by Argo CD (GitOps, details)demo-app/.gitlab-ci.yml · workflows
KubernetesDéploiement progressif sans coupure, PDB, sondes, requests et limits, ConfigMap de profil sans redéploiementZero-downtime rolling updates, PDBs, probes, requests and limits, profile ConfigMap without redeployingmanifestes en directlive manifests · demo-app/k8s
Kubernetes, de l'intérieurKubernetes, from the insideClient de l'API sur le modèle des contrôleurs (LIST + WATCH, reprise après 410 Gone), logs en flux, sonde synthétique, chaque action du bac à sable racontée et chronométrée en directAn API client built like the controllers (LIST + WATCH, resumption after 410 Gone), streamed logs, a synthetic probe, every sandbox action told and timed liveinside.hoortrend.com (dépôt privé)(private repository)
SécuritéSecurityRBAC au moindre privilège, Pod Security « restricted », NetworkPolicy, pare-feu sur l'interface publique du worker, conteneurs non-root en lecture seule, CSP stricte, détection de secrets et scan d'images bloquant en CILeast-privilege RBAC, Pod Security “restricted”, NetworkPolicy, a firewall on the worker's public interface, non-root read-only containers, strict CSP, secret detection and a blocking image scan in CIdemo-app/k8s/10-deployment.yaml · 00-namespace.yaml · manifestes du bac à sablesandbox manifests · sécuritésecurity
TestsTestsTests du moteur d'inside.hoortrend.com sur un enregistrement réel du bac à sable (dépôt privé) ; tests HTTP de demo-app ; tests unitaires et de bout en bout du service de contact contre un serveur SMTP jetable (dépôt privé) ; test de fumée après chaque déploiementTests of inside.hoortrend.com's engine on a real recording of the sandbox (private repository); demo-app's HTTP tests; unit and end-to-end tests of the contact service against a throwaway SMTP server (private repository); smoke test after each deploymentdemo-app/tests · workflows
ObservabilitéObservabilitykube-prometheus-stack, règles d'alerte écrites pour le lab, alertes envoyées par mail, tableaux de bord, alertes affichées en direct, retard de la supervision sur l'API mesurékube-prometheus-stack, alert rules written for the lab, alerts sent by mail, dashboards, alerts shown live, the monitoring's delay behind the API measuredGrafana · Observability · inside
FiabilitéReliabilitySauvegardes vérifiées par restauration, procédures, retour arrièreBackups checked by restoring them, runbook, rollbackbackups · runbook
RéseauNetworkingUn seul point d'entrée HTTPS, services hors cluster en upstream, TLS automatique, redirections de domaine sans coupureA single HTTPS entry point, off-cluster services as upstreams, automatic TLS, domain moves without downtimeexternal · TLS · how
Identité et accèsIdentity and accessServeur OAuth 2.0 / OpenID Connect (PKCE, JWT RS256, JWKS, rotation de clé, TOTP), SSO entre quatre applications dont Grafana et GitLab, droits limités côté applicationOAuth 2.0 / OpenID Connect server (PKCE, RS256 JWT, JWKS, key rotation, TOTP), SSO across four applications including Grafana and GitLab, rights limited on the application sideopenid-configuration · jwks.json · identity
ConteneursContainersImages non-root, construites sans privilège par Kaniko dans un podNon-root images, built without privileges by Kaniko inside a poddemo-app/Dockerfile · Dockerfiles du portfolioportfolio Dockerfiles

Pas encore en place, donc pas revendiqué : scan des images avant la fusion (elles ne sont analysées qu'après), pare-feu sur master1, agrégation de logs, traces, SLO formels, infrastructure du cluster décrite en code (l'inventaire Kubespray n'est pas versionné).

Not in place yet, so not claimed: scanning images before the merge (they are only scanned after), a firewall on master1, log aggregation, tracing, formal SLOs, the cluster's infrastructure described as code (the Kubespray inventory is not versioned).

Limites connuesKnown limits

Ce lab est une démonstration, pas une production à haute disponibilité :

  • Un seul worker. Les deux pods de chaque application tournent sur la même machine : elle protège d'un crash de pod, pas d'une panne de machine.
  • Un seul plan de contrôle (pas de haute disponibilité de l'API ni d'etcd).
  • GitLab et Zabbix vivent sur la machine, hors du cycle de vie Kubernetes : leurs mises à jour sont manuelles.
  • Pas d'agrégation de logs : les journaux se lisent pod par pod.
  • Les mails d'alerte partent d'un domaine sans SPF ni DKIM : acceptés par le serveur de messagerie, ils peuvent arriver en spam. Ceux du formulaire de contact passent le SPF, mais ne sont pas signés (DKIM).
  • master1 n'a pas de pare-feu : etcd, le kubelet et node-exporter y répondent encore à Internet.
  • Une seule version en ligne : pas d'environnement de préproduction, les merge requests construisent sans déployer.
  • Le terminal k9s du portfolio est une émulation alimentée par l'API, pas un vrai terminal distant.
  • GitLab 17.9.0 n'a reçu aucun correctif de sécurité depuis février 2025, et le SSO permet désormais à n'importe qui d'y ouvrir un compte (externe) : sa mise à jour est prioritaire.
  • Le serveur d'identité tourne en un seul exemplaire (SQLite, limites de débit en mémoire) : quelques secondes d'indisponibilité à chaque déploiement, sessions de notes et notes de l'API perdues au redémarrage.
  • inside.hoortrend.com tourne en un seul exemplaire et garde ses récits en mémoire : ils repartent de zéro à chaque déploiement.
  • Argo CD ne gère que demo-app : les manifestes des autres applications sont encore appliqués à la main.

This lab is a demonstration, not a highly available production:

  • One worker. Both pods of each application run on the same machine: that guards against a pod crash, not a machine failure.
  • One control plane (no API or etcd high availability).
  • GitLab and Zabbix live on the host, outside the Kubernetes lifecycle: their updates are manual.
  • No log aggregation: logs are read pod by pod.
  • Alert mails come from a domain with neither SPF nor DKIM: accepted by the mail server, they may land in spam. Contact form mails pass SPF, but are not signed (DKIM).
  • master1 has no firewall: etcd, the kubelet and node-exporter still answer the Internet there.
  • A single live version: no staging environment, merge requests build without deploying.
  • The portfolio's k9s terminal is an emulation fed by the API, not a real remote terminal.
  • GitLab 17.9.0 has had no security fix since February 2025, and the SSO now lets anyone open an (external) account on it: updating it is a priority.
  • The identity server runs as a single instance (SQLite, in-memory rate limits): a few seconds of downtime at each deploy; notes sessions and the API's notes are lost on restart.
  • inside.hoortrend.com runs as a single instance and keeps its stories in memory: they start from scratch at each deploy.
  • Argo CD only manages demo-app: the other applications' manifests are still applied by hand.

HistoriqueHistory

Ce qui a changé, jour par jour, depuis que cette documentation existe. Depuis le 19 octobre, seul le dépôt demo-app est public : ses merge requests sont liées, les numéros des autres dépôts, privés, restent cités pour la traçabilité. Avant le 17 octobre : la chronologie du portfolio.

21 octobre 2026

  • Le lab se sépare du portfolio : lab.hoortrend.com, avec son namespace lab et son dépôt (lab !1), ses droits de déploiement (infra !22) et son propre service d'envoi des notifications par e-mail. Le bac à sable lui appartient désormais. portfolio.ilyes-litim.fr devient le portfolio professionnel ; aucune redirection entre les deux. Liens et mode côte à côte d'inside.hoortrend.com vers le lab (inside !2), lecture en direct de demo-app autorisée au lab (demo-app !10), page « Comment ça marche » réécrite pour le lab, droits remesurés.
  • Argo CD sur argocd.hoortrend.com (administrateur seulement) : demo-app est synchronisé depuis son dépôt public, avec correction automatique des écarts ; l'image reste posée par le pipeline après l'analyse Trivy (infra !20). Vérifié : une mise à l'échelle faite à la main est annulée en 5 secondes.
  • Trivy : une nouvelle faille HIGH dans l'image kubectl (bibliothèque standard de Go), sans version corrigée de kubectl, acceptée jusqu'au 30 novembre 2026 comme les quatre précédentes (infra !23).
  • Cette page : section « GitOps avec Argo CD » (déploiement, fonctionnement, partage des rôles avec le pipeline). Mot de passe administrateur d'Argo CD réinitialisé (infra !21).

20 octobre 2026

  • demo.hoortrend.com montre ses pods en direct et celui qui vous sert à chaque requête ; les pods se trouvent par DNS, toujours sans accès à l'API Kubernetes (demo-app !8).
  • Page Notifications du portfolio simplifiée (éditeur de message et tableau des règles retirés) ; les notifications partent aussi par e-mail, à une adresse confirmée par un code (portfolio !20).
  • La page d'accueil et le portfolio mettent en avant demo-app : version, pod, nœud et temps de service lus en direct sur demo.hoortrend.com, chemin de livraison, code et pipeline (demo-app !7).
  • inside.hoortrend.com quitte demo-app pour son propre dépôt (privé, historique conservé) et son namespace ; bascule sans coupure, certificat repris (inside !1, infra !19, portfolio !18). demo-app devient une application de démonstration sur demo.hoortrend.com, dont le pipeline vérifie depuis Internet la version servie (demo-app !6).
  • Le formulaire de contact envoie : directement au serveur de messagerie, sans compte SMTP ; adresse d'envoi sur ilyes-litim.fr, dont le SPF autorise worker1 ; testé de bout en bout (portfolio !17).
  • Pression disque sur worker1 pendant le pipeline du portfolio : déploiements relancés une fois la pression retombée.
  • Cette page : section du formulaire de contact ; nouvelle section « Comment le cluster a été monté » (Kubespray, Helm, services de la machine, applications).

19 octobre 2026

  • Pare-feu sur worker1 : sur l'interface publique, seuls SSH, HTTP(S), OpenVPN et master1 passent, en IPv4 et IPv6. Testé dans un namespace réseau isolé, installé avec un retrait automatique de secours, puis vérifié (infra !16).
  • Alertes par mail au propriétaire du lab, pour tous les namespaces ; kube-proxy n'est plus collecté (sre-demo !2, infra !17).
  • Scan Trivy bloquant et :latest posé seulement après le scan, dans chaque pipeline qui construit une image (landing !18, sre-demo !1, infra !15, portfolio !14). Image kubectl 1.35.9 pour tous les déploiements (demo-app !4, auth-lab !7).
  • Manifestes sans tag figé : ceux du portfolio désignaient deux images qui n'existaient plus (portfolio !14).
  • GitLab : une merge request ne fusionne qu'avec un pipeline réussi, sur les six projets.
  • GitLab : seul demo-app reste public, pour les visiteurs comme pour les comptes SSO ; portfolio, landing et sre-demo passent en privé, ainsi que le groupe ilyes-litim. Vérifié sans compte et avec un compte SSO de test, supprimé ensuite : un seul projet visible, les autres en 404.
  • inside.hoortrend.com : le playground vu de l'intérieur. Watch de l'API, logs, sonde et Prometheus ; chaque action racontée et chronométrée, avec ce qu'en a vu un client (demo-app !1, demo-app !2, demo-app !3). Portfolio : route silencieuse pour la sonde, mode intégré pour le côte à côte, lien depuis le playground (portfolio !13) ; rôle de déploiement de la CI déplacé (infra !14).
  • Côte à côte dans les deux sens : le playground du portfolio peut aussi afficher inside à côté de ses boutons. Chaque site ne se laisse afficher par l'autre que sous sa forme intégrée (demo-app !5, portfolio !15).
  • Pression disque sur worker1 pendant le pipeline du portfolio : déploiements relancés une fois la pression retombée.
  • SSO du lab sur Grafana (rôle Viewer) et GitLab (comptes externes), droits comparés à ceux d'un visiteur ; sur les projets publics : issues réservées aux membres, demandes d'accès coupées, discussions des merge requests verrouillées (landing !14).
  • Métriques de sauvegarde enfin lues par Prometheus.
  • Serveur d'identité : noms réservés, chapitre 16 de la formation (Grafana et GitLab).
  • Cette page : sections Identité et SSO, Historique.
  • Portfolio : le lab d'authentification sur le schéma d'architecture, chronologie et compétences corrigées, noms de domaine qui débordaient du schéma (portfolio !11) ; page Architecture réorganisée, le parcours étape par étape d'abord, puis le schéma avec sa propre barre d'outils (portfolio !12).

18 octobre 2026

  • Grafana ne s'arrête plus par manque de mémoire (limite 2 Gi).
  • Démo publique : Grafana en lecture seule sans connexion, dépôts GitLab publics (landing !9).
  • Sauvegardes nocturnes d'etcd, de GitLab et de Zabbix, vérifiées par restauration.
  • Documentation : sauvegarde, compétences et preuves (landing !10, landing !11).
  • Pipeline du portfolio : trois vulnérabilités de starlette corrigées, détection de secrets et scan d'images bloquant (portfolio !9) ; pression disque sur worker1 pendant ce pipeline.
  • Référencement des deux sites : robots.txt, sitemap, canonical, Open Graph (portfolio !10, landing !12).
  • Lab d'authentification en ligne : serveur OAuth 2.0 / OpenID Connect, deux applications, une API, formation ; rotation de la clé de signature testée en production.
  • Base nginx de hoortrend.com corrigée : 53 vulnérabilités (landing !13).

17 octobre 2026

  • Le portfolio passe sur son propre domaine, portfolio.ilyes-litim.fr, en trois étapes sans coupure ; l'ancien nom redirige (portfolio !2, portfolio !3, portfolio !4, landing !4).
  • La page de contact rejoint le portfolio, avec un service séparé qui détient le mot de passe SMTP (landing !1, portfolio !5, landing !5).
  • hoortrend.com réorganisé : menu commun, accueil allégé, page « Comment ça marche », GitLab et Zabbix dans les services (landing !2, landing !3, landing !6).
  • Portfolio réorganisé, page Architecture à jour, menus mobiles (portfolio !1, portfolio !6, portfolio !7, landing !7).
  • GitLab : le portfolio rejoint le groupe ilyes-litim, les anciens projets sont archivés (portfolio !8, landing !8).

What changed, day by day, since this documentation exists. Since 19 October, only the demo-app repository is public: its merge requests are linked, the numbers of the other, private, repositories are kept for traceability. Before 17 October: the portfolio timeline.

21 October 2026

  • The lab splits from the portfolio: lab.hoortrend.com, with its namespace lab and repository (lab !1), its deploy rights (infra !22) and its own e-mail notification service. The sandbox now belongs to it. portfolio.ilyes-litim.fr becomes the professional portfolio; no redirection between the two. inside.hoortrend.com's links and side-by-side mode point to the lab (inside !2), demo-app lets the lab read it live (demo-app !10), "How it works" page rewritten for the lab, permissions measured again.
  • Argo CD on argocd.hoortrend.com (admin only): demo-app is synced from its public repository, with drift corrected automatically; the image is still set by the pipeline after the Trivy scan (infra !20). Checked: a scale-up made by hand is reverted within 5 seconds.
  • Trivy: a new HIGH flaw in the kubectl image (Go standard library), with no fixed kubectl release, accepted until 30 November 2026 like the four before it (infra !23).
  • This page: "GitOps with Argo CD" section (deployment, how it works, sharing the work with the pipeline). Argo CD admin password reset (infra !21).

20 October 2026

  • demo.hoortrend.com shows its pods live and which one serves each of your requests; the pods find each other through DNS, still without access to the Kubernetes API (demo-app !8).
  • Portfolio Notifications page simplified (message editor and rules table removed); notifications also go by e-mail, to an address confirmed with a code (portfolio !20).
  • The home page and the portfolio put demo-app forward: version, pod, node and uptime read live from demo.hoortrend.com, delivery path, code and pipeline (demo-app !7).
  • inside.hoortrend.com leaves demo-app for its own repository (private, history kept) and namespace; switched without downtime, certificate carried over (inside !1, infra !19, portfolio !18). demo-app becomes a demonstration application on demo.hoortrend.com, whose pipeline checks the served version from the Internet (demo-app !6).
  • The contact form sends: straight to the mail server, no SMTP account; sending address on ilyes-litim.fr, whose SPF allows worker1; tested end to end (portfolio !17).
  • Disk pressure on worker1 during the portfolio pipeline: deploys retried once it cleared.
  • This page: contact form section; new section “How the cluster was built” (Kubespray, Helm, host services, applications).

19 October 2026

  • Firewall on worker1: on the public interface, only SSH, HTTP(S), OpenVPN and master1 get through, over IPv4 and IPv6. Tested in an isolated network namespace, installed with a fallback automatic removal, then checked (infra !16).
  • Alerts by mail to the lab owner, for every namespace; kube-proxy is no longer scraped (sre-demo !2, infra !17).
  • Blocking Trivy scan and :latest set only after the scan, in every pipeline that builds an image (landing !18, sre-demo !1, infra !15, portfolio !14). kubectl 1.35.9 image for every deploy (demo-app !4, auth-lab !7).
  • Manifests without frozen tags: the portfolio's named two images that no longer existed (portfolio !14).
  • GitLab: a merge request only merges with a passing pipeline, in all six projects.
  • GitLab: only demo-app stays public, for visitors and SSO accounts alike; portfolio, landing and sre-demo become private, and so does the ilyes-litim group. Checked without an account and with a test SSO account, deleted afterwards: one project visible, the others 404.
  • inside.hoortrend.com: the playground from the inside. API watches, logs, a probe and Prometheus; every action told and timed, with what a client saw of it (demo-app !1, demo-app !2, demo-app !3). Portfolio: a quiet route for the probe, an embedded mode for side by side, a link from the playground (portfolio !13); the CI deploy role moved (infra !14).
  • Side by side both ways: the portfolio's playground can also show inside next to its buttons. Each site lets the other frame it only in its embedded form (demo-app !5, portfolio !15).
  • Disk pressure on worker1 during the portfolio pipeline: deploys retried once it cleared.
  • Lab SSO on Grafana (Viewer role) and GitLab (external accounts), rights compared with a visitor's; on public projects: issues members-only, access requests off, merge request discussions locked (landing !14).
  • Backup metrics finally read by Prometheus.
  • Identity server: reserved names, course chapter 16 (Grafana and GitLab).
  • This page: Identity and SSO, History.
  • Portfolio: the authentication lab on the architecture diagram, timeline and skills corrected, domain names that spilled over the diagram (portfolio !11); Architecture page reorganised, the step-by-step story first, then the diagram with its own toolbar (portfolio !12).

18 October 2026

  • Grafana no longer stops for lack of memory (2 Gi limit).
  • Public demo: Grafana read-only without login, public GitLab repositories (landing !9).
  • Nightly backups of etcd, GitLab and Zabbix, checked by restoring them.
  • Documentation: backups, skills and evidence (landing !10, landing !11).
  • Portfolio pipeline: three starlette vulnerabilities fixed, secret detection and a blocking image scan (portfolio !9); disk pressure on worker1 during that pipeline.
  • Search engines for both sites: robots.txt, sitemap, canonical, Open Graph (portfolio !10, landing !12).
  • Authentication lab online: OAuth 2.0 / OpenID Connect server, two applications, an API, a course; signing-key rotation tested in production.
  • hoortrend.com nginx base fixed: 53 vulnerabilities (landing !13).

17 October 2026

  • The portfolio moves to its own domain, portfolio.ilyes-litim.fr, in three steps without downtime; the old name redirects (portfolio !2, portfolio !3, portfolio !4, landing !4).
  • The contact page joins the portfolio, with a separate service holding the SMTP password (landing !1, portfolio !5, landing !5).
  • hoortrend.com reorganised: shared menu, lighter home page, “How it works” page, GitLab and Zabbix among the services (landing !2, landing !3, landing !6).
  • Portfolio reorganised, Architecture page updated, mobile menus (portfolio !1, portfolio !6, portfolio !7, landing !7).
  • GitLab: the portfolio joins the ilyes-litim group, the old projects are archived (portfolio !8, landing !8).