Skip to content

Security: kstrat2001/darkmux

Security

SECURITY.md

Security Policy

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.

Reporting a vulnerability

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).

Supported versions

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)

Threat model

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:

What darkmux defends

  • 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 its Debug/Display redacts the password; the raw value is only exposed at the point of connection. On non-macOS, the full URL is supplied via DARKMUX_REDIS_URL (your responsibility to keep out of shell history / logs).
  • Daemon binds to loopback by default. darkmux serve binds 127.0.0.1 unless you explicitly bind elsewhere. CORS is deny-by-default (only null/file:// origins); extra origins are opt-in via DARKMUX_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-DD shape, so a request cannot traverse the filesystem.

What darkmux does NOT defend (by design)

  • 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 serve to 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_DIR sink writes records into a BLAKE3 hash chain, and darkmux flow integrity-check walks 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_hash links, 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_hash linkage. 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_format header marker has been removed — is reported honestly but exits 0 by default. Pass --strict to darkmux flow integrity-check to 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.

Known limitations (tracked)

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 / detector detail ≤ 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_ORIGINS allowlist matches origins by normalized string (lowercased scheme/host, trailing slash stripped) but does not collapse port-equivalent or hostname-alias forms (http://localhost vs http://localhost:80; localhost vs 127.0.0.1). Configure origins consistently. The secure default (loopback bind, deny-by-default CORS) is unaffected.

Scope

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.

There aren't any published security advisories