The security model, and the sharp edges
Most of l2trace is a read-mostly monitor: it collects CAM tables and reconstructs L2 paths. A bug there is a wrong answer on a dashboard. This page is about the other half, the part that earns a security review, because l2trace grew a network access-control plane: a TACACS+ device-admin server and an 802.1X / RADIUS server that, in enforcement mode, decide who may log into a switch and which endpoint may onto which port. A bug there does not lose a data point. It locks an engineer out of the fabric during an incident, or lets an unknown device onto a hospital network. The entire design is organized around that asymmetry, and this page is the tour a reviewer who assumes the worst should take.
Why review it as a security system
Section titled “Why review it as a security system”The services that make decisions hold credentials for every managed device and answer packets from the network with an authorization. So the assets are: the per-device credentials, the shared secrets that authenticate the network devices, the topology and endpoint data (operationally sensitive: MACs, IPs, where things physically plug in), and above all the integrity of the accept-or-reject decision itself.
Attack surface
Section titled “Attack surface”- Collector to device: SNMP (v2c today), SSH / NAPALM, gNMI, NETCONF. Read-only against the devices, one credential per device.
- Event bus: NATS JetStream between collectors and the reconciler.
- Datastore: PostgreSQL with the AGE graph extension, bitemporal and append-only.
- AAA listeners, the exposed decision surface: TACACS+ on TCP/49, RADIUS on UDP/1812-1813, CoA / Disconnect on UDP/3799.
- Web UI / API: read-only FastAPI behind a TLS reverse proxy, optional OIDC SSO, with a LAN-only overlay that refuses non-RFC1918 sources.
- Control-plane input: Nautobot is the source of intent; l2trace caches it and decides locally.
Credentials and secrets
Section titled “Credentials and secrets”No plaintext secrets live in the database or in git. Every secret is referenced by
a URI (env://NAME, file://path, literal://...) and resolved at use time
through one secrets layer, so the database stores the reference, not the value.
SNMP communities, RADIUS shared secrets, and device passwords all sit behind that
indirection. On the split-host collection topology, communities are resolved in
the remote poller’s own environment and never reach the central database.
Passwords are never logged, never cached, never stored. An auth failure records a credential-free reason: the username is fair to log, the password is not. Vault and Vaultwarden backends are on the roadmap; today the backends are env, file, and literal, which is honest but puts secret hygiene on the operator.
The access-control services
Section titled “The access-control services”Both ship monitor-first and are not deployed in production. Enforcement is triple-gated: a per-device flag AND a global “enforcement permitted” AND NOT global monitor-mode. All three must be true for a single port to be enforced, and that is proven on lab switches before any production port.
TACACS+
Section titled “TACACS+”The full lockout doctrine has its own page,
and it is the one thing to read before enabling enforcement. The short version: an
internal fault (database down, a bug, a timeout) returns ERROR, not FAIL, so the
network device falls through to its next method (local) instead of locking every
admin out of every switch. A real wrong password returns FAIL, which is
authoritative. A buggy AAA server is more dangerous than a dead one, and the code
treats that distinction as an invariant. A mandatory local break-glass account is
configured and verified on the device first, before AAA is ever pointed at l2trace.
TACACS+ body obfuscation is MD5-pad, which is the protocol’s mechanism (RFC 8907),
not encryption and not our choice. It is weak by design; the mitigations are a
trusted network path and a TACACS_REJECT_UNENCRYPTED option that refuses
unobfuscated bodies. Command authorization sets are matched with RE2 (linear-time
regex) so an operator’s pattern cannot catastrophically backtrack and stall the
auth path, because ReDoS on the decision plane is an availability attack.
802.1X / RADIUS
Section titled “802.1X / RADIUS”MAB, plus EAP: PEAPv0 / EAP-MSCHAPv2 over a real TLS tunnel, provisioning, and CoA / Disconnect. Three properties matter to a reviewer:
- Message-Authenticator is enforced. That is the BlastRADIUS mitigation (CVE-2024-3596): an Access-Request without a valid HMAC Message-Authenticator is dropped, so a source-IP spoof does not produce a verifiable reply.
- The policy engine is fail-safe by construction. First-match, weight-ordered, AND-ed conditions, and no match never denies. A missing or malformed policy cannot silently become a reject. Opt-in regex conditions are again RE2.
- A control-plane outage does not stop authentication. Nautobot authors policy; l2trace caches it and decides locally, so a Nautobot outage never stops 802.1X. The reasoning is on the Nautobot resilience page. The auth path must not have a hard dependency on a system that can be down.
Monitor mode makes zero network changes: it Accepts with no VLAN so a switch running
authentication open leaves every device where it is, and the value is the recorded
discrepancy (unknown MAC, or observed port and VLAN not matching intent).
What the crypto actually guarantees
Section titled “What the crypto actually guarantees”- MSCHAPv2 (RFC 2759): the pure-Python MD4 / DES / SHA1 construction reproduces the RFC 2759 section 9.2 known-answer vectors byte for byte (pinned in tests), and a live supplicant accepted the authenticator response, so mutual auth is real. Be clear-eyed: MS-CHAPv2 as a primitive is cryptographically weak. Its security under PEAP comes from the outer TLS tunnel, not the inner method. For a stronger option, EAP-TLS (mutual certificates) is shipped: the client presents a certificate that must chain to a configured trust anchor, with optional CRL revocation, and there is no weak password primitive in the path at all. See Authenticate with client certificates.
- PEAP TLS: pinned to TLS 1.2 for a deterministic record flow; server cert only today.
- MPPE session keys (wireless): derived from the PEAP TLS master secret using the
classic EAP-TLS PRF (RFC 5216 / RFC 2548 / RFC 3079). The PRF is validated against
OpenSSL’s TLS1-PRF byte for byte, and two independent TLS endpoints derive the
identical MSK, which is an interoperability proof, not just self-consistency. Two
honest notes. First, Python’s
sslexposes the master secret only through a key-log file, so the server routes it through a private 0600 temp file it reads once and unlinks the instant the handshake completes, bounding the secret’s lifetime on disk to the handshake window. Second, keys are emitted only on a genuine auth success: in monitor mode a bad credential still Accepts so a wired port is not cut, but it carries no usable keys, because on wireless an Access-Accept with keys is access, and handing keys to a bad password would authenticate it.
Fail-safe posture, in four lines
Section titled “Fail-safe posture, in four lines”- Internal fault becomes ERROR becomes fall-through to local. Never fail-closed on the whole fleet because of a bug in the monitor.
- No policy match never becomes a deny.
- Control plane (Nautobot) down, the local cache still decides.
- Enforcement is off by default and triple-gated; monitor-first everywhere.
Data handling
Section titled “Data handling”Bitemporal storage (valid-time and recording-time) gives an append-only audit trail for free: a late arrival becomes an explicit belief revision, not a silent overwrite, and “what did we believe last Tuesday” stays answerable. The bitemporality page has the model. The operator’s site-specific runbook (hosts, communities, addresses) is git-ignored and local-only. Before anything is published to PyPI there is a written, two-stage PII audit, because source distributions ship tests and a fixture copied from live output would otherwise be archived forever.
Network exposure and HA
Section titled “Network exposure and HA”NATS has no authentication. This is the loudest “know your assumption” item. The event bus rides entirely on a network firewall. On split-host and multi-node deploys the firewall is mandatory: DOCKER-USER rules restrict Postgres and NATS to specific peer source IPs, and Postgres also carries a strong password. If the firewall is wrong, the bus is open, so it is named explicitly rather than assumed away. The production topology (Swarm plus Patroni-managed Postgres behind an HAProxy write endpoint plus a 3-node NATS cluster) is on the HA deployment page; a single-node loss fails the DB, the bus, and the app over with nothing manual.
Supply chain
Section titled “Supply chain”While validating the wireless key path we found and fixed an upstream bug in pyrad:
its multi-block salt decryption chained on the wrong block, corrupting MS-MPPE keys
past the first 16 bytes for any proxy that decrypts them. The fix went upstream with
a regression test, validated against the RFC and an independent implementation.
l2trace’s own tests never relied on the buggy path, so l2trace was correct regardless;
the fix helps the ecosystem.
The sharp edges, stated plainly
Section titled “The sharp edges, stated plainly”- NATS is unauthenticated; security is the firewall.
- TACACS+ MD5-pad is obfuscation, not encryption (protocol-level). Trusted path plus reject-unencrypted is the mitigation.
- MS-CHAPv2 is a weak primitive if you use PEAP; its security rests entirely on the TLS tunnel. EAP-TLS (mutual certificates) is shipped and removes the weak primitive from the path, so prefer it wherever you can issue client certificates.
- The TLS master secret briefly touches disk (0600, unlinked at handshake end)
because stdlib
sslgives no in-memory export. Bounded, but real. - Access control is unproven in production. Monitor-first, lab-validated only.
- Secrets backends are env and file today; Vault and Vaultwarden are roadmap.
- SNMP is v2c on the collection side; v3 is not the current default.
The one-line summary: l2trace is a bitemporal L2 topology store that grew a network access-control plane, and that plane is engineered fail-safe first, with the crypto validated against RFCs and known-answer vectors. The two assumptions you must not violate are that the NATS firewall is correct, and that you do not enforce in production until it is soaked on lab gear.