Aller au contenu principal
Version: 1.0.0

Ce qui quitte l'hôte

bitscanner expédie la description du réseau de quelqu'un hors de la machine sur laquelle il s'exécute. Deux questions deviennent donc porteuses : qu'y a-t-il dedans, et comment cela circule-t-il. Cette page répond aux deux, puis énonce sans détour les limites de cette version.

Pour savoir ce qui arme le sondage en premier lieu, voir Le scan, et les deux interrupteurs qui l'arment.

Ce que contient la télémétrie​

Tout ce que bitscanner expédie porte sur le réseau qui entoure l'hôte, jamais sur le contenu de l'hôte lui-même.

EnregistrementChamps
network_stateRoutes uniquement : destination, gateway, interface, metric, flags
neighbor_tablePar voisin : ip_address, hardware_addr, interface, state, type
neighbor_discoveryPar voisin : adresse, MAC, vendor (recherche OUI hors ligne), device_type, is_reachable, first_seen/last_seen — plus hostname si reverse_dns est armé, services (ports, et bannières si port_scan est armé), mdns_services/ssdp_info si service_discovery est armé. Plus les sous-réseaux scannés
gateway_discoveryPar passerelle : adresse, MAC, fabricant, is_default, joignabilité, et — uniquement avec gateway_probe — les ports qui ont répondu ainsi que toute bannière présentée
service_discoveryRépondeurs : name, type, host, port, protocol, discovered_via, enregistrements TXT mDNS

Deux conséquences méritent d'être dites explicitement :

  • Les sondes les plus bruyantes enregistrent les versions logicielles d'autrui. Une bannière issue du port 22 ou 25 est une chaîne de version appartenant à un équipement qui n'est peut-être pas le vôtre. Ce n'est pas un effet de bord à découvrir plus tard ; c'est précisément ce à quoi servent port_scan et gateway_probe, et c'est pourquoi ils sont désactivés par défaut et conditionnés à une déclaration d'autorisation.
  • hostname n'existe que si vous avez armé reverse_dns. Sans cela, un voisin est une adresse, une MAC et une supposition de fabricant.

Ce qu'il ne collecte délibérément pas​

network_state transportait autrefois interfaces (nom, MTU, MAC, adresses), listeners (l'inventaire des sockets locaux), une liste connections toujours vide et un champ dns_servers que rien ne remplissait jamais — un champ nul affirmant un inventaire de résolveurs qui n'a jamais été collecté. Tous ont disparu, ainsi que le code et les types qui les portaient.

Les trois premiers n'étaient qu'une redite des collecteurs réseau et ports de bitcollector. bitscanner ne collecte aucun fait sur l'hôte — pas de processus, pas de paquets installés, pas de comptes locaux, pas de lignes de commande. Si c'est un enregistrement sur la machine elle-même qu'il vous faut, il vient du collecteur, sous la confidentialité par défaut de celui-ci.

Chaque enregistrement est encapsulé dans une enveloppe portant une somme de contrôle SHA-256 (checksum) sur son en-tête et sa charge utile. C'est un contrôle d'intégrité, pas une signature — bitscanner n'a pas d'équivalent de la chaîne de preuves signée et chaînée par hachage de bitcollector.

Le trafic sortant est en https, ou l'agent ne démarre pas​

La règle porte sur les données, pas sur l'identifiant d'authentification : une carte du réseau du client qui circule en clair est lisible et modifiable par quiconque se trouve sur le chemin, qu'un jeton l'accompagne ou non.

telemetry.destinations[0]: endpoint http://es.example.com:9200 is not https — refusing to
start. Everything this agent collects about the customer's network would cross that link
in cleartext, readable and alterable by anyone on the path, credential or no credential.
Use an https endpoint; if this is a lab and you accept that the data is exposed, set
security.i_accept_plaintext_egress: true

security.i_accept_plaintext_egress est l'unique dérogation, réservée au fichier, elle est annoncée dans le journal à chaque démarrage, et elle est faite pour les laboratoires. Avec un identifiant configuré, il n'existe aucune dérogation — un endpoint en clair, ou tls.skip_verify: true, est refusé sans discussion, parce que tout ce qui peut répondre à la place de l'endpoint récolte l'identifiant.

Trois autres refus de la même famille :

  • Un identifiant intégré à l'URL est refusé, avec pour instruction de le déplacer dans le bloc auth. Go transforme les informations d'utilisateur de l'URL en un véritable en-tête Authorization: Basic, celui-ci atterrit dans chaque ligne de journal qui affiche l'endpoint, et l'expurgation de l'agent ne peut pas l'atteindre.
  • Un élément de certificat qui serait silencieusement ignoré est refusé. Définir ca_cert / client_cert / client_key alors que tls.enabled est faux est une erreur, parce que le bloc se lirait comme du TLS mutuel tout en ne présentant rien.
  • headers: n'existe pas. L'ancien exemple de configuration annonçait headers: {Authorization: "Bearer YOUR-TOKEN"} et il n'y a jamais eu une ligne de Go pour le lire — les opérateurs croyaient leur télémétrie authentifiée alors qu'elle partait anonymement. Une configuration contenant cette clé est refusée nommément.

Aucune redirection n'est jamais suivie​

refusing to follow the redirect from <from> to <to>: this agent never follows redirects on
egress, because net/http would re-attach the credential when the hostname matches (even
downgrading https to http) and would replay the batch body on a 307/308 to a host the
server chose. Point the endpoint at its final URL instead

Ces deux moitiés sont des propriétés de la bibliothèque standard de Go, pas des spéculations. Sa règle de réattachement de l'en-tête Authorization compare uniquement le nom d'hôte — ni le schéma, ni le port — de sorte qu'un serveur répondant 301 Location: http://same-host:80/ récupère le jeton de porteur en clair. Un en-tête de clé d'API personnalisé ne figure pas du tout sur la liste des en-têtes sensibles de la bibliothèque standard : il est donc recopié vers n'importe quel hôte, sur n'importe quel schéma. Et sur un 307/308, le corps de la requête — l'inventaire réseau du client — est rejoué vers l'hôte que désigne la redirection.

Le refus est une structure, pas un réglage

Le client sortant n'est pas un *http.Client. Tous les champs de cette structure sont exportés : c.CheckRedirect = nil ou le remplacement du transport aurait donc supprimé les deux contrôles en une seule affectation — et il a été prouvé que ces deux modifications laissaient la suite de tests entièrement au vert. Le client est encapsulé dans un type qui n'expose que Do et CloseIdleConnections, et le transport est construit à partir d'une configuration TLS plutôt qu'accepté depuis l'appelant : il ne reste donc aucun round-tripper fourni par l'appelant avec lequel réécrire une requête.

L'identifiant est appliqué au moment de l'envoi, et non au moment de la construction de la requête, et il échoue en position fermée : si le canal ou l'identifiant ne passe pas le contrôle — non résolu, pas en https, vérification de certificat désactivée — le lot n'est pas envoyé. « Poser l'en-tête si l'on en a un sous la main » est exactement la forme qui a déjà expédié une fois de la télémétrie non authentifiée.

Les endpoints sont expurgés dans les journaux​

Un endpoint est journalisé à chaque échec d'export : l'expurgation procède donc par refus par défaut plutôt que par une liste de noms qui ressemblent à des identifiants — toutes les valeurs de la chaîne de requête et le fragment entier sont expurgés, les noms de paramètres étant conservés pour que la ligne reste exploitable au diagnostic (api_key=[redacted] indique quel paramètre était défini). Les segments de chemin qui semblent porter un identifiant sont expurgés eux aussi. Si l'URL ne s'analyse pas du tout, l'expurgateur renvoie une constante — jamais le texte de l'appelant, parce qu'une URL qui échoue à l'analyse est en général une URL dont le mot de passe contient précisément les caractères qui cassent l'analyseur.

Ce dernier point a une limite énoncée : l'heuristique de chemin peut manquer un secret court, ou qui ressemble à un mot ordinaire, dans un segment de chemin. Le contrôle porteur, c'est que les identifiants ont leur place dans le bloc auth, où ils sont typés, vérifiés et jamais formatés dans une chaîne de caractères.

Fichiers de secrets​

Un jeton n'est réellement sorti du fichier de configuration que si personne d'autre ne peut le lire. bitscanner config validate refuse de démarrer dans chacun des cas suivants, et le message nomme le chemin fautif ainsi que le correctif. Vérifié par exécution — les chemins ci-dessous sont des chemins de documentation substitués à ceux de la transcription réelle :

# mode
control_plane: auth.token_file "/etc/bitscanner/control-plane.token" is mode 0644 —
readable by group or other; restrict it with `chmod 600 /etc/bitscanner/control-plane.token`

# any directory on the path, not just the one holding the file
control_plane: the directory "/opt/agent" on the path to auth.token_file
"/opt/agent/secrets/control-plane.token" is mode 0775 — group- or world-writable, so
another account can replace the file this agent reads. It is not the directory holding the
file — it is 1 level(s) above it — but another account can rename the whole subtree and
put its own in place. Restrict it with `chmod 755 /opt/agent` (or tighter), or move the
secret somewhere only root and this agent can write

L'ensemble complet des refus :

RefuséParce que
Mode lisible par le groupe ou par les autresTous les comptes locaux de l'hôte peuvent lire le jeton
Détenu par un compte tiers« Seul le propriétaire peut le lire » ne vaut rien quand le propriétaire est quelqu'un d'autre
Pas un fichier ordinaireUn FIFO ou un périphérique n'est pas un fichier de secret — et l'ouverture simple d'un FIFO sans écrivain bloquerait l'agent au démarrage sans le moindre diagnostic
Atteint via un lien symboliqueLa cible du lien se trouve dans un répertoire que le contrôle n'aurait pas examiné
Tout répertoire du chemin accessible en écriture au groupe ou à tous — y compris /tmpCe compte peut renommer le sous-arbre et mettre son propre fichier à la place

Les ancêtres du chemin résolu et le chemin tel qu'il est écrit sont tous deux parcourus, et il est prouvé, par périphérique et inode, que le chemin résolu désigne bien le fichier dont le descripteur est lu. Chacun de ces points était un vrai trou, comblé lors d'une vague différente : ne contrôler que le parent immédiat, laisser passer un grand-parent accessible en écriture à tous, puis l'image en miroir — ne parcourir que la chaîne résolue, ce qui acceptait un fichier 0600 dans un répertoire 0700 atteint à travers un répertoire 0777, parce que la chaîne résolue est immaculée et que c'est dans la chaîne écrite qu'habite l'attaquant.

Enfin, chaque secret provient d'exactement une source : la valeur en ligne, un <field>_file, ou un <field>_env nommant une variable d'environnement. Deux sources à la fois constituent une erreur plutôt qu'une règle de priorité — avec une règle de priorité, un opérateur qui ajoute token_file en laissant en place un jeton en ligne périmé ne peut pas savoir lequel des deux circule sur le réseau.

Enrôlement : la clé ne quitte jamais l'hôte​

Vous émettez un jeton d'enrôlement à usage unique dans le tableau de bord Cert-IX et vous le placez dans un fichier que seul l'agent peut lire :

enrollment:
endpoints:
- "https://<your-cert-ix-agent-endpoint>"
enrollment_token_file: "/etc/bitscanner/enrollment.token" # chmod 600, owner-only

Au premier démarrage, l'agent :

  1. Génère une paire de clés ECDSA P-256 sur l'hôte. La moitié privée est écrite en 0600 dans agent.data_dir, qui est forcé à 0700 — créé et re-chmodé, parce que MkdirAll laisse intact le mode d'un répertoire existant et qu'une mise à niveau dans un répertoire de données en 0755 le conserverait sinon. La clé privée ne quitte jamais la machine.
  2. S'enregistre une seule fois, en présentant le jeton d'enrôlement dans l'en-tête X-Agent-Enrollment-Token et sa clé publique dans le corps de la requête.
  3. Stocke le JWT d'agent renvoyé par la passerelle, en 0600, avec sa date d'expiration.

Après cela, le jeton est consommé : le fichier peut être supprimé, et un redémarrage charge l'identité stockée et ne procède pas à un nouvel enregistrement. Un second enregistrement donnerait un 409 contre un jeton déjà consommé, ce qui laisserait l'agent mort. Le JWT est renouvelé auprès de l'endpoint de rafraîchissement une heure avant son expiration, en utilisant le jeton lui-même — aucun jeton d'enrôlement n'intervient.

Le jeton d'enrôlement n'est pas un jeton de porteur

Il n'est présenté que dans X-Agent-Enrollment-Token, sur exactement une requête. Le placer dans auth.token_file est la mauvaise configuration qui rendait l'intégration impossible dans une version antérieure : la passerelle d'ingestion attend un JWT d'agent, si bien que chaque requête de télémétrie répond 401. Utilisez plutôt auth: {type: enrollment} sur la destination — cela présente l'identité stockée, et il n'y a rien à coller.

L'endpoint d'enrôlement doit être en https dans toutes les configurations. security.i_accept_plaintext_egress ne s'y applique pas — c'est par là que revient l'identité.

Ni la clé privée ni le jeton ne sont jamais journalisés, formatés dans une erreur ou placés dans un champ de journal : les méthodes String() et GoString() du type d'identité n'affichent que l'identifiant de l'agent et la date d'expiration, si bien qu'un %v égaré ne peut pas inscrire définitivement le JWT de cet hôte dans un fichier de journal.

Limites honnêtes​

Énoncées ici plutôt que découvertes plus tard.

Les avis de sécurité Go qui affectaient 0.1.2 sont corrigés dans 0.1.3​

0.1.3-ga est compilé avec go1.25.12, qui embarque le correctif des deux avis auxquels 0.1.2-ga (compilé avec go1.26.4) était exposé :

AvisCVSSExploité dans la nature ?De quoi il s'agitCorrigé dans 0.1.3-ga
CVE-2026-39822 (GO-2026-4970)7.8 ÉlevéNon — absent de la liste CISA KEV, EPSS sous le seuilÉvasion de la racine via un lien symbolique assorti d'une barre oblique finale dans os✅
CVE-2026-42505 (GO-2026-5856)5.3 MoyenNon — absent de la liste CISA KEV, EPSS sous le seuilFuite de confidentialité d'Encrypted Client Hello dans crypto/tls✅

🪤 Bon à savoir si vous épinglez vous-même vos chaînes d'outils : une version de Go supérieure n'est pas toujours la version corrigée. CVE-2026-39822 est corrigé dans go1.25.12 sur la branche 1.25, mais seulement dans go1.26.5 sur la branche 1.26 : go1.26.4 — une chaîne d'outils pourtant numériquement plus récente — l'embarquait donc encore. Un simple plancher de « version minimale » ne peut pas exprimer un rétroportage propre à une branche, et c'est pourquoi cette version épingle une image exacte et laisse l'autorité au scanner plutôt qu'au numéro de version. Notre propre compilation a été refusée une fois pour exactement cette raison, avant que l'épinglage ne soit corrigé.

Si vous exécutez encore 0.1.2-ga, il embarque l'avis Élevé — mettez à jour. La version de la chaîne d'outils est affichée par bitscanner version et consignée dans le SBOM CycloneDX de la version publiée : contrôlez donc la compilation que vous avez sous les yeux plutôt que de faire éternellement confiance à ce tableau.

« Livré » signifie « un 2xx est revenu »​

L'agent rapporte ce qu'il a mis en file d'attente et ce qui a été accepté :

INFO network_collector collection cycle complete
{"queued_for_delivery": ["network_state", "neighbor_table", "neighbor_discovery"]}
INFO telemetry telemetry batch accepted by destination {"status": 200, …}

Un 2xx est la parole de la destination, pas la preuve que l'enregistrement a été stocké, et la formulation dit accepté plutôt que livré à dessein. L'agent ne peut pas en savoir davantage, et une ligne qui prétendrait le contraire inventerait une garantie. La ligne queued_for_delivery est affichée à chaque cycle — y compris avec le scan désactivé — pour qu'un échec de livraison ne puisse jamais être pris pour un collecteur qui ne tourne pas.

config validate ne sait pas distinguer un jeton d'enrôlement d'un JWT​

Les deux sont des chaînes opaques dans un fichier : un bitscanner config validate sur une configuration dont le auth.token_file contient un jeton d'enrôlement rapporte donc :

Configuration is valid.

…et chaque export échoue ensuite à l'exécution. La validation contrôle les permissions du fichier, son propriétaire, chaque répertoire du chemin qui y mène, et le fait qu'exactement une source soit configurée — elle ne vérifie pas, et ne peut pas vérifier, que les octets qu'il contient sont le bon type d'identifiant. Si votre télémétrie renvoie 401 sur une installation neuve, c'est la première chose à contrôler.

Étapes suivantes​

Cette page vous a-t-elle été utile ?