Security teams keep getting the same assignment: inventory the AI tools employees are actually using. And they keep getting it with the same constraints — no endpoint agents deployed, no TLS inspection allowed, and no appetite for a months-long rollout before the first answer arrives. Employees adopt new AI services weekly; the policy question ("who is using AI, and which services?") can't wait for an agent deployment project, and in many regulated environments payload inspection is politically or legally off the table anyway.
Passive network detection answers the inventory question from traffic alone: no agent, no TLS interception, no payload access. A sensor watches a mirrored copy of network traffic and reports which devices are talking to which AI services. It is the zero-friction first step of AI governance — and it has real, documentable limits.
This post covers both: the architecture we run in production (including the capture path that took us two attempts and the sensor failure that taught us the most), and the honest boundary of what passive detection can and cannot see. The sanitized Zeek configuration, detection script, and AI-domain list from this post are open source at github.com/AIQSO/zeek-ai-detection.
The Four Signals
Everything passive detection knows comes from four signals, each with a built-in limit.
DNS queries. Before a device talks to an AI service, it usually resolves the service's domain. Watching DNS tells you which hosts resolve AI-provider domains and how often — a usage-frequency picture per device. The limit: DNS-over-HTTPS (DoH) and DNS-over-TLS hide queries from passive view. But DoH use is itself observable, and a host that switches its DNS into an encrypted channel is a policy signal worth surfacing on its own.
TLS SNI. Almost all AI-service traffic is TLS, and the Server Name Indication field in the client handshake names the service in cleartext — so you can see which AI service an encrypted session is talking to without touching the encrypted content. The limit: Encrypted Client Hello (ECH) will erode SNI visibility over time. As with DoH, we treat ECH adoption itself as a signal: it tells you where visibility is being lost.
JA3/JA4 fingerprints. The way a TLS client negotiates its handshake fingerprints the client software. That distinguishes a Python SDK from a CLI tool from a browser — which matters, because "a developer wired our data pipeline into an external AI API" is a different governance conversation than "someone uses a chatbot in a browser tab." The limit: fingerprints classify the client type. They never identify the person, and they never read the session.
Flow metadata. LLM responses stream. That produces a recognizable connection shape — long-lived sessions with a server-push cadence — visible in flow records without any payload access. The limit is the point: prompts and responses are never visible from passive traffic. Reading content would require a TLS-inspecting proxy or an endpoint agent, which this architecture deliberately is not.
Attempt 1: The SPAN Hack That Failed
Our first capture path never touched the switch. We used Linux tc (traffic control) with a mirred action on the hypervisor to copy traffic from a host interface into the sensor container — a software SPAN port, assembled from qdisc filters.
It worked when we built it. Then it silently degraded. tc mirror rules don't survive contact with operational reality: interface reconfigurations, host reboots, bridge changes — each can quietly detach the mirror, and nothing tells you it happened. There is no alarm for "your qdisc filter no longer exists." The sensor keeps running, its pipeline keeps reporting healthy, and your AI inventory quietly stops reflecting the network.
The lesson we took: if your capture path can fail silently, it will — and a monitoring system fed by a dead capture path is worse than no monitoring, because it looks like coverage. We removed the tc mirror entirely and moved the mirroring to where it belongs: the switch.
Attempt 2: A Switch Mirror Port Done Right
The production capture path has four pieces, and each one encodes a lesson.
1. Mirror on the switch, inside the NAT boundary. On the aggregation switch, one port is configured as a mirror (port-level "Mirroring" operation) with the LAN-side uplink as its source — the port that carries the traffic between your devices and the gateway. One placement warning that costs people weeks: do not mirror the WAN side of the gateway. That traffic is post-NAT, so every flow appears to originate from the gateway itself and per-device attribution — the entire point of the exercise — is lost.
2. A dedicated capture NIC, broken out of the bond. The mirror port cables to a NIC on the hypervisor that we permanently removed from the LACP bond. A bonded NIC cannot be a clean capture NIC — the bond owns it. This NIC now looks unused in every inventory, which is exactly why we documented it as load-bearing: "never re-add this NIC to the bond" is written down where the next cleanup pass will find it.
3. An IP-less bridge with bridge-ageing 0 — the landmine. The capture NIC connects to the sensor container through a Linux bridge with no IP address, pure layer 2:
auto vmbr2
iface vmbr2 inet manual
bridge-ports enp5s0f3
bridge-stp off
bridge-fd 0
bridge-ageing 0
That last line is the single most common silent failure in bridge-based capture. A default Linux bridge is a learning bridge: it watches source MACs and then forwards frames only toward the port where each destination MAC lives. Mirrored traffic breaks that assumption — the bridge learns the mirrored frames' MACs on the capture-NIC side, concludes those destinations live there, and stops flooding frames to the sensor port. With default ageing, the sensor goes quiet minutes after boot while every line of configuration looks correct. bridge-ageing 0 disables learning and forces flood-always behavior.
4. A promiscuous sensor interface. The mirrored frames are addressed to other hosts' MACs, so the sensor's capture interface must be promiscuous or the kernel drops everything before Zeek sees it. Zeek's node.cfg then points at that interface:
[zeek]
type=standalone
host=localhost
interface=eth1
With this path live, our pilot sensor receives roughly 730 packets per second of mirrored LAN traffic. The acceptance test we set was end-to-end and falsifiable: a workstation — not the hypervisor, a separate physical machine — had to appear in the AI inventory attributed to a specific AI provider. It did, within minutes of the mirror going live.
The ssl.log Moment
Before the proper mirror existed, our Zeek ssl.log was empty, and we spent real time treating that as a bug — checking analyzer configuration, checking the SSL scripts were loaded, suspecting version issues.
It was not a bug. The sensor simply had no TLS traffic in view: watching only its own host's interface, there were no third-party TLS handshakes to log. The moment mirrored traffic arrived, ssl.log populated with SNI values and SNI-based AI detection started working — with zero configuration changes.
The lesson generalizes: an empty Zeek log is a visibility question before it is a configuration question. Confirm the sensor can see traffic between other hosts (tcpdump on the capture interface is enough) before touching a config file.
Postmortem: The Sensor That Died Quietly
The most instructive failure came after everything worked. Two problems compounded into one very confusing week.
The OOM. Our sensor container ran Zeek with a 1 GB memory limit — fine when it watched one host's traffic, not fine under full-LAN mirror load. Zeek crashed out of memory. The log forwarder running next to it kept publishing heartbeats, so every pipeline dashboard showed a live, healthy sensor producing nothing. We chased a "silent drop" alert through the ingestion pipeline before anyone checked whether the Zeek process was actually running. The fix was mechanical (4 GB memory limit); the lesson was not: monitor the process, not just the pipeline. A healthy forwarder in front of a dead sensor is the most convincing lie your monitoring will ever tell you.
The error mask. Diagnosing the crash took longer than it should have because of a bug we'd shipped in our own ingest API. The bulk-ingest endpoint reported every failed event with the same hardcoded reason string — the ingestion function caught every exception and collapsed it into a boolean false, so the endpoint could only guess at why. That masking had already hidden a Redis connection-pool exhaustion for 12 days; during the Zeek crash investigation it sent us down the wrong path again. The fix: let ingestion errors propagate with their true exception class, and put that class name in the per-event reject reason. Never collapse distinct failure modes into one error message — the exception class is the first thing triage needs.
Both failures share a shape. Passive detection systems fail toward silence: a detached mirror, a learning bridge, a dead process behind a live forwarder, a masked exception. If you build one, spend your engineering budget on making silence loud.
Detection Limits: The Honest Table
Every detection approach has limits. Most vendors bury theirs. We think stating them plainly is what makes the inventory trustworthy — an auditor's first question is "what does this not see?", and you should have the answer written down before they ask.
| Signal | What it sees | What it cannot see |
|---|---|---|
| DNS queries | Which hosts resolve AI-provider domains, and how often | DoH/DoT hides queries — though DoH use is itself a flaggable policy signal |
| TLS SNI | Which AI service an encrypted session is talking to | ECH will erode SNI over time — ECH adoption is itself an evasion signal |
| JA3/JA4 | Whether the client is an SDK, a CLI, or a browser | Who the person is, or anything inside the session |
| Flow metadata | Streaming-response patterns characteristic of LLM traffic | Prompts and payloads — never visible passively |
And the hard blind spots, which no amount of tuning removes:
- Local models generate no external traffic. Ollama or llama.cpp running on a workstation is invisible to network detection by definition. If local AI matters to your policy, you need a different control.
- VPNs, personal hotspots, and cellular paths bypass the monitored network entirely.
- Communication, never content. Passive detection sees that a device talks to an AI service — never what was said.
- Sensor placement bounds everything. Traffic that never crosses the mirrored link does not exist to the sensor. In our own pilot, wireless devices on a different aggregation switch were a documented coverage gap until a second sensor and a DNS-level source could close it. Draw your coverage map honestly, gaps included.
What This Buys You
Passive AI-service detection is not the end state of AI governance. It is the correct first state: a device-level AI inventory you can stand up in hours — no endpoint agents, no TLS interception, no user-visible change — with boundaries you can state precisely to leadership or an auditor. From that inventory, the rest of governance becomes concrete: you write policy against observed usage instead of guesses, and you know exactly which gaps (local models, off-network paths) need a different control.
The Zeek detection script, the curated AI-domain list, and the capture-path configuration from this post — including the bridge-ageing 0 landmine and the verification checklist — are open source at github.com/AIQSO/zeek-ai-detection. The production version of this pattern — device attribution, JA3/JA4 fingerprinting, flow-shape heuristics, risk scoring, and dashboards on top of the same passive foundation — ships as Faron, our agentless shadow-AI detection product.
If you're facing the "who is using AI?" question in a regulated or security-conscious environment, we've been through the failure modes so you don't have to be. Book a call and we'll walk through what passive detection would and wouldn't see on your network.