Zum Hauptinhalt springen
Version: 1.0.0

Scannen, und die beiden Schalter, die es scharfstellen

Aktives Scannen ist der einzige Teil von bitscanner, der unaufgeforderte Pakete an Geräte sendet, die jemand anderem gehören. Ein Netz zu sondieren, für das Sie keine Berechtigung haben, ist zuerst eine Rechtsfrage und erst danach eine technische — und welches Segment vor dem Agenten liegt, entscheiden die Schnittstellen des Hosts, nicht Sie: Ein VPN, ein gebrücktes Container-Netz oder eine weit gefasste Unternehmensschnittstelle kann Adressraum in Reichweite bringen, den niemand vorgesehen hat.

Deshalb ist der Rahmen die Voreinstellung, nicht die Fähigkeit. Jeder Schalter unten verweigert im Zweifelsfall, und die, auf die es ankommt, kann nur ein Mensch setzen, der eine Datei bearbeitet.

Was der Agent ganz ohne all das meldet, steht im bitscanner-Überblick.

Die beiden Schalter​

Nichts sondiert, solange nicht BEIDE wahr sind
  1. scanning.enabled stellt den Rahmen scharf — „dieser Agent darf sondieren, hier ist der Geltungsbereich, und ich bin dazu berechtigt“.
  2. scanning.probes.<name> wählt, welche Sonden darin laufen — wie laut das wird.

Jede Sonde steht voreingestellt auf false, deshalb sendet scanning.enabled: true für sich allein genommen weiterhin exakt null Pakete.

Es sind zwei Entscheidungen, weil die beiden Fehlermodi verschieden sind: Ein falscher Geltungsbereich sondiert die falschen Leute, ein falscher Sondensatz sondiert die richtigen Leute zu aggressiv. Eines von beidem zu erweitern, ist immer eine ausdrückliche Änderung an der Datei.

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

Eine Sonde, die scharfgestellt ist, während der Rahmen aus ist, ist ein Startfehler, der beide Schlüssel benennt, und keine Einstellung, die stillschweigend ignoriert wird. Sie stillschweigend zu ignorieren, würde einen Operator, der seine eigene Datei liest, in dem Glauben lassen, ein Scan laufe — oder ihn schließen lassen, das Produkt sei kaputt, wenn keine Ergebnisse erscheinen:

$ 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

Dieselbe Behandlung gilt für eine Sonde, deren Voraussetzung aus ist. Ein Portscan läuft über die Nachbarliste, die die Nachbarerkennung erzeugt; das eine ohne das andere zu verlangen, ist also eine Konfiguration, die laufen und nichts hervorbringen würde — aus Sicht des Operators nicht von einem kaputten Agenten zu unterscheiden:

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

Und den Rahmen einzuschalten, ohne die Berechtigungsbestätigung:

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.

Eine implizite Voreinstellung „scanne alles“ gibt es ebenfalls nicht. allowed_cidrs darf nicht leer sein, wenn der Rahmen eingeschaltet ist, Einträge müssen kanonische Netze sein, und ein 0.0.0.0/0 wird rundheraus abgelehnt:

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

Diese Ablehnung gibt es, weil jeder CIDR-Parser dieser Welt eine Hostadresse mit Netzmaske stillschweigend aufweitet. Der Operator hat eine Adresse geschrieben und 65.534 freigegeben.

scanning.* und security.* können nur aus der Datei kommen​

Einen davon über eine Umgebungsvariable zu setzen, stoppt den Agenten
$ 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.

Das ist der Sinn dieser Bestätigungen, und es lohnt sich, deutlich zu sagen, warum.

Der ganze Wert von i_accept_scanning_authorization liegt darin, dass ein Mensch ihn in eine Datei geschrieben hat, die geprüft, im Diff verglichen und in der Konfigurationsverwaltung gehalten werden kann. Eine Variable, die von einem Shell-Profil, einem systemd-Drop-in, einer Compose-Datei oder einem kompromittierten Elternprozess exportiert wird, ist nichts davon. Könnte die Umgebung ihn setzen, wäre die Kontrolle eine Formalie.

Drei Eigenschaften machen das strukturell, statt daraus eine Regel zu machen, an die sich jemand erinnern muss:

  • Die Umgebung ist für diese Schlüssel keine Quelle. Die Umgebungsbindung ist eine ausdrückliche Allowlist gewöhnlicher Betriebsschlüssel (agent.id, Intervalle, Batch-Größen). Nichts darin kann eine Sonde scharfstellen oder die Transportsicherheit lockern.
  • Die Sicherheits-Teilbäume werden erneut aus der Datei gelesen, und zwar von einem zweiten Loader ohne jede Umgebungsbindung; diese Werte überschreiben, was der Haupt-Loader auch immer erzeugt hat.
  • Ein Versuch führt zur Ablehnung, nicht zur Filterung. Der gesamte Namensraum BITSCANNER_SCANNING_* und BITSCANNER_SECURITY_* ist reserviert, und der Fehler benennt die Variable — denn einem Operator, der glaubt, er habe einen Scan scharfgestellt, darf nicht einfach nichts gesagt werden.

Der reservierte Namensraum ist bewusst weiter gefasst als die Konfigurationsschlüssel: Eine eigene Variable namens BITSCANNER_SECURITY_TOKEN wird ebenfalls abgelehnt. Eine Ablehnung, die die Variable benennt, ist in Sekunden zu beheben; eine Musterliste, die raten muss, welche Schreibweisen zählen, ist es nicht.

Die Sondenleiter​

Alle sieben stehen voreingestellt auf false. Aufgelistet ist die am wenigsten eingreifende zuerst — und jede Zeile sagt, was tatsächlich ins Netz geht, denn ein Operator kann nicht in etwas einwilligen, das nur über seinen Namen beschrieben ist.

SondeWas sie ins Netz sendetBenötigt
neighbor_discoveryNichts. Liest den ARP/NDP-Cache des Kernels und reichert jeden Eintrag aus einer mitgelieferten Offline-Tabelle für MAC/OUI-Hersteller an—
gateway_discoveryNichts Neues. Routingtabelle plus MAC-Zuordnung aus dem ARP-Cache und der Offline-OUI-Tabelle. ⚠️ In 0.1.3-ga und früher nicht zutreffend — es sendete zusätzlich ein ICMP-Echo an jedes gefundene Gateway; siehe die Warnung unten—
reverse_dnsEine PTR-Abfrage je unbenanntem Nachbarn. Das ist ein Paketneighbor_discovery
service_discoveryEine mDNS-Abfrage an 224.0.0.251:5353 und ein SSDP-M-SEARCH an 239.255.255.250:1900. Je ein Paket — und jedes Gerät in der Broadcast-Domäne sieht sieneighbor_discovery
gateway_probeTCP-Verbindungen zu den Ports des Gateways selbst (80, 443, 22, 23, 53; dann 8080/8443 zur Dienstcharakterisierung, mit Banner-Auslesen), dazu ein ICMP-Echogateway_discovery
active_arp_sweepEINGREIFEND. Läuft den nutzbaren Bereich jedes angeschlossenen Subnetzes ab und baut zu einer Handvoll Ports auf jeder Adresse TCP-Verbindungen auf — Hunderte bis Tausende Verbindungsversuche pro Zyklusneighbor_discovery
port_scanAM STÄRKSTEN EINGREIFEND. ~19 gängige TCP-Ports auf jedem entdeckten Nachbarn, dazu Banner-Auslesen bei denen, die eines präsentieren (21/22/25/110/143)neighbor_discovery

Drei davon verdienen mehr als eine Tabellenzeile.

reverse_dns ist ein Schalter, weil es keiner war​

Die Nachbarerkennung wird als der sichere erste Lauf verkauft: die Tabellen des Kernels lesen, nichts senden. Diese Behauptung war früher falsch. Jeder unbenannte Nachbar wurde bedingungslos rückwärts aufgelöst, und es gab keine Konfiguration, die das abschaltete — eine Beobachtung des Scan-Rahmens mit ausschließlich scharfgestelltem neighbor_discovery verzeichnete zwei gewährte Sondierungen pro Zyklus, und das waren genau diese Abfragen.

Eine Rückwärtsauflösung ist ein Paket, und ein aufschlussreicheres, als es aussieht. Sie geht an den Resolver, auf den dieser Host zeigt — in einem Unternehmenssegment oft an einen, den das Sicherheitsteam des Kunden selbst betreibt und protokolliert —, und die Abfrage offenbart genau, welche seiner Hosts dieser Agent aufgezählt hat, ein PTR nach dem anderen. In einem Segment, in dem der Agent ohne Wissen des Netzwerkteams erprobt wird, ist das die Offenlegung, nicht der Portscan.

Sie ist standardmäßig aus, damit der Null-Paket-Modus über die Konfiguration erreichbar ist.

gateway_probe braucht Rechte, die es vielleicht nicht hat​

Die Erreichbarkeitsprüfung öffnet einen Raw-ICMP-Socket (ip4:icmp), was Root oder CAP_NET_RAW voraussetzt. Fehlt das, schlägt der Verbindungsaufbau fehl und der Agent weicht auf ein UDP-Datagramm aus — und dieser Ausweichweg wird vom Guard gesondert geprüft, denn ein nicht freigegebenes Ziel darf nicht dadurch freigegeben werden, dass die erste Methode fehlgeschlagen ist.

Das Gateway ist jemandes Router und oft das eine Gerät, dessen Ausfall das ganze Segment lahmlegt. In Ihrer Routingtabelle zu stehen, ist keine Berechtigung, es zu sondieren — und genau deshalb sind gateway_discovery (passiv, beantwortet „hinter welchem Router sitze ich, und hat sich dessen MAC geändert“) und gateway_probe (aktiv) getrennte Schlüssel.

gateway_discovery öffnete diesen Raw-Socket ebenfalls, in 0.1.3-ga und früher

Die Zeile oben sagt, gateway_discovery bringe nichts Neues auf die Leitung, und der Absatz oben sagt, der Raw-ICMP-Socket gehöre zu gateway_probe. In allen Releases bis einschließlich 0.1.3-ga trifft beides nicht zu.

Der Erreichbarkeitsaufruf lag in dem Zweig, der genommen wird, wenn gateway_probe aus ist. gateway_discovery zu aktivieren und gateway_probe bewusst abzulehnen — genau so sagt ein Betreiber „öffnen Sie keinen Raw-Socket zu meinem Router und zwingen Sie mich nicht, CAP_NET_RAW zu vergeben" — öffnete also trotzdem einen, einmal pro Gateway und Zyklus, mit einem UDP-Datagramm an Port 33434 als Rückfallweg, wann immer der Raw-Socket verweigert wurde.

Was nie betroffen war: Diese Sonde wurde weiterhin vom Scan-Guard freigegeben, konnte also nur eine Adresse innerhalb Ihrer allowed_cidrs erreichen, und wurde mit scanning.enabled: false vollständig verweigert. Der Fehler ist, dass ein als ausgeschaltet dokumentierter Schalter dennoch handelte — nicht, dass irgendetwas die Hülle verlassen hätte.

Wenn Sie 0.1.3-ga oder älter mit aktiviertem gateway_discovery betreiben, nehmen Sie entweder hin, dass Ihr Gateway in jedem Zyklus angepingt wird, oder schalten Sie gateway_discovery ab, bis Sie aktualisieren können. Im Quellcode sendet der passive Pfad jetzt überhaupt nichts mehr: Er meldet Erreichbarkeit nur aus dem vom Kernel selbst aufgelösten ARP-Eintrag, meldet keine Laufzeit, die er nicht gemessen hat, und der Build schlägt fehl, wenn eine andere Funktion als der gateway_probe-Pfad diesen Socket erreichen kann — den Socket an eine Funktion statt an seine Aufrufer zu binden, ist der Grund, warum dies zwei Review-Durchgänge überstanden hat.

active_arp_sweep wird von der Datei begrenzt, nicht vom Host​

Die Subnetze stammen aus den Schnittstellen dieses Hosts, also entscheidet ein VPN oder eine weit gefasste Unternehmensschnittstelle — nicht Ihre Konfiguration —, was vor dem Sweep liegt. allowed_cidrs und min_prefix_length sind das, was ihn eingrenzt. min_prefix_length steht voreingestellt auf 22 (~1.022 Hosts) und wird unterhalb von /16 vollständig abgelehnt: nicht wegen der Pakete, die das Host-Budget begrenzt, sondern weil die Sweep-Schleife bei /8 16 Millionen Adressen ablaufen und für jede einzelne um Erlaubnis fragen würde.

Das gemessene Null-Paket-Ergebnis​

Die Behauptung „Nachbarerkennung allein sendet nichts“ gehört zu denen, die man messen und nicht bloß behaupten muss. Ausgeführt auf einem gewöhnlichen Linux-Host mit scanning.enabled: true, scharfgestelltem neighbor_discovery und allen anderen Sonden aus — der freigegebene Bereich unten wurde durch einen Dokumentationsbereich ersetzt, sonst nichts verändert:

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"]}

Vier aufeinanderfolgende Zyklen, 116–119 echte Nachbarn in 27 angeschlossenen Subnetzen, und probes_allowed_since_start blieb in jedem einzelnen bei 0. Der Scan-Guard wurde nie befragt, weil es nichts zu fragen gab: Die Nachbarn kamen aus den Tabellen des Kernels und die Herstellernamen aus einer mitgelieferten Tabelle. Drei Telemetrie-Datensätze wurden trotzdem pro Zyklus zur Zustellung eingereiht.

So sieht ein erster Rollout aus: den Rahmen einschalten, ausschließlich neighbor_discovery scharfstellen und herausfinden, was auf dem Segment ist, bevor Sie entscheiden, ob etwas Lauteres gerechtfertigt ist.

Die Zähler sagen, auf welche Uhr sie sich beziehen

probes_allowed_since_start und probes_denied_since_start sind Gesamtwerte über die Prozesslaufzeit; hosts_probed_this_cycle und host_budget_remaining_this_cycle werden in jedem Zyklus zurückgesetzt. Früher wurden sie gemeinsam unter einer Überschrift ausgegeben, die behauptete, alle vier seien Werte je Zyklus, und auf einem laufenden Agenten stieg die Zahl der gewährten Sondierungen über die Zyklen hinweg von 686 auf 1142, während die Host-Zahl bei 16 blieb — was sich wie ein eskalierender Scan liest und nichts dergleichen war. Der Guard führt keine Entscheidungszähler je Zyklus, deshalb sind die Gesamtwerte als Gesamtwerte gekennzeichnet, statt für die Log-Zeile eine Zahl je Zyklus zu erfinden.

scanguard: Die Entscheidung fällt dort, wo das Paket hinausgeht​

Jede Sonde wird unmittelbar vor dem Verbindungsversuch freigegeben — nicht dann, wenn die Zielliste aufgebaut wird.

Warum dieser Unterschied das ganze Design ausmacht

Eine gefilterte Zielliste ist eine Konvention, und Konventionen sind genau das, woran künftige Aufrufpfade still vorbeiführen. Jemand fügt einen Codepfad hinzu, der seine eigene Liste aufbaut, oder verwendet eine Adresse aus einer Antwort wieder — und der Filter, der drei Funktionen früher angewendet wurde, gilt dafür nicht. Die Prüfung dorthin zu legen, wo das Paket hinausgeht, bedeutet: Ein neuer Aufrufpfad fragt entweder den Guard, oder er lässt sich gegen die Infrastruktur des Collectors gar nicht erst übersetzen.

Der Guard verweigert auf jeder Ebene, statt durchzulassen: Ein Guard ohne jede Konfiguration verweigert alles; ein nil-Guard, der einem Collector übergeben wird, wird durch „alles verweigern“ ersetzt statt umgangen; eine Adresse, die sich nicht parsen lässt — oder mehrdeutig parst —, wird verweigert; erschöpftes Budget, erschöpfte Taktung und abgelaufene Frist führen zur Verweigerung, nicht zu einer abgeschwächten Ausführung.

Jede Entscheidung, Gewährung wie Verweigerung, wird mit einem maschinenlesbaren Grund festgehalten:

AbgelehntFestgehaltener Grund
Aktives Scannen ist ausscanning_disabled
Das Ziel lässt sich nicht parsenunparseable_target
Loopback, Link-Local, Multicast, unspezifizierte Adresse, Netz- oder Broadcast-Adresse des freigegebenen Bereichsreserved_address
Alles außerhalb von allowed_cidrsoutside_allowed_cidrs
Öffentlicher Adressraum ohne allow_public_targetspublic_target_not_allowed
max_hosts_per_cycle erreichthost_budget_exhausted
max_cycle_duration erreichtcycle_duration_exceeded
Ein Subnetz, das weiter ist als min_prefix_length, oder außerhalb des Geltungsbereichssubnet_wider_than_min_prefix, subnet_outside_allowed_cidrs
Eine Multicast-Gruppe, die keine bekannte Discovery-Gruppe istnot_a_discovery_multicast_group
Der Agent fährt herunter, oder der Kontext des Zyklus wurde mitten in der Sonde abgebrochencontext_cancelled

Auch Gewährungen tragen einen Grund — in_scope_private, in_scope_public_explicitly_allowed, link_local_discovery_group, subnet_within_sweep_limits —, sodass der Audit-Trail sagt, warum eine Sonde erlaubt war, und nicht bloß, dass sie es war.

Die Ablehnungen gelten auch dann, wenn die Allowlist sonst zutreffen würde. Das prüft die Testsuite des Guards direkt, und sie läuft auf dem ausgelieferten Commit durch: Loopback (v4 und v6), Link-Local (v4 und v6), Multicast einschließlich der SSDP-Gruppe, die unspezifizierte Adresse, die begrenzte Broadcast-Adresse sowie Netz- und Broadcast-Adresse des freigegebenen Bereichs werden allesamt verweigert, obwohl sie innerhalb von allowed_cidrs liegen; ein privater Host im Geltungsbereich wird ohne das Opt-in für öffentliche Ziele gewährt; und eine IPv4-gemappte IPv6-Adresse kann die Allowlist nicht umgehen.

0 probes sent, 4094 denied outside_allowed_cidrs ist keine Fehlfunktion. Es ist der für den Operator sichtbare Beleg dafür, dass die Allowlist das ist, was entscheidet.

Die Obergrenzen sind Teil des Rahmens​

SchlüsselVoreinstellungGrenzenWas sie zurückhält
min_prefix_length2216–32Das weiteste Subnetz, das ein Sweep ablaufen darf
max_hosts_per_cycle10241–65536Verschiedene Hosts, die pro Zyklus sondiert werden, über alle Subnetze hinweg
max_concurrency161–256Gleichzeitig laufende Sondierungen, vom Guard selbst durchgesetzt
per_probe_delay50ms0–10sMindestabstand zwischen Sondierungspaketen — je Sonde, sodass ein 19-Port-Scan eines einzelnen Hosts 19-mal getaktet wird
max_cycle_duration5m≤ 1hEchtzeit-Obergrenze für das Sondieren innerhalb eines Zyklus
allow_public_targetsfalse—Sondieren außerhalb von RFC1918 / RFC6598 / RFC4193

Das sind keine Tuning-Vorschläge. Ein max_concurrency von 10.000 ohne Verzögerung über ein /22 ist ein Denial-of-Service gegen das eigene Segment des Kunden, ausgeliefert von einem Binary, das er auf unsere Empfehlung hin installiert hat — deshalb werden die Werte beim Laden auf ihren Bereich geprüft und außerhalb davon abgelehnt.

Nächste Schritte​

  • Was den Host verlässt — was die entdeckten Datensätze tatsächlich enthalten, wie sie reisen, und die Grenzen, mit denen dieses Release ausgeliefert wird.
  • bitscanner — Überblick — was ein Zyklus hervorbringt, was er kostet, und wie Sie den Download verifizieren.
  • Die Guards von bitenforcer — derselbe Design-Instinkt, angewandt auf einen anderen Wirkungsradius: messen, benennen und lieber verweigern als raten.
  • Asset Management — wo entdeckte Geräte zu Assets mit einer verantwortlichen Person werden.

War diese Seite hilfreich?