darkmux is a personal project, semver-stable as of v1.0.0, released under the MIT license with no warranty (see DISCLAIMER.md). This file describes how to report a vulnerability and what the project's threat model is — what darkmux does and does not defend against — so you can make an informed decision about where and how to run it.
Please report security issues privately, before public disclosure.
- Open a private security advisory on the repository (GitHub → Security → Report a vulnerability). This is the preferred channel.
- If you cannot use GitHub advisories, file a normal issue that says only "security issue, please enable private reporting" — without details — and the maintainer will open a private channel.
Please do not open a public issue or PR that describes an exploit before a fix is available. This is a solo-maintained project; fixes ship on the maintainer's schedule, but security reports are triaged first.
This request applies to issues someone else finds. It does not bind the maintainer's own self-disclosure: when a published claim about darkmux turns out to be wrong, the maintainer may correct the record immediately, because correcting a claim he made outranks an embargo he would only be keeping from himself (see darkmux#1769 for the case that established this).
As of v1.0.0, darkmux follows semver. The current 2.x line receives security
fixes and maintenance. Older major versions (0.x, 1.x) get no backports;
upgrade to the 2.x line to receive fixes. Pin a version for reproducibility,
but track the 2.x line for security updates.
| Version | Supported |
|---|---|
2.x (current) + main |
✅ |
1.x and older |
❌ (upgrade to 2.x) |
darkmux is a single-operator, local-first tool. The trust boundary is your machine and the people who can reach it. It is not a multi-tenant service and makes no attempt to defend one user from another on a shared host. Within that frame:
- The observability viewer escapes all record-derived content. Flow records
can carry attacker-influenced strings (a container tool can write its own
trajectory events — see Known limitations below). The viewer
(
darkmux serve) HTML-escapes every record field it renders and routes all click handling through a single delegated listener with no inline event handlers, so a crafted identifier in a flow record cannot execute script in your browser. This is enforced by build-time guards in the serve crate. - Secrets stay out of plaintext config. A Redis password is never written to
config.json— it lives in the macOS Keychain, is read at runtime, and is never logged. Every Redis URL is wrapped so itsDebug/Displayredacts the password; the raw value is only exposed at the point of connection. On non-macOS, the full URL is supplied viaDARKMUX_REDIS_URL(your responsibility to keep out of shell history / logs). - Daemon binds to loopback by default.
darkmux servebinds127.0.0.1unless you explicitly bind elsewhere. CORS is deny-by-default (onlynull/file://origins); extra origins are opt-in viaDARKMUX_DAEMON_CORS_ORIGINS. - No shell interpolation of untrusted strings. Dispatch invokes Docker and
subprocesses through argument vectors (
Command::arg), not a shell string, so record/identifier values cannot inject shell commands. Operator-supplied identifiers (e.g.mission_id) are validated against an identifier charset at the CLI boundary. - Path parameters are validated. The daemon's date-scoped flow endpoints
accept only a strict
YYYY-MM-DDshape, so a request cannot traverse the filesystem.
-
AI-generated code is not sandboxed for security. The internal runtime runs each dispatch in a per-invocation Docker container with kernel-enforced workspace isolation — better than a bare directory, but Docker on macOS is a VM boundary, not a guarantee against a determined adversary. Run only on a machine where that risk is acceptable. (See DISCLAIMER.md.)
-
The daemon has no authentication. Anyone who can reach the bind address can read your flow records and drive the viewer. Keep it on loopback, or put it behind your own authenticated reverse proxy / a private network (Tailscale) — do not expose
darkmux serveto the public internet. -
Flow records are not authenticated. Any process that can write to the flows directory or the Redis stream can author records. The audit sink (below) makes post-hoc tampering detectable, but nothing makes the live stream unforgeable.
-
The audit sink is a detection substrate, not a prevention one. The opt-in
DARKMUX_AUDIT_DIRsink writes records into a BLAKE3 hash chain, anddarkmux flow integrity-checkwalks that chain. It does not make records impossible to alter, and running it does not by itself make you compliant with any regulatory framework. darkmux makes no compliance claim: producing evidence and being compliant are different things, and only the second one needs a lawyer. -
Known gaps in the audit chain, stated plainly. The #1769 bypass — where a record carrying an unrecognized enum value skipped content verification entirely, so its other fields could be rewritten while the chain still validated — is closed as of 2026-08-11 (#1772): the chain now hashes the stored bytes rather than a re-serialization of a parsed struct, so there is no parse step left to skip.
What remains is structural rather than a bug.
Truncating the tail of a file is undetectable, and it is the least conspicuous of the three — it leaves a present, plausible-looking file rather than a missing day. Delete the last record line (or the last k of them) and the walk sees a shorter but fully consistent chain: every
prev_hashlinks, every byte-hash matches, the file reports valid — including under--strict. Nothing records how many records a file should contain or hashes its tail. The next append then extends the chain from the truncated end, so the deletion is permanent. Note this is not addressed by anchoring the chain root: a root anchor proves where the chain started, not where it should have ended. A tail anchor or an external high-water mark would be needed.Whole-file deletion is undetectable, because each per-day file seeds its own chain and nothing records which files should exist.
And the chain root is un-anchored — computed and stored locally — so an attacker with write access who rewrites a file wholesale (fresh header, recomputed hashes, relinked
prev_hash) produces one that validates.All three ceilings are inherent to an anchorless local chain. What the walk does catch: in-place edits, reordering, insertion, and any deletion that is not a pure suffix — removing the first record, or a run from the middle, breaks the following record's
prev_hashlinkage. Only records removed from the very end leave nothing behind to disagree with.A file that cannot be content-verified at all — a pre-2.6.0 struct-hash file, or one whose
hash_formatheader marker has been removed — is reported honestly but exits 0 by default. Pass--stricttodarkmux flow integrity-checkto make that exit 3 (#1775), so an unattended consumer can distinguish "verified" from "never checked".Treat all of this as a supporting detective control composed with disk encryption and filesystem permissions, not as a standalone guarantee against a local attacker.
-
Model behavior is not policed. darkmux faithfully executes what the model produces. Review agent output before it touches anything you care about.
These are accepted, documented gaps — not undisclosed vulnerabilities:
- Trajectory event injection (#237).
Container tools can write to the dispatch trajectory file, which the host
tailer turns into flow records. The identifier fields on those records
(
session_id,machine_id,handle,model,mission_id) are stamped by the host from the dispatch context, not read from the container — so a container influences only the record payload (tool name, finish reason, reasoning text, detector detail). That payload is (a) output-encoded at the viewer, so it can't execute script, and (b) size-bounded at ingest so a container can't inject a pathologically large field into the flow stream / audit chain / Redis (reasoning_text≤ 256 KiB;tool_name/finish_reason/ detectordetail≤ 4 KiB). The residual is that a container can still emit plausible (well-formed, in-bounds) telemetry about its own run — a self-affecting concern bounded by the operator's per-dispatch caps, not a cross-trust-boundary one. - CORS origin normalization (#289).
The
DARKMUX_DAEMON_CORS_ORIGINSallowlist matches origins by normalized string (lowercased scheme/host, trailing slash stripped) but does not collapse port-equivalent or hostname-alias forms (http://localhostvshttp://localhost:80;localhostvs127.0.0.1). Configure origins consistently. The secure default (loopback bind, deny-by-default CORS) is unaffected.
In scope: the darkmux binary, the darkmux serve daemon and viewer, the
internal runtime, and the dispatch path. Out of scope: vulnerabilities in
third-party software darkmux orchestrates or depends on (LMStudio, Docker,
the models you load) — report those to their respective projects.