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.
| Domaine | Ce que c'est | What it is | Où ça tourne | Where it runs |
| hoortrend.com | Cette page d'accueil, sa documentation et la page « Comment ça marche » | This home page, its documentation and the “How it works” page | Kubernetes · ns landing · 2 pods nginx |
| lab.hoortrend.com | Le 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.fr | Le portfolio professionnel et sa page de contact | The professional portfolio and its contact page | Kubernetes · ns portfolio · 2 pods portfolio-web + 2 pods contact-api |
| grafana | Tableaux de bord Prometheus | Prometheus dashboards | Kubernetes · ns monitoring |
| rancher | Console de gestion du cluster | Cluster management console | Kubernetes · ns cattle-system |
| argocd | Argo CD : synchronisation GitOps de demo-app (administrateur seulement) | Argo CD: GitOps sync of demo-app (admin only) | Kubernetes · ns argocd |
| gitlab | Forge Git et CI/CD (GitLab 17.9, installation Omnibus) | Git forge and CI/CD (GitLab 17.9, Omnibus install) | Machine worker1 · via upstream | worker1 host · via upstream |
| registry | Registre d'images : toutes les images du lab | Container registry: every image of the lab | Machine worker1 · via upstream | worker1 host · via upstream |
| auth, notes, board, api | Lab d'authentification : serveur OAuth 2.0 / OIDC, deux clients, une API | Authentication lab: OAuth 2.0 / OIDC server, two clients, an API | Kubernetes · ns auth-lab |
| inside | Le 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 timed | Kubernetes · ns inside · 1 pod |
| demo | Application 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 |
| zabbix | Supervision Zabbix 6.4 | Zabbix 6.4 monitoring | Machine worker1 · via upstream | worker1 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.
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:
| Service | Port interne | Internal port | Particularité | Note |
| GitLab | 8929 | external_url sans port, nginx Omnibus en listen_port 8929 | external_url without a port, Omnibus nginx on listen_port 8929 |
| Registry | 5443 | Même principe pour registry_nginx | Same principle for registry_nginx |
| Zabbix | 8444 | Site nginx dédié + PHP-FPM ; le serveur Zabbix garde le port 10051 pour les agents | Dedicated 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.
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
- 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.
- 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.
- 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).
- Il évalue enfin la santé de chaque objet : un Deployment est « Healthy » quand tous ses pods sont à jour et prêts.
- 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
| Étape | Qui | Comment |
| Tests, build, analyse de l'image | Pipeline GitLab | Kaniko, puis Trivy, qui bloque sur une vulnérabilité HIGH ou CRITICAL corrigeable. |
| Choix de l'image en production | Pipeline GitLab | kubectl 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 manifestes | Argo CD | Ré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
- repo-server clones demo-app's public repository and renders the manifests of the
k8s/ folder at the latest revision of main.
- 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.
- 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).
- Finally it assesses the health of each object: a Deployment is "Healthy" when all its pods are up to date and ready.
- 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
| Step | Who | How |
| Tests, build, image scan | GitLab pipeline | Kaniko, then Trivy, which blocks on a fixable HIGH or CRITICAL vulnerability. |
| Which image runs in production | GitLab pipeline | kubectl 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 manifests | Argo CD | Replicas, 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.
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
- Créer l'enregistrement DNS A vers l'IP du worker.
- Create the DNS A record to the worker's IP.
- Écrire un Ingress avec l'annotation
cert-manager.io/cluster-issuer: letsencrypt-prod et un bloc tls. - Write an Ingress with the
cert-manager.io/cluster-issuer: letsencrypt-prod annotation and a tls block.
- Vérifier :
kubectl get certificate passe à True en quelques secondes. - Check:
kubectl get certificate turns True within seconds.
Une alerte arrive par mail
An alert arrives by mail
- Lire l'alerte dans Alertmanager ou sur la page Observabilité : son nom, son namespace et sa description disent quoi regarder.
- Read the alert in Alertmanager or on the Observability page: its name, namespace and description say what to look at.
- 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. - 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.
- Tester la chaîne : une alerte factice envoyée avec
amtool alert add arrive en mail environ 30 s plus tard. - 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
- Préférer l'ingress : un sous-domaine en HTTPS ne demande aucun port de plus.
- Prefer the ingress: an HTTPS subdomain needs no extra port.
- 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. - 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.
- Vérifier :
sudo hoortrend-firewall status montre le compteur de la nouvelle règle qui monte. - Check:
sudo hoortrend-firewall status shows the new rule's counter going up.
Décisions d'architectureArchitecture decisions
Les choix qui structurent le lab, avec l'alternative écartée et ce que le choix coûte. Chacun est visible dans le code ou la configuration cités.
The choices that shape the lab, with the alternative set aside and what the choice costs. Each one is visible in the code or configuration cited.
D1 · La CI déploie seulement une image ; les manifestes sont appliqués à la mainD1 · CI only deploys an image; manifests are applied by hand
Pourquoi. Le compte ci-deployer ne peut modifier qu'un Deployment nommé par namespace : un pipeline compromis ne peut pas prendre le cluster.
Why. The ci-deployer account can only change one named Deployment per namespace: a compromised pipeline cannot take over the cluster.
Alternative et compromis. GitOps (Argo CD, Flux) appliquerait tout depuis Git. Coût actuel : un manifeste fusionné peut rester non appliqué, comme dans l'incident Grafana. Évolution possible, pas encore justifiée à cette taille.
Alternative and trade-off. GitOps (Argo CD, Flux) would apply everything from Git. Current cost: a merged manifest can stay unapplied, as in the Grafana incident. A possible evolution, not yet justified at this size.
D2 · GitLab et Zabbix sur la machine, derrière l'ingressD2 · GitLab and Zabbix on the host, behind the ingress
Pourquoi. Ils écoutent sur une adresse privée de la boucle locale ; l'ingress est le seul point d'entrée HTTPS et gère tous les certificats.
Why. They listen on a private loopback address; the ingress is the only HTTPS entry point and handles every certificate.
Alternative et compromis. Les installer dans Kubernetes (chart GitLab) : plus lourd pour un worker de 16 Go. Coût : mises à jour hors du cycle de vie Kubernetes.
Alternative and trade-off. Running them in Kubernetes (GitLab chart): heavier for a 16 GB worker. Cost: updates outside the Kubernetes lifecycle.
D3 · Le formulaire de contact est un service séparéD3 · The contact form is a separate service
Pourquoi. contact-api reçoit les données de n'importe quel visiteur et envoie du mail, sans aucun jeton Kubernetes ; portfolio-web parle à l'API Kubernetes et n'envoie rien. Pensé au départ pour garder le mot de passe SMTP hors de portfolio-web ; depuis D15 il n'y a plus de mot de passe, la séparation des privilèges reste.
Why. contact-api takes input from any visitor and sends mail, with no Kubernetes token; portfolio-web talks to the Kubernetes API and sends nothing. First meant to keep the SMTP password out of portfolio-web; since D15 there is no password, and the separation of privileges remains.
Alternative et compromis. Une route de plus dans portfolio-web : plus simple, mais un seul processus aurait les deux privilèges. Coût : une image et un Deployment de plus.
Alternative and trade-off. One more route in portfolio-web: simpler, but one process would hold both privileges. Cost: one more image and Deployment.
D4 · Le bac à sable a son propre namespaceD4 · The sandbox has its own namespace
Pourquoi. Les visiteurs peuvent y casser des pods : quota, NetworkPolicy sans sortie et Pod Security « restricted » bornent ce qu'ils peuvent faire.
Why. Visitors can break pods there: a quota, a NetworkPolicy without egress and Pod Security “restricted” bound what they can do.
Alternative et compromis. Agir sur les pods du portfolio lui-même : spectaculaire, mais un visiteur couperait le site. Coût : un deuxième jeu de RBAC à maintenir.
Alternative and trade-off. Acting on the portfolio's own pods: impressive, but a visitor would take the site down. Cost: a second set of RBAC to maintain.
D5 · Grafana à 1 réplicaD5 · Grafana at 1 replica
Pourquoi. Sa base SQLite est sur un volume ReadWriteOnce, que deux pods ne peuvent pas partager sans risque.
Why. Its SQLite database is on a ReadWriteOnce volume, which two pods cannot share safely.
Alternative et compromis. Plusieurs réplicas sur PostgreSQL. Coût actuel : Grafana est indisponible pendant un redémarrage.
Alternative and trade-off. Several replicas on PostgreSQL. Current cost: Grafana is down during a restart.
D6 · Démo publique : visiteur anonyme et dépôts publicsD6 · Public demo: anonymous viewer and public repositories
Pourquoi. Un compte démo partagé, dont le mot de passe est publié, pourrait être modifié par le premier visiteur venu et bloquer tous les autres.
Why. A shared demo account with a published password could be changed by any visitor and lock everyone else out.
Alternative et compromis. Lecture seule (Viewer, dépôts publics) ; le dépôt infra et les logs de jobs CI restent privés.
Alternative and trade-off. Read-only (Viewer, public repositories); the infra repository and CI job logs stay private.
Révisée le 19 octobre. Un seul dépôt reste public : voir D11.
Revised on 19 October. Only one repository stays public: see D11.
D7 · Sauvegardes croisées sans le registreD7 · Crossed backups without the registry
Pourquoi. Les images se reconstruisent depuis Git ; les sauvegarder doublerait le volume pour rien.
Why. Images are rebuilt from Git; backing them up would double the volume for nothing.
Alternative et compromis. Coût : après une perte de worker1, relancer les pipelines avant que les pods ne puissent redémarrer.
Alternative and trade-off. Cost: after losing worker1, pipelines must be re-run before pods can start again.
D8 · Un serveur d'identité écrit à la mainD8 · A hand-written identity server
Pourquoi. Il sert à une formation : chaque étape d'OAuth 2.0 et d'OpenID Connect est visible dans le code et vérifiée par un test. Que Grafana et GitLab l'acceptent sans adaptation montre qu'il suit la norme.
Why. It serves a course: every step of OAuth 2.0 and OpenID Connect is visible in the code and checked by a test. Grafana and GitLab accepting it as is shows it follows the standard.
Alternative et compromis. Keycloak ou un fournisseur du marché : le bon choix pour protéger de vraies données. Ici, rien de réel n'est protégé par ce serveur ; ce qui lui manquerait pour la production est listé dans la formation.
Alternative and trade-off. Keycloak or a market provider: the right choice to protect real data. Here, nothing real is protected by this server; what it would need for production is listed in the course.
D9 · Les droits sont décidés par l'application, pas par le jetonD9 · Rights are decided by the application, not the token
Pourquoi. Le SSO dit qui est connecté. Grafana impose Viewer et GitLab fait de chaque compte SSO un compte externe : même un serveur d'identité compromis ne donnerait pas plus que les droits d'un visiteur.
Why. The SSO says who signed in. Grafana enforces Viewer and GitLab makes every SSO account external: even a compromised identity server would give no more than a visitor's rights.
Alternative et compromis. Transmettre des groupes ou des rôles dans le jeton : plus souple, mais l'élévation de droits dépendrait alors de ce serveur. Coût : impossible de donner plus de droits à quelqu'un via le SSO ; un administrateur se connecte par mot de passe.
Alternative and trade-off. Passing groups or roles in the token: more flexible, but privilege escalation would then depend on this server. Cost: nobody can be given more rights through the SSO; an administrator signs in with a password.
D10 · Le tag :latest n'est posé qu'après le scanD10 · The :latest tag is set only after the scan
Pourquoi. Les manifestes pointent sur :latest. Si ce tag est poussé au build, un simple redémarrage de pod peut tirer une image que le scan a refusée. Appliqué partout depuis le 19 octobre 2026. Ce jour-là, les manifestes du portfolio, de hoortrend.com et de sre-demo désignaient encore des tags figés, dont deux qui n'existaient plus dans le registre : les appliquer aurait remis une vieille version ou bloqué le déploiement. Une vérification de la CI refuse désormais tout autre tag que :latest.
Why. Manifests point to :latest. If that tag is pushed at build time, a plain pod restart can pull an image the scan refused. Applied everywhere since 19 October 2026. That day, the manifests of the portfolio, hoortrend.com and sre-demo still named frozen tags, two of which no longer existed in the registry: applying them would have put an old version back or blocked the rollout. A CI check now refuses any tag other than :latest.
Alternative et compromis. Des manifestes figés sur le tag du commit : plus strict, mais chaque kubectl apply ramènerait une ancienne version. Coût : un job de plus (promote, avec crane, sans retélécharger l'image).
Alternative and trade-off. Manifests pinned to the commit tag: stricter, but every kubectl apply would bring back an old version. Cost: one more job (promote, with crane, without downloading the image again).
D11 · Un seul dépôt public : demo-appD11 · A single public repository: demo-app
Pourquoi. Choix du propriétaire du lab, le 19 octobre 2026 : les visiteurs et les comptes SSO ne voient plus qu'un dépôt, celui du site qui montre le playground de l'intérieur. Tout le reste du code est privé.
Why. The lab owner's choice, on 19 October 2026: visitors and SSO accounts only see one repository, the one of the site that shows the playground from the inside. All the other code is private.
Alternative et compromis. Garder les quatre dépôts publics (D6) : plus de preuves consultables. Coût du choix : les merge requests des autres dépôts ne s'ouvrent plus sans être membre. Cette page garde leurs numéros, une copie de chaque pipeline (Workflow de chaque projet) et renvoie aux pages en direct (Manifestes et Architecture du lab, inside.hoortrend.com).
Alternative and trade-off. Keeping the four repositories public (D6): more evidence one can open. Cost of the choice: the merge requests of the other repositories no longer open without being a member. This page keeps their numbers, a copy of each pipeline (Each project's workflow) and points to the live pages (the portfolio's Manifests and Architecture, inside.hoortrend.com).
D12 · inside.hoortrend.com observe par WATCH, pas en interrogeant l'API à intervalle fixeD12 · inside.hoortrend.com observes with WATCH, not by polling the API
Pourquoi. Un LIST puis un WATCH par type d'objet, partagés par tous les visiteurs : chaque changement arrive dès qu'il est écrit, et la charge sur l'API ne dépend pas du nombre de spectateurs. Les étapes rapides, comme un pod marqué terminating dans l'EndpointSlice 12 à 52 ms après sa suppression (mesuré), passeraient entre deux interrogations.
Why. One LIST then one WATCH per kind of object, shared by every visitor: each change arrives as soon as it is written, and the load on the API does not depend on the number of viewers. Fast steps, such as a pod marked terminating in the EndpointSlice 12 to 52 ms after its deletion (measured), would fall between two polls.
Alternative et compromis. Interroger l'API chaque seconde, comme le playground : plus simple. Coût du watch : gérer les reprises (resourceVersion, 410 Gone) et garder l'état en mémoire, donc un seul réplica et un historique perdu à chaque déploiement.
Alternative and trade-off. Polling the API every second, like the playground: simpler. Cost of the watch: handling resumption (resourceVersion, 410 Gone) and keeping state in memory, hence a single replica and a history lost at each deploy.
D13 · Le pare-feu de worker1 est une chaîne sur l'interface publique, pas ufwD13 · worker1's firewall is a chain on the public interface, not ufw
Pourquoi. Une quinzaine de ports internes de worker1 répondaient à Internet (kubelet, webhook d'admission, exporters, agent Checkmk lisible par tous, Zabbix, postfix). Une chaîne iptables et ip6tables, appelée seulement pour l'interface publique, laisse passer SSH, HTTP, HTTPS, OpenVPN et master1, et rejette le reste. Ce qu'elle laisse passer retourne aux règles de Calico et de kube-proxy, inchangées ; le trafic des pods, le tunnel VXLAN et le VPN n'y passent jamais. Elle a été testée dans un namespace réseau isolé, puis installée avec un retrait automatique au bout de 10 minutes, annulé une fois les vérifications faites.
Why. About fifteen internal ports of worker1 answered the Internet (kubelet, admission webhook, exporters, a Checkmk agent anyone could read, Zabbix, postfix). An iptables and ip6tables chain, called only for the public interface, lets SSH, HTTP, HTTPS, OpenVPN and master1 through and drops the rest. What it lets through goes back to the Calico and kube-proxy rules, unchanged; pod traffic, the VXLAN tunnel and the VPN never go through it. It was tested in an isolated network namespace, then installed with an automatic removal after 10 minutes, cancelled once the checks were done.
Alternative et compromis. ufw prend la main sur les politiques d'INPUT et de FORWARD, que Calico et kube-proxy gèrent déjà. Des HostEndpoints Calico garderaient tout dans un seul outil, mais leur refus par défaut couvre aussi le trafic des pods : plus risqué à régler sur un seul worker. Coût : un script et une unité systemd installés à la main, comme les sauvegardes, et une liste de ports à tenir à jour. master1 n'est pas encore couvert.
Alternative and trade-off. ufw takes over the INPUT and FORWARD policies, which Calico and kube-proxy already manage. Calico HostEndpoints would keep everything in one tool, but their default deny also covers pod traffic: riskier to get right on a single worker. Cost: a script and a systemd unit installed by hand, like the backups, and a list of ports to keep up to date. master1 is not covered yet.
D14 · Les alertes partent par mail, directement au serveur du domaineD14 · Alerts go by mail, straight to the domain's mail server
Pourquoi. Un seul lecteur, le propriétaire du lab, dont la boîte reçoit déjà les notifications de GitLab par ce chemin. Alertmanager remet le mail au serveur de messagerie du domaine (STARTTLS, certificat vérifié) : pas de compte SMTP, donc pas de mot de passe à stocker dans le cluster. Testé de bout en bout : deux alertes réelles envoyées puis leur fin, sans échec.
Why. A single reader, the lab owner, whose mailbox already gets GitLab's notifications this way. Alertmanager hands the mail to the domain's mail server (STARTTLS, certificate checked): no SMTP account, so no password to store in the cluster. Tested end to end: two real alerts sent, then their end, with no failure.
Alternative et compromis. Un relais SMTP authentifié (mot de passe d'application) : meilleure délivrabilité, mais un secret de plus. Slack ou un webhook : aucun canal de ce type n'existe pour le lab, et alert-webhook ne fait que journaliser. Coût : le domaine expéditeur n'a ni SPF ni DKIM. Le serveur accepte les mails, mais peut les classer en spam ; le DNS du domaine (SPF, DKIM, DMARC) est à compléter.
Alternative and trade-off. An authenticated SMTP relay (app password): better deliverability, but one more secret. Slack or a webhook: no such channel exists for the lab, and alert-webhook only logs. Cost: the sender domain has neither SPF nor DKIM. The server accepts the mails, but may file them as spam; the domain's DNS (SPF, DKIM, DMARC) still has to be completed.
D15 · Le formulaire de contact envoie sans compte SMTPD15 · The contact form sends without an SMTP account
Pourquoi. Le formulaire attendait un mot de passe d'application Google qui n'a jamais été créé : il n'avait encore envoyé aucun message. Comme Alertmanager (D14), contact-api remet le message au serveur de messagerie de hoortrend.com (STARTTLS, certificat vérifié), sans compte. L'adresse d'envoi est sur ilyes-litim.fr, dont le SPF autorise déjà l'adresse de worker1 ; vérifié depuis un pod, le serveur de Google voit bien cette adresse. Testé de bout en bout le 20 octobre 2026 : un message envoyé par le formulaire public, accepté par le serveur.
Why. The form waited for a Google app password that was never created: it had not sent a single message yet. Like Alertmanager (D14), contact-api hands the message to hoortrend.com's mail server (STARTTLS, certificate checked), with no account. The sending address is on ilyes-litim.fr, whose SPF already allows worker1's address; checked from a pod, Google's server does see that address. Tested end to end on 20 October 2026: one message sent through the public form, accepted by the server.
Alternative et compromis. Un relais SMTP authentifié (compte Google et mot de passe d'application) : un secret à stocker dans le cluster et à renouveler. Coût : pas de signature DKIM ni d'enregistrement DMARC sur ilyes-litim.fr ; le pod doit rester sur worker1 (son adresse est celle que liste le SPF), et le SPF doit suivre si cette adresse change.
Alternative and trade-off. An authenticated SMTP relay (Google account and app password): a secret to store in the cluster and to renew. Cost: no DKIM signature and no DMARC record on ilyes-litim.fr; the pod must stay on worker1 (its address is the one the SPF lists), and the SPF must follow if that address changes.