Aller au contenu principal
Version: 1.0.0

Le scan, et les deux interrupteurs qui l'arment

Le scan actif est la seule partie de bitscanner qui envoie des paquets non sollicités à des équipements appartenant à autrui. Sonder un réseau que vous n'êtes pas autorisé à sonder est une question juridique avant d'être une question technique — et le segment placé devant l'agent est choisi par les interfaces de l'hôte, pas par vous : un VPN, un réseau de conteneurs ponté ou une interface d'entreprise étendue peuvent mettre à portée un espace d'adressage que personne n'avait prévu.

C'est donc l'enveloppe, et non la capacité, qui est la valeur par défaut. Chaque interrupteur ci-dessous échoue en position fermée, et ceux qui comptent ne peuvent être positionnés que par un humain modifiant un fichier.

Pour savoir ce que l'agent rapporte sans rien de tout cela, voir la présentation de bitscanner.

Les deux interrupteurs​

Rien ne sonde tant que les DEUX ne sont pas vrais
  1. scanning.enabled arme l'enveloppe — « cet agent peut sonder, voici le périmètre, et j'y suis autorisé ».
  2. scanning.probes.<name> choisit quelles sondes s'exécutent à l'intérieur — c'est-à-dire à quel point c'est bruyant.

Toutes les sondes valent false par défaut : scanning.enabled: true seul envoie donc exactement zéro paquet.

Ce sont deux décisions parce que les deux modes de défaillance sont différents : un mauvais périmètre sonde les mauvaises personnes, un mauvais jeu de sondes sonde les bonnes personnes trop agressivement. Élargir l'un ou l'autre est toujours une modification explicite.

scanning:
enabled: false # the envelope
i_accept_scanning_authorization: false # your assertion that you may probe these networks
allowed_cidrs: [] # the ONLY address space that may ever be probed
probes:
neighbor_discovery: false # …and every probe, individually

Une sonde armée alors que l'enveloppe est désactivée est un échec au démarrage qui nomme les deux clés, pas un réglage discrètement ignoré. L'ignorer silencieusement laisserait un opérateur relisant son propre fichier croire qu'un scan tourne — ou conclure que le produit est cassé lorsque aucun résultat n'apparaît :

$ bitscanner config validate --config config.yaml
Error: configuration validation failed: failed to load config: config validation failed:
scanning.probes.neighbor_discovery, scanning.probes.port_scan are true but
scanning.enabled is false: the probe toggles choose WHICH probes run, scanning.enabled
arms the envelope that permits any of them at all, and with the envelope off every one of
those probes would be denied. Either set scanning.enabled: true (together with
scanning.i_accept_scanning_authorization and scanning.allowed_cidrs), or turn off
scanning.probes.neighbor_discovery, scanning.probes.port_scan

Le même traitement s'applique à une sonde dont le prérequis est désactivé. Un scan de ports s'exécute par-dessus la liste de voisins que produit la découverte de voisins : demander l'un sans l'autre donne donc une configuration qui tournerait sans rien produire — indiscernable, du point de vue de l'opérateur, d'un agent cassé :

scanning.probes.port_scan is true but scanning.probes.neighbor_discovery is false: the
port scan probes the neighbours that neighbour discovery finds; with neighbour discovery
off the target list is empty and nothing would be scanned. Set
scanning.probes.neighbor_discovery: true as well, or turn off scanning.probes.port_scan

Et activer l'enveloppe sans la déclaration d'autorisation :

scanning.enabled is true but scanning.i_accept_scanning_authorization is false: active
scanning sends unsolicited packets to other people's devices, and setting this key is you
asserting that you are authorised to probe every network listed in scanning.allowed_cidrs
(you own them, or you hold written permission from whoever does). Probing a segment you
are not authorised to probe — a shared/colocated network, or a partner network reachable
over a VPN — may be unlawful.

Il n'existe pas non plus de valeur par défaut implicite « tout scanner ». allowed_cidrs doit être non vide lorsque l'enveloppe est activée, les entrées doivent être des adresses de réseau canoniques, et un 0.0.0.0/0 est refusé sans discussion :

scanning.allowed_cidrs entry "10.20.1.3/16" is not a network address: it authorises
10.20.0.0/16 (65536 addresses), which is almost certainly wider than intended. Write the
network explicitly, or use a /32 (or /128) for a single host

Ce refus existe parce que tous les analyseurs CIDR de la Terre élargissent silencieusement une adresse d'hôte assortie d'un masque de réseau. L'opérateur a écrit une adresse et en a autorisé 65 534.

scanning.* et security.* ne peuvent venir que du fichier​

En définir un depuis une variable d'environnement arrête l'agent
$ BITSCANNER_SCANNING_ENABLED=true bitscanner config validate --config config.yaml
Error: configuration validation failed: failed to load config: refusing to start:
BITSCANNER_SCANNING_ENABLED set in the environment, and the scanning envelope and the
security block are settable ONLY from the config file. Those keys — scanning.enabled,
scanning.i_accept_scanning_authorization, scanning.allowed_cidrs, every
scanning.probes.*, and security.i_accept_plaintext_egress — are acknowledgements that a
person is authorised to probe somebody else's network, or that a customer's network map
may cross the wire in cleartext. An acknowledgement has to be written by hand into a file
that can be reviewed and diffed, not inherited from a shell profile, a systemd drop-in, a
compose file or a parent process. This is NOT ignored and NOT applied: the agent stops.

C'est là tout l'objet de ces déclarations, et il vaut la peine de dire crûment pourquoi.

Toute la valeur de i_accept_scanning_authorization tient à ce qu'une personne l'a écrit dans un fichier qui peut être relu, comparé et versionné dans la gestion de configuration. Une variable exportée par un profil de shell, un drop-in systemd, un fichier compose ou un processus parent compromis n'est rien de tout cela. Si l'environnement pouvait la définir, le contrôle ne serait qu'une formalité.

Trois propriétés en font une garantie structurelle plutôt qu'une règle dont il faut se souvenir :

  • L'environnement n'est pas une source pour ces clés. La liaison avec l'environnement est une liste d'autorisation explicite de clés opérationnelles ordinaires (agent.id, intervalles, tailles de lot). Rien de ce qu'elle contient ne peut armer une sonde ni relâcher la sécurité du transport.
  • Les sous-arbres de sûreté sont relus depuis le fichier par un second chargeur qui n'a aucune liaison avec l'environnement, et ces valeurs écrasent tout ce qu'a produit le chargeur principal.
  • Une tentative est un refus, pas un filtrage. L'intégralité des espaces de noms BITSCANNER_SCANNING_* et BITSCANNER_SECURITY_* est réservée, et l'erreur nomme la variable — parce qu'un opérateur qui croit avoir armé un scan ne doit surtout pas rester sans réponse.

L'espace de noms réservé est délibérément plus large que les clés de configuration : une variable qui vous appartient et qui s'appelle BITSCANNER_SECURITY_TOKEN est refusée elle aussi. Un refus qui nomme la variable se corrige en quelques secondes ; une liste de motifs qui doit deviner quelles orthographes comptent, non.

L'échelle des sondes​

Les sept valent false par défaut. Elles sont listées de la moins intrusive à la plus intrusive — et chaque ligne indique ce qui part réellement sur le réseau, parce qu'un opérateur ne peut pas consentir à quelque chose qui n'est décrit que par son nom.

SondeCe qu'elle met sur le réseauNécessite
neighbor_discoveryRien. Lit le cache ARP/NDP du noyau lui-même et enrichit chaque entrée à partir d'une table de fabricants MAC/OUI hors ligne fournie avec l'agent—
gateway_discoveryRien de nouveau. Table de routage, plus attribution de la MAC depuis le cache ARP et la table OUI hors ligne. ⚠️ Faux dans 0.1.3-ga et les versions antérieures — un écho ICMP était aussi envoyé à chaque passerelle trouvée ; voir l'avertissement ci-dessous—
reverse_dnsUne requête PTR par voisin sans nom. C'est un paquetneighbor_discovery
service_discoveryUne requête mDNS vers 224.0.0.251:5353 et un M-SEARCH SSDP vers 239.255.255.250:1900. Un paquet chacun — et tous les équipements du domaine de diffusion les voientneighbor_discovery
gateway_probeConnexions TCP vers les ports de la passerelle elle-même (80, 443, 22, 23, 53 ; puis 8080/8443 pour la caractérisation des services, avec lecture des bannières), plus un écho ICMPgateway_discovery
active_arp_sweepINTRUSIF. Parcourt la plage utilisable de chaque sous-réseau attaché et tente une connexion TCP sur une poignée de ports de chaque adresse — des centaines à des milliers de tentatives de connexion par cycleneighbor_discovery
port_scanLE PLUS INTRUSIF. ~19 ports TCP courants sur chaque voisin découvert, plus la lecture des bannières sur ceux qui en présentent une (21/22/25/110/143)neighbor_discovery

Trois d'entre elles méritent mieux qu'une ligne de tableau.

reverse_dns est un interrupteur parce qu'il n'en était pas un​

La découverte de voisins est présentée comme la première exécution sans risque : lire les tables du noyau, n'envoyer rien. Cette affirmation était fausse. Chaque voisin sans nom faisait l'objet d'une résolution inverse inconditionnelle, et aucune configuration ne permettait de la désactiver — une observation de l'enveloppe de scan avec seulement neighbor_discovery armé enregistrait deux sondes autorisées par cycle, qui étaient ces résolutions.

Une résolution inverse est un paquet, et un paquet plus révélateur qu'il n'y paraît. Elle part vers le résolveur que cet hôte désigne — sur un segment d'entreprise, souvent un résolveur que l'équipe sécurité du client exploite et journalise — et la requête révèle précisément lesquels de leurs hôtes cet agent a énumérés, un PTR à la fois. Sur un segment où l'agent est mis à l'essai à l'insu de l'équipe réseau, c'est cela la divulgation, pas le scan de ports.

Elle est désactivée par défaut pour que le mode zéro paquet soit atteignable par configuration.

gateway_probe a besoin d'un privilège dont il ne dispose peut-être pas​

Le contrôle de joignabilité ouvre un socket ICMP brut (ip4:icmp), ce qui exige root ou CAP_NET_RAW. Sans cela, l'ouverture échoue et l'agent se rabat sur un datagramme UDP — et ce repli est soumis au garde-fou séparément, parce qu'une cible non autorisée ne doit pas devenir autorisée du seul fait que la première méthode a échoué.

La passerelle est le routeur de quelqu'un, et souvent le seul équipement dont la panne met tout le segment à terre. Figurer dans votre table de routage ne vaut pas autorisation de la sonder, et c'est pourquoi gateway_discovery (passif, répond à « derrière quel routeur suis-je, et sa MAC a-t-elle changé ») et gateway_probe (actif) sont deux clés distinctes.

gateway_discovery ouvrait lui aussi ce socket brut, dans 0.1.3-ga et les versions antérieures

La ligne ci-dessus indique que gateway_discovery n'émet rien de nouveau, et le paragraphe ci-dessus indique que le socket ICMP brut appartient à gateway_probe. Dans toutes les versions jusqu'à 0.1.3-ga incluse, ni l'un ni l'autre n'est vrai.

L'appel de joignabilité se trouvait dans la branche empruntée lorsque gateway_probe est désactivé. Armer gateway_discovery en refusant délibérément gateway_probe — c'est précisément ainsi qu'un exploitant dit « n'ouvrez pas de socket brut vers mon routeur, et ne m'obligez pas à accorder CAP_NET_RAW » — en ouvrait donc un quand même, une fois par passerelle et par cycle, avec un datagramme UDP vers le port 33434 en repli chaque fois que le socket brut était refusé.

Ce qui n'a jamais été affecté : cette sonde restait autorisée par le garde-fou de scan, elle ne pouvait donc jamais atteindre qu'une adresse située dans vos allowed_cidrs, et elle était refusée d'emblée avec scanning.enabled: false. Le défaut est qu'un interrupteur documenté comme inactif agissait quand même — non que quoi que ce soit se soit échappé de l'enveloppe.

Si vous utilisez 0.1.3-ga ou une version antérieure avec gateway_discovery armé, soit vous acceptez que votre passerelle soit pinguée à chaque cycle, soit vous désactivez gateway_discovery en attendant de pouvoir mettre à jour. Dans le code source, le chemin passif n'émet désormais plus rien : il ne rapporte la joignabilité que d'après l'entrée ARP résolue par le noyau lui-même, ne rapporte aucun temps d'aller-retour qu'il n'a pas mesuré, et la construction échoue si une fonction autre que le chemin de gateway_probe peut atteindre ce socket — épingler le socket à une fonction plutôt qu'à ses appelants est ce qui a permis à ce défaut de survivre à deux revues.

active_arp_sweep est borné par le fichier, pas par l'hôte​

Les sous-réseaux proviennent des interfaces de cet hôte, si bien que c'est un VPN ou une interface d'entreprise étendue — et non votre configuration — qui décide de ce qui se trouve devant le balayage. Ce sont allowed_cidrs et min_prefix_length qui le retiennent. min_prefix_length vaut 22 par défaut (~1 022 hôtes) et est refusé en deçà de /16 purement et simplement : non pas à cause des paquets, que le budget d'hôtes borne, mais parce que la boucle de balayage sur un /8 parcourrait 16 millions d'adresses en demandant l'autorisation pour chacune.

Le résultat zéro paquet, mesuré​

L'affirmation « la découverte de voisins, seule, n'envoie rien » est de celles qu'il faut mesurer plutôt qu'asséner. Exécution sur un hôte Linux ordinaire avec scanning.enabled: true, neighbor_discovery armé et toutes les autres sondes désactivées — la plage autorisée ci-dessous a été remplacée par une plage de documentation, et rien d'autre n'a été modifié :

WARN scanguard active scanning is ENABLED: this agent will send unsolicited probes
{"allowed_cidrs": ["10.20.0.0/24"], "allow_public_targets": false,
"min_prefix_length": 22, "max_hosts_per_cycle": 1024, "max_concurrency": 16,
"per_probe_delay": "50ms", "max_cycle_duration": "5m0s"}
WARN agent ACTIVE SCANNING ARMED: this agent will send unsolicited probes
{"armed_probes": ["neighbor_discovery"], "confined_to_allowed_cidrs": ["10.20.0.0/24"], …}

INFO neighbor_scanner neighbor discovery complete
{"neighbors_found": 116, "subnets": 27, "duration": "71.116454ms"}
INFO network_collector scan envelope
{"probes_allowed_since_start": 0, "probes_denied_since_start": 0,
"denied_by_reason_since_start": {}, "hosts_probed_this_cycle": 0,
"host_budget_remaining_this_cycle": 1024}
INFO network_collector collection cycle complete
{"queued_for_delivery": ["network_state", "neighbor_table", "neighbor_discovery"]}

Quatre cycles consécutifs, 116 à 119 voisins réels répartis sur 27 sous-réseaux attachés, et probes_allowed_since_start est resté à 0 dans chacun d'eux. Le garde-fou de scan n'a jamais été consulté, parce qu'il n'y avait rien à lui demander : les voisins sortaient des tables du noyau et les noms de fabricants d'une table fournie avec l'agent. Trois enregistrements de télémétrie ont tout de même été mis en file d'attente par cycle.

C'est à cela que ressemble un premier déploiement : activez l'enveloppe, n'armez que neighbor_discovery, et découvrez ce qui se trouve sur le segment avant de décider si quelque chose de plus bruyant se justifie.

Les compteurs indiquent sur quelle horloge ils sont

probes_allowed_since_start et probes_denied_since_start sont des totaux sur la durée de vie du processus ; hosts_probed_this_cycle et host_budget_remaining_this_cycle sont réinitialisés à chaque cycle. Ils étaient auparavant affichés ensemble sous un intitulé qui prétendait que les quatre étaient par cycle, et sur un agent en fonctionnement le compteur d'autorisations est monté de 686 à 1142 au fil des cycles tandis que le nombre d'hôtes restait à 16 — ce qui se lit comme un scan qui s'emballe alors qu'il n'en était rien. Le garde-fou ne tient aucun compteur de décisions par cycle : les totaux sont donc étiquetés comme des totaux plutôt que d'inventer un chiffre par cycle pour la ligne de journal.

scanguard : la décision se prend là où le paquet part​

Chaque sonde est autorisée immédiatement avant que la connexion ne soit tentée — et non au moment où la liste de cibles est construite.

Pourquoi cette distinction constitue tout le principe de conception

Une liste de cibles filtrée est une convention, et les conventions sont précisément ce que les futurs chemins d'appel contournent discrètement. Quelqu'un ajoute un chemin de code qui construit sa propre liste, ou réutilise une adresse issue d'une réponse, et le filtre appliqué trois fonctions plus tôt ne s'y applique pas. Placer le contrôle là où le paquet part signifie qu'un nouveau chemin d'appel interroge le garde-fou, ou bien ne compile pas contre la plomberie du collecteur.

Le garde-fou échoue en position fermée à tous les niveaux : le garde-fou sans aucune configuration refuse tout ; un garde-fou nul remis à un collecteur est remplacé par un « tout refuser » plutôt que contourné ; une adresse qui ne s'analyse pas — ou qui s'analyse de façon ambiguë — est refusée ; l'épuisement du budget, du cadencement et de l'échéance refuse au lieu de dégrader.

Chaque décision, autorisation comme refus, est consignée avec une raison exploitable par une machine :

RefuséRaison consignée
Le scan actif est désactivéscanning_disabled
La cible ne s'analyse pasunparseable_target
Boucle locale, lien-local, multidiffusion, adresse non spécifiée, adresse de réseau ou de diffusion de la plage autoriséereserved_address
Tout ce qui est en dehors d'allowed_cidrsoutside_allowed_cidrs
Espace d'adressage public sans allow_public_targetspublic_target_not_allowed
max_hosts_per_cycle atteinthost_budget_exhausted
max_cycle_duration atteintcycle_duration_exceeded
Un sous-réseau plus large que min_prefix_length, ou hors périmètresubnet_wider_than_min_prefix, subnet_outside_allowed_cidrs
Un groupe de multidiffusion qui n'est pas un groupe de découverte connunot_a_discovery_multicast_group
L'agent s'arrête, ou le contexte du cycle a été annulé en cours de sondecontext_cancelled

Les autorisations portent elles aussi une raison — in_scope_private, in_scope_public_explicitly_allowed, link_local_discovery_group, subnet_within_sweep_limits — de sorte que la piste d'audit dise pourquoi une sonde a été permise, et pas seulement qu'elle l'a été.

Les refus tiennent même lorsque la liste d'autorisation correspondrait par ailleurs. C'est éprouvé directement par la suite de tests du garde-fou, qui passe sur le commit livré : la boucle locale (v4 et v6), le lien-local (v4 et v6), la multidiffusion y compris le groupe SSDP, l'adresse non spécifiée, l'adresse de diffusion limitée, ainsi que les adresses de réseau et de diffusion de la plage autorisée sont toutes refusées alors même qu'elles se trouvent à l'intérieur d'allowed_cidrs ; un hôte privé dans le périmètre est autorisé sans l'activation explicite des cibles publiques ; et une adresse IPv6 mappée en IPv4 ne peut pas contourner la liste d'autorisation.

0 probes sent, 4094 denied outside_allowed_cidrs n'est pas un dysfonctionnement. C'est la preuve, visible par l'opérateur, que c'est bien la liste d'autorisation qui décide.

Les plafonds font partie de l'enveloppe​

CléDéfautBornesCe qu'elle retient
min_prefix_length2216–32Le sous-réseau le plus large qu'un balayage puisse parcourir
max_hosts_per_cycle10241–65536Hôtes distincts sondés par cycle, tous sous-réseaux confondus
max_concurrency161–256Sondes en vol simultanément, imposé par le garde-fou lui-même
per_probe_delay50ms0–10sÉcart minimal entre paquets de sondage — par sonde, si bien qu'un scan de 19 ports sur un même hôte est cadencé 19 fois
max_cycle_duration5m≤ 1hPlafond en temps d'horloge sur le sondage à l'intérieur d'un cycle
allow_public_targetsfalse—Le sondage hors RFC1918 / RFC6598 / RFC4193

Ce ne sont pas des suggestions de réglage. Un max_concurrency de 10 000 avec un délai nul sur un /22, c'est un déni de service contre le propre segment du client, délivré par un binaire qu'il a installé sur notre recommandation — les valeurs sont donc contrôlées par rapport à leur plage au chargement, et refusées en dehors.

Étapes suivantes​

  • Ce qui quitte l'hôte — ce que contiennent réellement les enregistrements découverts, comment ils circulent, et les limites livrées avec cette version.
  • Présentation de bitscanner — ce que produit un cycle, ce qu'il coûte, et comment vérifier le téléchargement.
  • Les garde-fous de bitenforcer — le même réflexe de conception appliqué à un rayon d'action différent : le mesurer, le nommer, et refuser plutôt que deviner.
  • Gestion des actifs — là où les équipements découverts deviennent des actifs dotés d'un propriétaire.

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