Case Study — Open Source · Email Security · Production
Vexa Mail Insight — self-hosted DMARC observability, running in production
DMARC aggregate reports arrive as zipped XML in a mailbox, and nothing on a small hosting account reads them. So I built the thing that does: one container, SQLite, an IMAP poller, and a UI that answers who is sending mail as your domains and whether it authenticates. It has been ingesting real reports since July 2026; the September 16 snapshot covered 55 domains and 63,329 messages. Apache-2.0, published as a container anyone can pull.
- Next.js 16
- React 19
- TypeScript
- Drizzle
- SQLite
- Docker
- Helm
- systemd
Why it exists
Mailbox providers can send DMARC aggregate reports to the address in a domain’s rua tag. These XML reports describe sending IPs, authentication results, and message counts, often in zip or gzip attachments.
I run a small hosting estate and needed to inspect reports from Google, Outlook, and other providers. Monitoring those reports helps identify legitimate senders that fail alignment before changing an enforcement policy.
Why a self-hosted dashboard
I wanted to retain authentication history across many domains in my own database, so I could compare results before and after DNS changes.
The open-source prior art is parsedmarc, and it is good at what it does — it parses reports and writes JSON or CSV without needing anything else. The gap is the part after parsing: its documented dashboard integrations include Elasticsearch and Kibana, OpenSearch and Grafana, or Splunk. That is a reasonable ask inside a company and an unreasonable one next to a cPanel box. I wanted the whole thing to be one container with a database file next to it.
So the claim here is not novelty. It is shape: single-container operation over SQLite, with the UI in the same process, for people who own their domains and do not want to own a search cluster.
What it does
- Polls an IMAP mailbox for aggregate (RUA) reports and unpacks zip and gzip attachments, including the nested cases providers actually send.
- Parses the XML into normalized events — one row per source IP, SPF and DKIM result, alignment, disposition and message count — and keeps the raw report for re-parsing.
- Deduplicates by report ID and by source message, so a re-poll or a full rescan does not double-count anything.
- Enriches sender IPs with geolocation and reverse hostname lookups, on a schedule, so the "who is this" question has an answer.
- Visualizes the SPF lookup tree, which is where the ten-lookup limit quietly breaks domains that used to work.
- Exports a per-domain report as PDF, for the conversation with whoever owns the sending system.
- Ships a first-run install flow with a one-time token, optional OIDC SSO, role-based access control, an audit log, and a CLI for recovery when you lock yourself out.
Building and hardening it
First commit was 2026-04-26. The initial release covered ingest, normalization, the dashboard, and the report detail view. A later security pass addressed the things that matter once other people can run it, because a tool that reads your mailbox and stores IMAP credentials is not a toy.
That pass hardened the DMARC parser against XXE, zip bombs and zip-slip — a report is an untrusted file from a stranger, and the parser is the attack surface. It added an SSRF-safe fetch primitive used by the webhook and MTA-STS paths, encrypted IMAP credentials at rest, moved password hashing to scrypt at N=131072, made /api/v1/** default-deny, put a same-origin CSRF guard on cookie-authenticated mutating routes, rate-limited the AI endpoints, and made the first boot bind to loopback behind a one-time install token rather than opening a setup page to the internet. July added a per-request nonce CSP, Zod validation and RBAC on privileged routes, and a single-use ticket for the GeoIP progress stream.
Two breaking changes in September were the ones I am most confident about, because they removed a choice rather than adding a control: the encryption root is now read from the environment only, and the admin API token is derived from SECRET_KEY. Before that, the key could live in the database, which meant a copied database file carried every stored IMAP password with it. That is the kind of defect you only find by actually operating the thing.
The release that published an unsigned chart
Release 0.3.0 pushed a Helm chart as an OCI artifact, then failed at the signing step with an authentication error. The chart job used helm registry login, which writes Helm’s registry config, while cosign expected Docker credentials. The published chart was unsigned.
That chart job had been added after v0.2.2, so 0.3.0 was its first release run. Version 0.3.1 fixed the job by using docker/login-action, which provides credentials both tools can read. The release notes direct users to verify 0.3.1; publishing the artifact and successfully signing it are separate steps.
In production
The live instance runs on a single server under systemd as an unprivileged user, serving a standalone Next.js build behind nginx, with the SQLite database in a directory only that user can read. Report ingestion began on July 24, 2026. The production runbook describes a source build under systemd; the published container is a separate installation path.
It has not been uninterrupted. On 2026-09-02 the instance was unreachable for 29 hours, and a SECRET_KEY rotation in September needed the service stopped for about ten seconds to update the key and re-encrypt the stored mailbox password in one transaction. Both are in the project log with what was checked and what was changed, which is the part that makes them worth anything.
Production snapshot (September 16, 2026)
- 55 domains configured, 54 of which have received at least one report.
- 3,810 aggregate reports parsed, from 14 distinct reporting organizations — Google accounts for 2,361 of them, Outlook and Enterprise Outlook for 1,410 between them.
- 11,643 normalized events covering 63,329 individual messages, from reports spanning 2026-05-17 to 2026-09-15.
- 54,688 of those messages passed DMARC alignment and 8,641 did not — 86.4% aligned across the estate.
- 1,384 distinct sender IP addresses tracked and enriched.
- 26,922 mailbox messages processed by the IMAP poller across 1,130 scheduled job runs.
- The SQLite database measured 51 MB in this snapshot.
Limits of the September 2026 record
- Every event in the September 16 snapshot carried disposition "none" — the estate is in monitoring mode, so the tool has shown me the 8,641 unaligned messages but has not yet been used to defend a move to quarantine. That is the next real test, and it has not happened.
- Forensic (RUF) report ingestion is planned, not built.
- The September 10, 2026 repository check recorded zero stars and forks. The evidence here is the release and my own deployment, without a claim of wider adoption.
- It has been operated by one person on one estate. The install flow has been run end to end against the published image — install token, seed, restart, key rotation through the recovery CLI, and a boot with no key to confirm the diagnostics fire — but not yet by a stranger on infrastructure I have never seen.
Running it yourself
- Apache-2.0, source at github.com/VexaMail/vexa-insight.
- Container image at ghcr.io/vexamail/vexa-insight, pullable without authentication.
- Helm chart published as a signed OCI artifact — verify against 0.3.1 or later.
- The container installation needs a mailbox receiving DMARC aggregate reports, persistent storage, and a configured encryption key. The repository documents installation and recovery.