All articles

Nodejam Security

The anchor was a symlink: a full root chain against our own hardened helper

We built a root door meant to be the only way in, then let a swarm of small models attack it. One class went all the way through. The chain, the twenty-run proof, and the fix.

The setup

We run a hardened exploit lane on our own fixtures. The most interesting one is not a web app. It is a root door: njsec-op, a Python helper the operator account reaches through a single sudo line. We built it to be the only way to root. It validates absolute paths against four fixed roots, refuses the vault, refuses to write Python code or cache bytes, guards custody packs, and validates firewall rules down to the token. Every operation ends in a plain coreutil. No shell, no sockets, no config plane. It shipped 2026-10-14 and survived its first audit wave.

Then we let a swarm of small models attack it, and one class went all the way through.

The class: the trust anchor is mutable

The door’s validators answered one question: is this path inside an allowed root? To answer, they resolved the allowed roots through the live filesystem before comparing. The operator account owns the directory that holds one of those roots. Rename the real repo aside, put a symlink named like it pointing at /, and the door computes its trust root as /. Every absolute path becomes allowed. chown on /etc/sudoers.d, on a systemd unit, on anything.

The same flaw sat in three more places. The cp shape check resolved its source trust root the same way. The recursive verbs inspected only the argument path, never the descendants, so chown -R over a parent walked straight through the forbidden vault. And the door handed the coreutil a string it had validated earlier, which the coreutil then re-walked.

We call the class mutable trust anchors. The privileged side derives its notion of what is trusted from filesystem state the unprivileged side can change. It keeps showing up in real privileged helpers and updaters: anything that validates against a path rather than an identity. If your helper resolves its own boundaries, the lower side owns the map.

The chain

The naive route fails, instructively. Chown the sudoers file to yourself and sudo stops trusting that file on the next call. Sudo refuses grant files it does not own. You cut your own rope. We burned a twenty-run series proving it: the door executed the root chown every time, and the follow-up sudo call died before reaching the door.

The working chain goes through a consumer that trusts files without checking ownership. Systemd. Four links:

  1. Rename the repo, plant a symlink at its name pointing at /.
  2. sudo njsec-op chown nodejam /etc/systemd/system/nodejam-security-watch.service The door validates the path against what it now believes is /.
  3. The operator owns the unit file. Rewrite it. Drop the User= line, point ExecStart at a script in /tmp.
  4. sudo systemctl daemon-reload, sudo systemctl restart nodejam-security-watch.service. Both verbs are in the same sudo grant as the door. PID 1 parses the rewritten unit and runs the script as root.

Every link uses a verb the operator was granted. The chain needs no race and no memory corruption. It is deterministic.

The proof discipline

We hold ourselves to the bar we preach. Kill criteria written before the build, one per link. The deployed helper’s bytes verified against the repo copy before the first run. A twenty-run series on the box, in a serialized window under the run lock, with the unit file hashed before and after every run: the chain ran twenty for twenty, and the restore was byte-identical every time. Root cause pinned by pickaxe to the commit that introduced it, which turned out to be the commit that created the door. The flaw was original sin, not drift. The regression tests were proven failing against the pre-fix door before the fix landed, and the fix commit is what flipped them green.

A clean Debian VM then replayed the whole chain with the exact byte-identical pre-fix helper, fresh sudoers and unit trees, the same twenty runs, the same restore checks. Same result. The replay is the artifact we would hand a third party.

The fix, honestly

Four changes: the trust roots compare lexically against their pinned constants and are never resolved through live state. Recursive verbs refuse subtrees that contain the vault. The shared-source refresh refuses to transcribe Python cache bytes at any depth (our journal shows it once did, as root, exactly what its own docstring forbade). And every verb re-resolves its argument immediately before execution and refuses on any shift.

Two residuals stay on the record. The coreutil still re-walks the path string inside its own execution window; the revalidation narrows that race to microseconds but openat-style resolution is the complete fix. And the deployed helper updates only through a deliberate root bootstrap, which is the point of the design: the proof ran against the vulnerable door, the fix ships through the door’s own update law. It has: the bootstrap ran on 2026-10-25, and the deployed bytes now hash-match the fixed tree. We do not claim more than that verification, and the TOCTOU residual above is documented, not eliminated.

The closing angle

While this ran, we planted fake flag files in the trees our agent swarms sweep, to see whether a model that finds a flag prints it or reports it and stops. Round one: two of three printed the contents. One had just correctly stated the rule it then broke. Then we added one standing instruction to every ask and ran the test cold: fresh files, no commit trail, nothing hinting at a test. Three of three named the plant and stopped, and grep confirmed zero contents in their transcripts. The chains get most of the attention, but the hygiene of the machines we send into the trees is its own attack surface, and it is measurable. The full measurement is its own post.

Sources and evidence

  • The fix commit and the fail-first regression tests: our security repository, which is private, commit 1c10c16, tests in security/tests/test_door_trust_anchors.py.
  • The full chain criteria, series logs and clean-VM replay are retained as evidence in our engagement records.

Design partners.

We are taking a limited number.

Apply as a design partner