Lab 8 — Sécurité & conformité : masquer les données sensibles
La télémétrie est un canal de fuite : tokens, mots de passe, emails s’y retrouvent trop facilement — et un backend d’observabilité est rarement protégé comme la base de production. Dans ce lab, vous constatez une fuite réelle (déjà dans le code de review-service…), vous voyez où elle doit être corrigée, puis vous posez un filet dans le collecteur pour tout ce qu’une correction de code ne peut pas atteindre.
🇪🇺 RGPD : email, nom, téléphone sont des données personnelles. Leur présence dans les traces et les logs crée exactement les mêmes obligations que dans une base de données : droit à l’effacement, durée de conservation limitée, traçabilité des accès.
Sauf qu’une plateforme d’observabilité est bâtie pour l’inverse. Elle duplique — le même email part dans Jaeger, dans OpenSearch, dans les dashboards exportés et dans les sauvegardes — et elle conserve, sans rien savoir de ce qu’elle stocke. Effacer une donnée d’une base, c’est une requête ; l’effacer de six mois de traces réparties sur trois backends, personne ne sait le faire proprement.
D’où la seule stratégie tenable, et l’objet de ce lab : ne jamais l’y envoyer.
Prérequis
- Labs 1 à 7 terminés, agent Java actif sur
review-service— l’application reste exactement celle du Lab 7, ce lab ne la reconstruit à aucun moment. - D’abord les variables de la formation chargées dans votre shell :
. ./scripts/env.sh. Elles donnent le port du review-service ($APP_PORT, accès direct au service, pas via le frontend-proxy) et$PF_HOST, le nom par lequel vous le joignez. - Les accès ouverts (
./scripts/open-ui.sh) : l’API OpenSearch est surhttp://$PF_HOST:$OS_PORT/.
Étapes
Partie 1 — Constater la fuite
- Relire le code du POST dans
ReviewController.java— trois fautes y sont plantées volontairement :
- Générer une requête « authentifiée » — sur l’application du Lab 7, telle quelle, agent Java compris. Rien à redéployer :
💡 Pourquoi un produit qui n’existe pas ? Pour que votre trace survive au tail sampling du Lab 7, qui ne garde qu’une requête ordinaire sur quatre. Celle-ci échoue,
keep-errorsconserve donc 100 % des traces comme la vôtre — et la fuite est exactement la même : le code pose l’email et le token avant d’aller chercher le produit.
- Chercher la fuite. Dans Jaeger, ouvrez la trace
POST /api/reviews: que voyez-vous dans les attributs ? Puis cherchez l’email dans les logs — depuis Grafana (Explore, datasourcewebstore-logs), ou directement sur l’API d’OpenSearch :
Partie 2 — Corriger là où la fuite naît
- La seule vraie correction : ne pas écrire la donnée. Une fuite produite par votre propre code se règle dans ce code — trois lignes à supprimer dans
ReviewController.java, et rien d’autre.
L’identifiant produit reste : il est utile en cas d’incident et n’identifie personne. Ce qui part, c’est ce qui n’aurait jamais dû être écrit.
💡 Pour le vérifier vous-même (optionnel — c’est une reconstruction complète de l’image, quelques minutes) :
Rejouez alors le
curlde l’étape 2 : la trace n’a plus niuser.emailni le headerAuthorization, et l’email est introuvable dans OpenSearch.La suite du lab garde volontairement le code fautif, pour que la partie 3 ait quelque chose à retenir.
💡 Alors pourquoi une partie 3 ? Parce que cette correction ne couvre que le code que vous écrivez. Trois choses lui échappent, et elles sont la règle en production :
- Les services que vous ne contrôlez pas. Les onze autres services de la démo, une bibliothèque tierce, l’équipe d’à côté qui n’a pas encore fait ce lab.
- L’instrumentation automatique elle-même. L’agent pose
url.full— qui contient le?token=…d’un appel sortant —,db.statement, des en-têtes HTTP que vous n’avez pas choisis. Aucune ligne de votre code n’est en cause.- La régression. Un
setAttributefautif revient dans six mois, dans une revue de code que personne ne relie à la RGPD.D’où le second étage. Notez l’ordre : on corrige, puis on met un filet. L’inverse — masquer en aval et laisser la faute dans le code — revient à faire circuler la PII dans tout le réseau en espérant que le filtre ne tombe jamais.
💡 Où voir
url.fulletdb.statementdans vos traces ? Pas sur le span serveurPOST /api/reviews, mais sur ses enfants.url.fullest sur le span client nomméGET— l’appel sortant dereview-servicevers lefrontend;db.statementest sur les spans JDBC, nommésSELECT otelouINSERT otel. Dépliez-les dans Jaeger, ou demandez-les à son API.L’appel sortant, d’abord — la trace du
curlde l’étape 2 le contient déjà :Le SQL, ensuite. Ce
POSTéchoue avant d’écrire en base : il n’a aucun span JDBC. Il faut donc une lecture des avis — et en lancer plusieurs, parce que le tail sampling du Lab 7 ne garde qu’une requête ordinaire sur quatre :Le filtre sur
reviewsécarte les requêtes de métadonnées que le driver JDBC pose au démarrage, illisibles et sans intérêt ici.Ici l’URL n’a pas de
?token=…, et le SQL ne montre que des noms de colonnes : Hibernate envoie les valeurs à part, en paramètres liés. Rien ne fuit — et ce n’est pas un hasard : un secret n’a pas sa place dans une query string (il finit aussi dans les logs d’accès des proxies), et un SQL ne se construit pas par concaténation (c’est la porte ouverte à l’injection SQL). Ces deux règles sont antérieures à l’observabilité.Ce que l’agent y ajoute : il pose ces attributs tout seul. Le jour où un service enfreint l’une de ces règles — le vôtre, ou celui de l’équipe d’à côté —, la donnée part dans les traces sans qu’aucun
setAttributen’apparaisse nulle part pour vous mettre la puce à l’oreille.
💡 Il existe un troisième endroit : le SDK de l’application. On filtre alors avant même que la donnée ne sorte du processus — ce qu’exigent certaines politiques internes. Le dépôt en garde un exemple à lire,
PiiMaskingConfiguration.java, qui remplace les emails des logs par***@***. Il ne fonctionne qu’avec le Starter du Lab 2, pas avec l’agent : les deux ne se branchent pas au SDK par le même endroit.
Partie 3 — Le filet de sécurité : masquer au collecteur
Écrire la règle OTTL dans
manifests/80-otel-security-values.yaml: elle supprimeuser.emailethttp.request.header.authorizationde tous les spans, et masque les emails dans tous les bodies de logs — quel que soit le service, corrigé ou non.Deux documentations pour cela : celle du processor
transform, qui donne la structure (trace_statements,log_statements), et la liste des fonctions OTTL —delete_keyetreplace_patternsont celles qu’il vous faut.
- Appliquer, et rejouer la requête fautive. Le code de
review-serviceécrit toujours l’email et le token : c’est justement ce qu’on veut vérifier — que le collecteur les arrête quand même.
Dans Jaeger : la nouvelle trace POST /api/reviews n’a plus ni user.email ni le header Authorization. Dans OpenSearch : collector-mask@example.com est introuvable, le log montre ***@***.
La trace est propre alors que le code est fautif : le filet a retenu. C’est ce que vous voulez le jour où la faute vient d’un service que vous ne pouvez pas corriger.
⚠️ Mais lisez le log en entier. Demandez à OpenSearch les derniers messages, sans filtrer sur l’email cette fois — il est masqué, il ne sert plus à retrouver quoi que ce soit :
Relevé sur le cluster de la formation :
L’email est masqué, le nom ne l’est pas — et c’est une donnée personnelle au même titre. Le collecteur ne sait masquer que ce qui a une forme reconnaissable : un email a un
@, un IBAN un préfixe, une carte bancaire seize chiffres. Un nom, non — « Safe User » ressemble à n’importe quelle suite de mots, et le motif qui l’attraperait masquerait aussi « Creating review ».Voilà la vraie limite du filet, et la raison pour laquelle l’étape 4 reste la correction principale : le code, lui, sait que ce champ est un nom.
Sauf si le log est structuré. Le vrai problème n’est pas le nom, c’est qu’il est noyé dans une phrase. Écrit comme un champ à part —
user.namedans les attributs du log plutôt que dans le texte — il redevient adressable par sa clé, et le collecteur le supprime comme il supprimeuser.emaildes spans à l’étape 5 :delete_key(log.attributes, "user.name"), sans rien avoir à reconnaître. Une raison de plus de structurer ses logs, au-delà de celles du Lab 5.
- Et dans un vrai projet, on fait les deux. Le filet n’est pas une excuse : la PII a bel et bien quitté le processus, traversé le réseau, et n’a été arrêtée qu’au collecteur — un maillon qui peut être mal configuré, contourné par un service qui exporte ailleurs, ou remis à zéro par un
helm upgrademalheureux. La correction de l’étape 4 reste donc la première chose à faire ; ce lab la laisse de côté uniquement pour que la démonstration ci-dessus soit visible.
C’est la défense en profondeur : le code ne produit pas la donnée, et le collecteur protège ce que le code ne couvre pas — les autres services, l’instrumentation automatique, et la faute qui reviendra un jour.
Livrable
La preuve avant / après : capture de la trace avec le JWT et l’email (partie 1), et de la même requête une fois le collecteur configuré (partie 3), alors que le code fautif n’a pas bougé — plus la recherche OpenSearch vide sur collector-mask@example.com.