bitmapper
bitmapper answers "what is this machine actually talking to, and which process is doing it?"
bitcollector reads the host's state. bitscanner looks outward at the segment. bitmapper
watches the traffic itself — live packets on the host's own interfaces, tracked into flows
and attributed to the process that owns the socket.
bitmapper delivers flow records, not traffic. As of 0.1.0-ga the capture engine is
connected to the exporter, and tracked connections reach the platform as flow records —
endpoints, ports, protocol, byte and packet counters, connection state, and — when it can be
determined at all, which today is seldom — the attributed process.
No payload bytes, no packet captures, and not the process command line, which
routinely carries credentials as arguments. If you were expecting a PCAP to arrive somewhere,
that is not what this agent sends.
Process attribution barely works in the published binary, and the fix is not released yet.
In 0.1.0-ga attribution is attempted once, at connection creation, against a socket-table
snapshot that cannot contain a new outbound connection's socket, and there is no
listening-socket inference at all — so flows commonly arrive with no process (measured on the
development host: 0 of 12 attributed), and destination_process is never populated. The
replacement — per-flow backfill, both ends, and an attribution field marking a direct socket
match (socket) apart from an inference (listening_socket) — is unreleased. That
inference is guarded: it is drawn only for an address on this host, only where exactly one
process is proved to hold the listening socket, and only for TCP, so a UDP flow never carries
it. Read process attribution before you plan around any of it.
It is one binary, on one platform. 0.1.0-ga is published for linux/amd64 only.
There is no macOS, Windows or arm64 download, and the reasons are
stated below rather than left as a gap on a downloads page.
It does not capture every protocol by default. capture.protocols defaults to
["tcp", "udp"] whether or not your configuration file mentions it, so a stock deployment sees
no ICMP, ICMPv6, SCTP, GRE, ESP or AH — the kernel drops them and nothing in the output says so.
Widening it is one line.
Parts of this agent are still not built. There is no service-topology aggregation, no
Prometheus metrics, and no network-policy detection; several configuration keys parse and do
nothing, and so do all of the command-line flags except --config. What is not
wired is the list, and it is worth reading before you plan around a key.
| Release | 0.1.0-ga, commit 64ebc64, built with go1.25.12 |
| Platforms | linux/amd64 only — macOS, Windows and arm64 are not published, and why |
| Supply chain | Reproducible build, CycloneDX SBOM, cosign signature, SBOM attestation, signed SHA256SUMS — same family key as the other bits |
| Status | Captures, attributes and tracks connections; exports flow records to the platform |
| Inbound network surface | Nothing listens. The HTTP/WebSocket and gRPC servers exist in the tree but are never constructed, and both ship disabled |
| Outbound | https only — registration, heartbeat, and flow records to the endpoint you configure |
| Privileges | Elevated: root, or CAP_NET_RAW + CAP_NET_ADMIN |
| Build requirement | CGO and libpcap, linked statically into the published binary |
Where it sits among the bits
The four agents answer four different questions, and the boundary is what each one looks at:
| Looks at | Answers | |
|---|---|---|
bitcollector | The host's own state | "What is this machine, and what state is it in?" |
bitscanner | The segment around the host | "What else is on this wire that I do not know about?" |
bitmapper | The packets crossing the host's interfaces — TCP and UDP by default, more if you ask | "What is this machine talking to, and which process is doing it?" |
bitenforcer | The host's configuration | "Does this machine match the policy I set?" |
A neighbour table tells you a device exists. A capture tells you it is being talked to, by what, and how often — which is the difference between an inventory and a dependency map.
What works today
Present and wired into the capture command's code path:
- Live packet capture — libpcap through
gopacket, statically linked into the published Linux binary, with BPF filter, snaplen, promiscuous mode and capture buffer size. - Kernel-side capture filtering —
capture.filter,capture.protocols,capture.portsandcapture.exclude_portsare compiled into one BPF expression and enforced by the kernel, so excluded traffic is never copied into the agent at all. The compiled expression is logged at start-up. Note the default:capture.protocolsis["tcp", "udp"]even when your file omits it, which means ICMP, SCTP, GRE and ESP are not captured unless you say so. See filtering. - Multi-interface capture — a named list of interfaces, or every interface that is up and non-loopback when you name none.
- Process attribution — present, but barely working in the published binary. PID, process
name, executable path and user, resolved from the host's socket tables. In
0.1.0-gathe lookup is attempted once, when the connection is created, against a snapshot that cannot contain a new outbound connection's socket, so flows commonly carry no process at all;destination_processis never populated. Per-flow backfill, attribution of both ends, and theattributionconfidence field (socketvslistening_socket) are not in the published binary, which has no listening-socket inference whatsoever — see process attribution for what ships today and what replaces it. That replacement also declines rather than guesses: it takes no identity from a listening socket unless the address is on this host and exactly one process is proven to hold the socket — one nginx socket on the development host is held by 17 processes — and it never infers for UDP, which has no LISTEN state. A blank process field is a refusal, not a failure. - Stateful connection tracking — bidirectional flow matching, a TCP state machine
(
SYN_SENT→ESTABLISHED→ … →CLOSED), per-connection packet and byte counters, a size-capped table with eviction, and periodic garbage collection of idle connections. - In-process event stream — packet, new-connection and statistics events fanned out to subscribers inside the process.
- Periodic statistics — packet, byte, drop and connection counters written as structured
JSON on
performance.stats_interval. - Control-plane integration — agent registration and heartbeat when
control_plane.*is configured. - Flow export — every tracked connection is handed from the capture path to the exporter
and POSTed to the platform as a flow record. Every flow produces at least one record, and its
last record is marked final with complete counters; a long-lived connection is refreshed once
per
batch_export.batch_intervalif its counters moved, so you see it before it ends. - Export filtering that actually filters —
output.filter(exclude_cidrs,exclude_loopback,protocols,ports) is applied at intake, before a record is built, so an excluded flow is never held in memory and can never be sent. Exclusions match either endpoint: excluding10.0.0.0/8means "do not tell Cert-IX about that network", and reporting that10.1.2.3opened a connection outward tells them about10.1.2.3just as completely. An entry that cannot be compiled refuses the run rather than degrading to "export everything". - Fail-fast configuration validation — the agent refuses to start on an invalid or insecure-by-omission configuration rather than starting in a weakened state.
What is not wired
Stated explicitly, so nobody has to discover it by reading the source:
- The HTTP/WebSocket and gRPC servers are never constructed. Because nothing builds them,
the security middleware behind them — authentication, IP allowlist, rate limiting, audit
logging — does not run. Do not treat the
security.*configuration as protection today. The upside is the honest one: with no server constructed, the agent exposes no inbound port. - No service-topology aggregation. The
ServiceTopologyandServiceEdgetypes exist but are never built, so there is no topology graph yet. - No Prometheus metrics and no
/metricsendpoint. - No network-policy detection.
Configuration keys that are validated but do nothing
These parse, and the agent will accept them, but no code consumes them:
output.backend, output.kafka, output.nats, output.redis, output.webhook,
output.file, kubernetes.*, storage.*, logging.*, performance.batch_size,
performance.connection_pooling, performance.memory_limit, performance.cpu_limit.
Command-line flags belong in the same category, and there are more of them than of keys.
--log-level and --log-format are registered, documented in --help, and read by nothing:
the logger is a fixed zap production logger (JSON, info level). The capture flags
(--interfaces, --filter, --snaplen, --promiscuous, --buffer-size, --workers,
--queue-size, --stats-interval) are bound under their flag names while the configuration is
read from nested keys, so they are parsed and ignored; --enable-grpc and --enable-http
default to true in --help and refer to servers that are never constructed. --config is
the only flag that changes behaviour. Do not take bitmapper --help as a statement of what
this release does.
Three things are not in that list, and the distinctions matter:
output.filteris read and enforced on the export path as of0.1.0-ga. It used to be in this list, and while the agent exported nothing at all that was merely dead weight. Once flow records began leaving the host it would have become a false privacy claim — an operator excluding a sensitive subnet getting no protection, no error, and no way to tell. It is now applied at intake. Four keys that could never act on anything (export_packets,export_stats,min_packet_size,require_payload) were removed from the schema instead of left parsing, and the agent names them at start-up if your file still sets one.capture.protocols,capture.portsandcapture.exclude_portsare compiled into the kernel BPF filter and enforced there. An earlier version of this page listed all three as validated-but-inert, and that was wrong — see the correction below.capture.exclude_interfacesis read and applied on the live capture path. See choosing interfaces.
capture.exclude_ports is a working privacy controlUntil 0.1.0-ga this page named capture.protocols, capture.ports and
capture.exclude_ports among the keys that "parse and do nothing", and the
capture page repeated it. That was the opposite of the truth: all three
compile into the BPF filter and are enforced by the kernel before a packet reaches the agent.
capture.exclude_ports is the one to act on. The configuration bitmapper ships sets
exclude_ports: [22] — "do not capture SSH" — and it works. If you read the old text and
deleted the key as dead weight, you removed a control that was protecting you and started
collecting traffic you had deliberately excluded, with nothing to indicate the change. Check
what your deployed configuration says before you assume you were unaffected.
capture.protocols needs the opposite kind of attention: it defaults to ["tcp", "udp"] whether
or not you set it, so a stock deployment silently captures no ICMP, ICMPv6, SCTP, GRE, ESP or
AH. The protocol default explains how to widen it.
They are part of the configuration schema and are validated for shape, so a file written against them loads. That is exactly why they are listed here: a key that parses cleanly and does nothing is the kind of thing that reads as a working feature. Assume a key has no effect unless it appears under what works today.
The Kubernetes enrichment package was removed rather than left dormant. It had no importers
and built its own HTTP client, which followed redirects and could leak a service-account bearer
token to a plaintext destination when a kubeconfig pointed at a redirecting server. The
kubernetes.* block remains in the schema and is now consumed by nothing.
Running it
bitmapper is a single binary with three commands:
# What you have
bitmapper version
# Bitmapper 0.1.0-ga
# Git Commit: 64ebc64
# Build Date: 2026-08-10T05:56:16Z
# Which interfaces this host can capture on
bitmapper interfaces
# Capture, with the configuration you supply
bitmapper capture --config /etc/bitmapper/config.yaml
--config, --log-level and --log-format are persistent flags on the root command, but
only --config does anything. There is no way to turn up logging detail while you are
setting it up: the logger is fixed at JSON, info level. What the agent does tell you at start-up
is the compiled capture filter and the interfaces it selected, and on
performance.stats_interval it logs packet, byte, drop and connection counters — that is the
evidence to read while you are commissioning a host.
It needs elevated privileges
Packet capture is privileged. On the published platform:
| Platform | What it needs |
|---|---|
| Linux | CAP_NET_RAW and CAP_NET_ADMIN (or root) |
It needs CGO — and the published binary already has it
Capture goes through gopacket/pcap, which is a C library binding. A build with
CGO_ENABLED=0 still compiles, but capture and interface listing refuse at runtime:
packet capture requires CGO (build with CGO_ENABLED=1 and libpcap installed)
That is deliberate — a binary that silently captured nothing would be worse than one that says why it cannot. This is also the single fact that decides which platforms are published, and it is why the release pipeline refuses any binary that was not built with CGO, is not statically linked, or has no libpcap inside it. See platforms.
The download you get needs no libpcap installed on the host: libpcap is linked statically into the published binary.
The enrolment credential
If you configure control_plane.*, the enrolment credential is read from
control_plane.enrollment_token_file. The agent requires it to be a regular file, mode 0600
or tighter, owned by root or by the agent's own user, and inside a directory that is not
group- or world-writable. A directory anyone can write to lets someone replace the file, so
checking only the file's own mode would be checking the wrong thing.
The check is POSIX-based. That covers every published binary, since 0.1.0-ga ships on Linux
only — but it is worth knowing it is a property of the platform rather than of the agent, if a
Windows build ever appears.
control_plane:
gateway_url: "https://<your-cert-ix-agent-endpoint>"
ingest_url: "https://<your-cert-ix-ingest-endpoint>"
tenant_id: "<your-tenant-id>"
enrollment_token_file: "/etc/bitmapper/enrollment.token"
Both URLs must be https://, and ingest_url is required once the control-plane block is
in use at all. tenant_id is sent as the X-Tenant-ID header — it is an identifier, not a
credential, so it is not what authenticates the agent.
The credential can also come from the environment as
BITMAPPER_CONTROL_PLANE_ENROLLMENT_TOKEN, or be replaced entirely by a mutual-TLS client
certificate (control_plane.client_cert_file / client_key_file, with the key file getting the
same permission check). Setting control_plane.enrollment_token inline in YAML works and is
discouraged — a secret in a config file is a secret on disk.
Configuration
bitmapper reads a YAML configuration file, conventionally /etc/bitmapper/config.yaml,
passed on every command:
bitmapper capture --config /etc/bitmapper/config.yaml
The keys that decide what it actually does are covered in detail on this page: which
interfaces it binds to under Choosing interfaces, and what it is
permitted to observe under Filtering — four keys that compile down to a single
kernel filter. The enrolment token is referenced by enrollment_token_file rather than
written inline.
Not every key in the file is live. Configuration keys that are validated but do nothing lists the ones the loader accepts and then ignores, so you can tell a real setting from a leftover.
What it costs on the host
Packet capture is the most expensive thing any of the bits do, and the cost scales with traffic rather than with host size. Two controls matter most:
- A BPF filter is the cheapest possible reduction — it drops packets in the kernel before they are ever copied to the agent.
- The connection table is size-capped with eviction and periodic GC of idle entries, so a host seeing many short-lived connections does not grow the table without bound.
performance.memory_limit and performance.cpu_limit are not implemented — they are in the
list above. Bound the process with your init system's controls (MemoryMax=, CPUQuota= in a
systemd unit) if you need a real ceiling.
Flows held awaiting export are bounded separately by batch_export.max_buffered_connections,
not by performance.connection_table_size — whose default would put a second six-figure
table on a capture host. Every drop is counted and logged; none of them is silent.
Platforms
0.1.0-ga publishes one signed binary. That is the narrowest platform list in the family,
and every omission below is a measured decision rather than an oversight:
| Platform | Published | Why not |
|---|---|---|
Linux amd64 | Yes | |
Linux arm64 | No | Cannot be assembled by an amd64 gcc, and there is no arm64 builder |
macOS amd64/arm64 | No | Needs clang and the macOS SDK |
Windows amd64 | No | Builds, but has never been run on Windows — see below |
The single cause behind all of it is that bitmapper is the one bit that cannot be
cross-compiled. Capture is gopacket → libpcap → CGO, so a build needs a real C toolchain and
a real libpcap for the target. Every other bit is pure Go, and GOOS/GOARCH is all it takes.
The trap this creates is specific and worth naming. internal/capture carries a //go:build !cgo stub that compiles on every platform and captures nothing. A CGO_ENABLED=0 build
therefore succeeds — it produces a binary that starts, registers, heartbeats, reports healthy,
and never sees a packet. So the release pipeline gates on the artefact rather than on the build
command: it refuses any binary that was not built with CGO, is not statically linked, or has no
libpcap inside it.
Windows is the interesting one, and the honest one. It genuinely builds — gopacket's Windows
support is pure Go and loads wpcap.dll at runtime — so we could publish it today. It has
never been run on Windows. That makes it a testing gap rather than a toolchain gap, and
publishing it would mean shipping a signed binary whose capture path no one has ever watched
work. A signed binary that cannot capture is worse than no binary, because the signature is
read as a statement about fitness. It is withheld until someone has run it.
If you need this data from a macOS, Windows or arm64 host today, bitmapper is not the answer
for that host. bitcollector ships on all five platforms and reports
that host's established network peers — a coarser answer than per-flow attribution, but a real
one on the machine you actually have.
Next steps
- How capture works — interfaces, BPF filters, process attribution and the connection state machine, and what each of them puts in memory.
- bitscanner — the agent that finds devices your inventory does not contain.
- bitcollector — the inward-facing host inventory.
- Verifying your downloads — the trust anchor, and why it matters more than the signature.
- What is bits? — how the agents fit together.
War diese Seite hilfreich?