How it works — architecture
When a tool promises to prove your data isn't leaving to MIT, the very next, very fair question is: how is it built, and can I check the build myself? A trust tool that you have to take on faith is just one more black box. So this page opens the lid. It walks through how MIT Network Audit is put together — what watches the network, what writes the log, and where the seam is between the part you can audit standalone and the part that plugs into Odoo.
When a tool promises to prove your data isn't leaving to MIT, the very next, very fair question is: how is it built, and can I check the build myself? A trust tool that you have to take on faith is just one more black box. So this page opens the lid. It walks through how MIT Network Audit is put together — what watches the network, what writes the log, and where the seam is between the part you can audit standalone and the part that plugs into Odoo.
The design is deliberately layered for one reason: the piece that actually watches your traffic is pure Python with zero Odoo imports, so it can be read, reasoned about, and even run on its own — without trusting the Odoo plumbing around it.
The big picture in one minute
Every network call your Odoo makes — a connection coming in from a browser, or a connection going out to Gmail, your LLM key, or an MIT licence endpoint — passes through the same short pipeline:
- The interceptor catches it at the lowest practical layer (down to the raw socket).
- Attribution names the culprit — it walks the live call stack to find which Odoo module made the call.
- Redaction throws the body away — only metadata survives; the contents are never captured in the first place.
- The sink writes one row to the append-only, hash-chained log
(
network.egress_log). - A rollup pre-aggregates those rows (
network.egress.rollup) so the dashboard is fast. - The Trust Report reads the rollup and shows you a verdict.
flowchart LR
subgraph traffic[Your Odoo network traffic]
IN[Inbound HTTP<br/>browser → Odoo]
OUT[Outbound calls<br/>requests · urllib · httpx<br/>imap/smtp/pop3 · SSL · socket]
end
IN -->|WSGI middleware| INT[Interceptor core<br/>pure Python · zero Odoo imports]
OUT -->|channel patches| INT
INT -->|attribute + redact| NE[NetEvent<br/>metadata only · no body]
NE --> SINK[DB sink]
SINK --> LOG[(network.egress_log<br/>append-only · hash-chained)]
LOG --> ROLL[(network.egress.rollup<br/>pre-aggregated)]
ROLL --> TR[Trust Report<br/>OWL dashboard]
Everything below is just the detail of those boxes.
Three layers, one clean seam
The module is split into three layers, and the split is the whole point. The part that does the watching is kept rigorously separate from the part that knows about Odoo — so the trustworthy core stays small, readable, and independent.
| Layer | Folder | What it is | Odoo imports? |
|---|---|---|---|
| The interceptor core | interceptor/ |
The pure watcher: channels, the captured event, redaction, the inbound middleware, the outbound patches. | None — zero. Auditable as a standalone library. |
| The glue | glue/ |
The Odoo-coupled injection that wires the core into the running server. | Yes — this is where Odoo coupling is allowed to live. |
| The Odoo model layer | models/ (plus views, security) |
The append-only log, the rollup, attribution, redaction policy, and configuration. | Yes — these are Odoo models. |
Why the core has zero Odoo imports — and why that matters to you
The interceptor/ folder is pure Python. It does not import odoo anywhere. That
is enforced, not just intended — the build pipeline greps the folder and fails if an
Odoo import ever sneaks in. The payoff is independence: you can read and run the core
that actually touches your traffic without trusting the Odoo glue around it. The
trust-critical code is deliberately the smallest, most self-contained part of the
module.
How do the two sides talk without the core knowing about Odoo? The pure core is handed two
small, well-defined collaborators by the host: an Attributor (which turns a live call
stack into the responsible ir.module.module) and a RedactionPolicy (which decides how
headers are masked). The core calls them through a plain Python interface; the Odoo glue
supplies the real, Odoo-aware implementations and the database sink. That seam is exactly
what keeps the auditable library free of Odoo.
What the interceptor catches — the capture channels
To prove your data isn't leaving, the module has to sit in the path of every way data can leave. It does that by wrapping each network channel a module might use, at more than one layer — so even a library it doesn't specifically recognise still produces a row.
Outbound channels
| Channel | What is wrapped | Layer |
|---|---|---|
requests |
HTTPAdapter.send |
HTTP library |
urllib / http.client |
the standard-library HTTP path | HTTP library |
httpx |
the modern async/sync HTTP client | HTTP library |
imap / smtp / pop3 |
the mail protocols | Mail protocols |
ssl.SSLSocket |
raw TLS sockets | L7 (encrypted socket) |
socket.connect / connect_ex |
the raw socket connect | L4 backstop |
The last one is the safety net. Wrapping socket.connect at L4 means that even a
library MIT Network Audit doesn't specifically know about still gets a destination row when
it opens a connection. A call can't slip past simply by being exotic — if it touches the
network, it leaves a trace.
Inbound HTTP
Inbound web requests are captured by a pure WSGI middleware (interceptor/wsgi.py). The
glue (glue/inbound_http.py) injects that middleware into odoo.http so it sits in front
of every incoming request. The middleware itself contains no Odoo code — it is part of the
pure core.
Channels are configurable, off by default means off
You can switch individual channels off at runtime from
Configuration → Settings → Disabled channels (a comma-separated list such as
socket,smtp; empty means capture everything). No restart is needed. See
Settings & forensic capture.
Attribution — naming who made the call
Seeing that a connection went out is only half the answer; you want to know who sent it. That is attribution.
At the moment a call is intercepted, the module walks the live call stack and maps the
frames back to the Odoo module that owns the code — resolving the result to an
ir.module.module record. So each captured row carries the attributed addon, its
author, and the call site. That is what lets the Trust Report split traffic into
"this came from MailDesk", "this came from the license locker", "this came from Odoo core" —
and how it would surface anything reaching the wrong destination, attributed to the module
responsible.
The auditor audits itself
By design, this module makes no outbound calls of its own. If attribution ever names
a call as coming from mit_network_audit itself, the Trust Report raises an alarm.
The watcher is not exempt from its own watching. See
Verdicts explained.
What a captured event holds — and what it can't
Redaction in this module is structural, not a setting. The captured event — internally a
NetEvent — simply has no body field. There is no toggle to "also store the body" in
the default capture path, because there is nowhere to put it. The private contents of your
traffic are not recorded, because they are never captured in the first place.
What a captured event does keep is metadata only:
- direction, channel, protocol, method
- host, URL path, query keys (keys only)
- destination and source IP + port
- status, and request / response sizes
- timings
- masked headers — header names are kept, header values are masked
- a salted HMAC fingerprint of the request
- the attributed addon + author + call site
There is one — locked-down — exception
An optional forensic body capture mode exists for legal cases. It is a separate,
officer-only store (network.egress_body), it is off by default, and every read
of a captured body is itself logged to the append-only journal. It does not change the
default behaviour: out of the box, no body is ever stored. See
Settings & forensic capture.
Fail-open — the auditor never breaks your traffic
A guard rail you should know about: capture is fail-open. Any exception inside capture, attribution, redaction, or the sink is swallowed. If something goes wrong in the auditor, the audited network call still completes normally. The module is built so that the thing watching your traffic can never become the reason your traffic fails.
The append-only log and the rollup
The sink writes each event as one row in network.egress_log — the
append-only journal. That table is the source of truth, and
it is tamper-evident by construction: it rejects write and unlink (even via sudo)
except the retention vacuum, and each row's hash chains the previous row's hash, so any
alteration is detectable. The mechanics of that chain, and how verify() and the signed
head defeat even truncation, are covered on
Why you can trust it.
The journal is exhaustive — one row per call — which is exactly what you want for forensic
evidence, but too much to read live for a dashboard. So a separate rollup
(network.egress.rollup) pre-aggregates the rows on a cron. The Trust Report and
Traffic Summary read that pre-aggregated table, which is how the dashboard stays fast
without re-scanning every row on every refresh. Each destination is also classified
MIT-bound vs third-party against a seeded catalogue of known MIT endpoints (see
Configuration → Egress Components) — that classification is what powers the two-bucket
split you see on the report.
Deployment-portable install
The interceptor only works if it is loaded into every worker process before any
request is served. The module is built to do that on the two very different ways Odoo gets
deployed, both funnelling through a single idempotent post_load():
| Deployment | How it loads |
|---|---|
| Self-hosted / Kubernetes | Server-wide via post_load, where --load / server_wide_modules is set — every worker is patched at startup. |
| Managed PaaS (Odoo.sh) | Installed from an ordinary Apps install; the interceptor wires itself in via ir.http._register_hook. |
You do not have to choose or configure the mechanism — it adapts to where it is running. For the step-by-step, see Installation.
See for yourself
Inbound and outbound calls flow through the interceptor and the sink and surface here, on the Trust Report.
The architecture exists to make one promise checkable: the only MIT-bound traffic is small licence metadata, and everything touching your real content goes to a provider you chose. The next page shows why the log itself can be trusted — the invariants that make the record tamper-evident.