hoortrend.com

Documentation

Sécurité

Security

Durcissement du cluster et des machines, identité et SSO, isolation du Web IDE.

Hardening of the cluster and the machines, identity and SSO, the isolated Web IDE.

DurcissementHardening

Dans les pods

  • Les namespaces landing, playground, auth-lab, inside, demo-app et portfolio 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, demo-app and portfolio 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 lab 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 les machines : sur l'interface publique, seuls les services destinés au public passent, en IPv4 et en IPv6 ; les composants internes du cluster ne répondent pas à Internet.

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 lab's 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.
  • Host firewall: on the public interface, only the services meant for the public get through, over IPv4 and IPv6; the cluster's internal components do not answer the Internet.

Dans l'API du lab

  • 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 : trois vulnérabilités HIGH dans starlette (dépendance de FastAPI), corrigées avant la mise en place du blocage. Ensuite, 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).
  • Une exception ne peut être acceptée qu'avec sa raison et une date d'expiration : passé cette date, le scan échoue de nouveau.
  • 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 lab's 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: three HIGH vulnerabilities in starlette (a FastAPI dependency), fixed before the gate was switched on. Then three HIGH vulnerabilities in alert-webhook's starlette, found before its scan was switched on, were fixed the same way (FastAPI upgrade).
  • An exception can only be accepted with its reason and an expiry date: past that date, the scan fails again.
  • 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. Flux authorization code avec PKCE obligatoire, jetons JWT signés RS256 (clés de vérification réservées aux applications du lab, 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. Authorization code flow with mandatory PKCE, RS256-signed JWTs (verification keys kept for the lab's own applications, 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), comme un visiteurexternal account: the public projects (only demo-app), 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é 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).

Code et tests : projet GitLab hoortrend/auth-lab (privé). Pour l'essayer : auth.hoortrend.com, comptes de démonstration. Ni la configuration technique du serveur ni ses clés ne sont publiques : seules les applications du lab les lisent.

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 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).

Code and tests: GitLab project hoortrend/auth-lab (private). To try it: auth.hoortrend.com, demo accounts. Neither the server's technical configuration nor its keys are public: only the lab's applications read them.

Web IDE isoléIsolated Web IDE

Le Web IDE de GitLab est un VS Code dans le navigateur, qui accepte des extensions tierces (registre Open VSX). Pour qu'une extension ne puisse jamais agir avec la session GitLab de l'utilisateur, l'éditeur ne tourne pas sur gitlab.hoortrend.com : il est chargé depuis une origine à part, workbench-<secret>.web-ide.gitlab.hoortrend.com, et chaque vue d'extension depuis la sienne, v--<empreinte>.web-ide.gitlab.hoortrend.com. Le navigateur les isole les unes des autres.

  • Un Ingress qui ne sert que l'éditeur : sur *.web-ide.gitlab.hoortrend.com, seul le dossier de VS Code est routé vers GitLab ; tout autre chemin répond 404.
  • Pas de repli : le mode « une seule origine », qui ferait retomber l'éditeur sur gitlab.hoortrend.com si son domaine ne répondait pas, est désactivé. GitLab refuse alors de servir ces fichiers sur son propre domaine.
  • Les en-têtes de GitLab limitent qui peut intégrer l'éditeur (frame-ancestors), qui peut lire ses fichiers (CORS limité à l'origine exacte) et isolent sa fenêtre (Cross-Origin-Opener-Policy).
  • Un certificat wildcard couvre tous ces sous-domaines (voir Certificats TLS).

GitLab's Web IDE is VS Code in the browser, and it accepts third-party extensions (Open VSX registry). So that an extension can never act with the user's GitLab session, the editor does not run on gitlab.hoortrend.com: it loads from an origin of its own, workbench-<secret>.web-ide.gitlab.hoortrend.com, and each extension view from its own, v--<hash>.web-ide.gitlab.hoortrend.com. The browser keeps them apart.

  • An Ingress that only serves the editor: on *.web-ide.gitlab.hoortrend.com, only the VS Code folder is routed to GitLab; any other path answers 404.
  • No fallback: the “single origin” mode, which would put the editor back on gitlab.hoortrend.com if its domain did not answer, is off. GitLab then refuses to serve these files on its own domain.
  • GitLab's headers limit who may embed the editor (frame-ancestors) and who may read its files (CORS restricted to the exact origin), and isolate its window (Cross-Origin-Opener-Policy).
  • A wildcard certificate covers all these subdomains (see TLS certificates).
kubectl -n external-hosts get ingress web-ide
kubectl -n external-hosts get certificate web-ide-gitlab-hoortrend-com-tls