main
9
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
fddbeb119e |
Drop the em-dashes, and repair the table one of them was holding up
House style across the organisation, and the punctuation most reliably read as machine-written. The shell in this repository already used " - " throughout; only the prose disagreed. Replaced by what each one stood in for rather than by a single substitute: a colon where an explanation follows, a comma before a conjunction, brackets where a pair was fencing a parenthetical. A blind pass corrupted the firewall rule table. Its "not applicable" cells were em-dashes, so `| - | - |` became `|: |: |` in the one table documenting the primary control, and nothing about the rendered page looked wrong enough to notice. Repaired to ASCII hyphens, with a test for cells holding punctuation and nothing else. Two more artefacts the same pass introduced, both at line wraps, where the conjunction rule could not see across the break: a colon before "and", and a dash that opened a line becoming a colon that opened one. The em-dash test builds the character from its bytes rather than writing it, since a check containing what it looks for reports itself and can only pass by exempting the file it lives in. |
||
|
|
2884608066 |
Let dejima-guard collect from this devbox, without giving it a shell
dejima-guard watches this box from a machine that is not this box, and it
pulls - it reaches in over SSH and the devbox never reaches out. That
inversion is what lets a guard report on a devbox that has been taken, so the
work here is one inbound pinhole and deliberately no outbound rule at all.
v5, optional, and node-side except for the key.
THE FORCED COMMAND I DOCUMENTED DID NOT WORK. It was
`command="dejima-devbox-audit ${SSH_ORIGINAL_COMMAND##* }"`, taking the last
word of whatever arrived - so `drift --state` became `--state`, which is not
a subcommand. Drift collection would have failed on every cycle while reach
kept working, leaving a box that looked watched with half of it unwatched.
Found by testing the line rather than by reading it.
bin/dejima-guard-shell replaces it: a whitelist of the two exact commands the
guard sends. Whitelisting says what may run; sanitising tries to describe
what may not, and gets it wrong quietly.
AND IT DEFAULTED TO ROOT, on a machine this project's own runbook builds
without a root password. The guard connects as an ordinary account and
elevates through sudo for those two invocations, with a sudoers line that
names both rather than the program - because NOPASSWD on the program alone
would also permit `drift --accept`, and a key that can re-baseline can tell
this box that whatever an intruder left on it is normal.
The runbook has four test commands: two that must work and two that must
print `refused`. A restriction nobody tried is a restriction nobody has, and
this one is what stands between a key taken off the guard and a shell here.
`bats test/` 171 passing, 10 new. Renumbering the guest runbook to fit §7 in
broke two anchors in the threat model, which links.bats caught.
|
||
|
|
499276322a |
A pinhole for dejima's remote-access notifications
dejima-notify-watch is a systemd user unit on the devbox, not something in a container, so its push to ntfy leaves the guest and crosses the LAN. A self-hosted ntfy is a private address, which the DROPs cover. So on a devbox built to this runbook, dejima's remote access is configured, enabled, and silently sends nothing. Nothing errors where the operator is looking, the phone never buzzes, and the cause is in another repository's documentation. The rule was always needed and was never written down. Optional, and shaped like the DHCP pair: one host, one port, above the DROPs, omitted entirely by anyone not using the feature. The cost is written down rather than absorbed. The ntfy server joins the VM's blast radius, so the rule names a host and not a subnet - a subnet rule works identically from the operator's side and hands a compromised devbox the LAN, which is the shortcut this documents against. It is not a new way to phone home: internet egress from the VM is already unbounded and deliberate, so this widens lateral reach and nothing else. Nothing checks it, and the runbook says so. The audit verifies containment - that what should be unreachable is - and a missing pinhole is the opposite shape, so it passes either way. v3 therefore declares no verify-node and mark-node will say the record is the operator's word for it. The upgrade note assumes the operator may have added this by hand already, because anyone using the feature probably has, and gives them the two things to re-check: that it sits above the DROPs, and that it is not a subnet. Also derives two upgrade tests from VERSION rather than a literal, so a version bump breaks the tests that are about versions and no others. Tested with `bats test/` (135 passing). |
||
|
|
8b253765cb |
Describe the boundary as it is, not as it changed
The threat model and the runbook are descriptions of the current tool, not a record of what moved. The IPv6 sections read as a changelog: what the rule list used to name, what was reachable before, what the audit used to report. That belongs in the pull request, which has it. Same rules, same reasoning, stated in the present tense. |
||
|
|
7cba3932c2 |
Bound the LAN over IPv6, and over the range a tailnet uses
The node firewall's DROPs named IPv4 ranges only, so the control this project exists to provide did not apply to IPv6 at all. That is open by default on every network including ones with no IPv6 in them: every Linux host brings up an fe80:: address on every interface whether or not anything routes v6, and a daemon bound to :: answers on it. Neighbours were reachable across the segment with all four DROPs in force, `ip -4 route` showed nothing unusual, and `dejima-devbox-audit reach` reported PASS. Dropped whole rather than enumerated, because IPv6 has no RFC 1918. A v6 network is numbered out of a globally routable delegated prefix, so no equivalent range list can be written here - fc00::/7 catches ULAs most networks do not use and misses the prefix a neighbour is actually on. On the node rather than in the guest, for the same reason as every other rule here: a sysctl is a setting root on the guest puts back. Also drops 100.64.0.0/10. A tailnet is numbered there and is none of the three RFC 1918 ranges, so a devbox on one reached every machine on the overlay. The audit gets a matching check: it asks ff02::1 who is on the link and counts the replies that are not its own. No positive control, because both reasons for v6 silence are the outcome wanted - unlike the v4 case, where a working firewall and a broken network look identical. It does report INDETERMINATE when the probe cannot run at all, which is why bootstrap now installs iputils-ping; without it the check would have passed vacuously, which is the one thing this program is built not to do. Docs: the threat model gains a "Not bounded" list collecting the negative space in one place, a section on the two address families, two new entries under "Rejected, with reasons", and a quarterly review cadence with the conditions to check rather than re-argue. Tested with `bats test/` (114 passing) and by exercising the probe against recorded ping output, including its own reply and DUP! lines. |
||
|
|
a4c90efd7b |
Hand off with dejima's dependencies met and its source on the box
A devbox is a machine that runs dejima, and this repository set one up without installing anything dejima needs. Bootstrap finished on a host with no container runtime at all. It now installs docker.io and docker-cli from Debian, compose by whichever route the release offers, and python3, jq and git - dejima's own stated prerequisites, the first two for its host-side credential proxy and its installer. The compose ladder is the sister repository's, unchanged in substance: distribution package, then backports, then Docker's own apt repository last and said out loud, because it is a third-party source on a machine whose value is a small, known surface. bin/dejima-devbox-clone then puts dejima's source at ~/dejima, and stops there. It does not run dejima's install.sh: that step writes into ~/.claude, ~/.local/bin and ~/.config/dejima, and what it does there is dejima's to define and to change. A copy of another project's setup contract kept here would go stale with nothing to notice. What is worth doing on this side is the ordering - source has to arrive over a network that is about to be locked down, so the clone happens before the addressing step and before the node rules go on. An existing directory is never modified. A checkout somebody has been working in is not this program's to pull into, and a directory that is not a checkout is more likely to be theirs than to be a mistake. Two smaller things follow from the same change: The account. dejima runs as a person, not as root, so bootstrap asks which one, adds it to the docker group and records it. The docker group is root on this VM in one command - docs/threat-model.md already treats that as a grant rather than a lapse, and the blast radius there is written assuming it. The audit's radius report was inventorying the wrong home. It runs as root, from the timer or under sudo, where HOME is root's - and root's home on a devbox holds nothing, so the report read "none found" on a box whose credentials sat one directory over. It now resolves the recorded account, falling back to $HOME so a host set up before this reports what it always did. Tested: 98 tests pass. test/clone.bats clones over file:// from a fixture repository, so it needs no network: the clone, DEJIMA_DIR, an existing checkout left alone with its uncommitted work intact, a non-checkout directory refused, an empty one accepted, a bad URL failing loudly, and that nothing is written into the account it clones for. test/audit.bats covers the home resolution. test/units.bats now checks that every program in bin/ is executable, which dejima-devbox-clone was not. |
||
|
|
d2d00dba90 |
Correct the escape argument, and state how poisoning is handled
Two claims in docs/threat-model.md were false rather than loose. An escape would be a downgrade, and the account already holds more than the escape would grant: root on the VM is strictly more capability than a process inside a container with a trimmed capability set. Root on this VM is already a row in the blast radius table, which is the case the model is drawn around, and the hypervisor firewall bounds it identically however root was reached. The escape section names what an escape reaches that the container did not, and it is not one thing. Every other project on the box becomes readable, and uncommitted work in those exists nowhere else. Beyond that, the model API credential: dejima keeps it out of the container deliberately, so a session can use the API while it runs and cannot carry the token away, and the proxy holding it runs on the devbox. That makes an escape onto the devbox the step that turns temporary use into a token someone keeps. It is not the only loss that reaches past the machine, and no longer says so. Source and uncommitted work leave just as easily. What puts the credential first in the queue is that it is a standing capability rather than a copy of data, and that it sits at a known path under a known name, which makes it among the easiest things on the box to find. That also corrects the rootless rejection, which claimed the set it would protect was close to empty. It is small but not empty, and the control covers neither half of it: the proxy runs as whichever account runs the session, and one account owns every project, so moving the session to a dedicated account moves the projects with it. The threat model no longer rests on the adversary being unsophisticated. Real supply-chain malware fingerprints its environment, stages, beacons and can hand a session to a person, and a prompt injection borrows the agent as its adaptive component. What the model rests on instead is that every one of those moves needs egress, which the node firewall bounds whether the thing being bounded is a script or a person driving it. Being undirected changes what is likely, not what is possible. One person's work per devbox is stated as a requirement of the threat model rather than an observation, and cross-project context poisoning is stated as handled in dejima, in the present tense. |
||
|
|
783c508d97 |
Carry dejima's canonical statements: the disclaimer, and the push rule
Two things dejima states canonically and this repository did not carry. The maturity disclaimer, in the same three places dejima pins it: README, SECURITY.md and the document that argues at length for security properties. Its own wording covers all three repositories by name - these projects, any of these repositories - so a reader landing here would not have seen it. Duplicated rather than linked, which is the opposite of what attribution and the security policy do, and deliberately: a disclaimer behind a link is a disclaimer the reader does not see, and its whole value is being on the page where the argument is being made. test/disclaimer.bats pins it in all three. The line is the one most likely to be dropped without anyone deciding to drop it, by a rewrite of a section that happened to contain it, and every other test here would still pass on a repository that had lost it. It earned its place immediately: a git checkout during development reverted the README paragraph and the guard caught the commit that would have shipped without it. The push rule goes in the credentials section, where it belongs: a session's forge is the one you chose, and everywhere else is where its dependencies live. Linked rather than restated, because the argument has a canonical home and a second copy of an argument is a second copy to keep right. |
||
|
|
8b86a4d4ad |
Write the threat model and the guest-side runbook
threat-model.md ranks the risks by what is actually likely rather than by what sounds serious, and puts lateral movement first because that is what a devbox is exposed to during ordinary use rather than under attack. It argues the container-escape deprioritisation rather than asserting it, and names the CVEs so a reader can check them. The decisive point is that an escape would be a downgrade: a session already holds a forge credential and the container's mounts, so landing in an unprivileged account on the VM grants less than it already had. The rejected options carry their reasons and the conditions that would overturn them. That section exists so a decision can be revisited on new information instead of re-argued from nothing, which is what happens when only the conclusion is written down. building-a-devbox.md is the guest half and links to the node half rather than duplicating it, so another hypervisor can be added later without touching a guest-side step. Two snapshots, because preflight and clean answer different questions. Two sections with no counterpart in the sister project: keeping credentials off the box, and patching as the whole of the response to escape. Every internal link and anchor was checked against the heading it targets. |