From a security review by the agent working on dejima, prompted by the operator asking whether the three gaps SECURITY.md listed were really the risks, given the guard usually runs on an ordinary VM with none of the devbox's hardening. They were not. All five findings confirmed by reading the code; all five acted on.
The unit ran as root
The container path has always run unprivileged, on the argument that root would be granting root to a program holding an SSH key to another machine, for no reason at all. The unit disagreed with its own project: no User=, and NoNewPrivileges does not drop capabilities for root, so it was doing less than it looked like. A bug here yielded root on a machine nothing in this repository hardens.
SECURITY.md also listed "runs as a non-root uid" among the stronger properties, which was true of the container and read as a property of the project. That is the sharpest part of the finding: a security document asserting something false about the path most people will use.
Now an account of its own, StateDirectory=, an empty CapabilityBoundingSet=, and the rest of what a unit that opens two connections and writes one directory can give up.
Collection was unbounded, and the devbox chose the size
Into a variable, then a file, then both sides of a diff. That is memory pressure and a full /var/lib on a host running other things, and it is the clearest thing the watched machine can do to the watcher's.
Capped, with an over-long answer treated as a finding rather than truncated. Truncating would hand the devbox a way to manufacture a diff, or to cut the state off just before whatever it had added. pipefail is off across that one pipeline deliberately: head closing the pipe SIGPIPEs ssh, so with it on, a capped read reports as a failed connection, which is the reading that sends an operator to the wrong end of the problem. accept goes through the same collector, since a baseline is the last place to accept an unbounded answer.
The ssh client inherited a config this project does not own
ForwardAgent, PermitLocalCommand, ClearAllForwardings and ControlMaster are named explicitly now, on a connection to a host the whole design assumes may be hostile. Not -F /dev/null, which would also discard a ProxyJump an operator may need to reach the devbox at all.
The first baseline was taken unattended
accept exists because that is a decision, and the timer's first fire bypassed it five minutes after boot, with the warning going to a journal nobody is reading yet. check refuses without a baseline now, and sends no heartbeat, so a guard that has not been told what normal looks like reports as down. That is what it is.
This rippled through the suite, as it should: every test that relied on a check quietly seeding a baseline now takes one deliberately.
The config file is executable-equivalent
alert_command runs from it, so write access is code execution as whoever the guard runs as. Refused if group- or other-writable, the way ssh treats a private key. The first version of that check tested the wrong digit and would have missed group-write entirely, which is the half a shared machine is most likely to have.
The framing, which is what prompted the review
The page led with three comparisons against dejima. All accurate, and none of them what a reader most needs. It now leads with the two things that actually bound this program: the host it happens to run on, and a collector that executes on the machine under suspicion, which is the detection ceiling. The dejima comparison stays, below them, under a heading that says it is not a fourth enforcement layer.
The push URL is a credential too
Raised by the operator while this was open, and it is the same rule this project had already written down: an Uptime Kuma push URL carries its token in the path, which is why the URL goes to curl through a config on stdin rather than in argv. The compose example then set it in an environment: block, where docker inspect prints it and anything that can read /proc sees it. Exactly the exposure the ntfy token was already being kept out of.
Worth stating what it grants rather than leaving it as "a URL": anyone holding it can tell Kuma this guard is healthy, which suppresses the one signal that reports a guard that has died. That is the channel a compromised devbox cannot reach, and the reason there is a second server in this design at all.
It moves to the mounted config beside ntfy_token, with a test that neither appears in the compose environment and that the setting is still documented where it went.
Tested: 102 passing, up from 88.
**Summary generated by LLM agent**
From a security review by the agent working on dejima, prompted by the operator asking whether the three gaps `SECURITY.md` listed were really the risks, given the guard usually runs on an ordinary VM with none of the devbox's hardening. They were not. All five findings confirmed by reading the code; all five acted on.
## The unit ran as root
The container path has always run unprivileged, on the argument that root would be granting root to a program holding an SSH key to another machine, for no reason at all. The unit disagreed with its own project: no `User=`, and `NoNewPrivileges` does not drop capabilities for root, so it was doing less than it looked like. A bug here yielded root on a machine nothing in this repository hardens.
`SECURITY.md` also listed "runs as a non-root uid" among the stronger properties, which was true of the container and read as a property of the project. That is the sharpest part of the finding: a security document asserting something false about the path most people will use.
Now an account of its own, `StateDirectory=`, an empty `CapabilityBoundingSet=`, and the rest of what a unit that opens two connections and writes one directory can give up.
## Collection was unbounded, and the devbox chose the size
Into a variable, then a file, then both sides of a `diff`. That is memory pressure and a full `/var/lib` on a host running other things, and it is the clearest thing the watched machine can do to the watcher's.
Capped, with an over-long answer treated as a **finding** rather than truncated. Truncating would hand the devbox a way to manufacture a diff, or to cut the state off just before whatever it had added. `pipefail` is off across that one pipeline deliberately: `head` closing the pipe SIGPIPEs ssh, so with it on, a capped read reports as a failed connection, which is the reading that sends an operator to the wrong end of the problem. `accept` goes through the same collector, since a baseline is the last place to accept an unbounded answer.
## The ssh client inherited a config this project does not own
`ForwardAgent`, `PermitLocalCommand`, `ClearAllForwardings` and `ControlMaster` are named explicitly now, on a connection to a host the whole design assumes may be hostile. Not `-F /dev/null`, which would also discard a `ProxyJump` an operator may need to reach the devbox at all.
## The first baseline was taken unattended
`accept` exists because that is a decision, and the timer's first fire bypassed it five minutes after boot, with the warning going to a journal nobody is reading yet. `check` refuses without a baseline now, and sends no heartbeat, so a guard that has not been told what normal looks like reports as down. That is what it is.
This rippled through the suite, as it should: every test that relied on a check quietly seeding a baseline now takes one deliberately.
## The config file is executable-equivalent
`alert_command` runs from it, so write access is code execution as whoever the guard runs as. Refused if group- or other-writable, the way ssh treats a private key. The first version of that check tested the wrong digit and would have missed group-write entirely, which is the half a shared machine is most likely to have.
## The framing, which is what prompted the review
The page led with three comparisons against dejima. All accurate, and none of them what a reader most needs. It now leads with the two things that actually bound this program: the host it happens to run on, and a collector that executes on the machine under suspicion, which is the detection ceiling. The dejima comparison stays, below them, under a heading that says it is not a fourth enforcement layer.
## The push URL is a credential too
Raised by the operator while this was open, and it is the same rule this project had already written down: an Uptime Kuma push URL carries its token in the path, which is why the URL goes to curl through a config on stdin rather than in argv. The compose example then set it in an `environment:` block, where `docker inspect` prints it and anything that can read `/proc` sees it. Exactly the exposure the ntfy token was already being kept out of.
Worth stating what it grants rather than leaving it as "a URL": anyone holding it can tell Kuma this guard is healthy, which suppresses the one signal that reports a guard that has died. That is the channel a compromised devbox cannot reach, and the reason there is a second server in this design at all.
It moves to the mounted config beside `ntfy_token`, with a test that neither appears in the compose environment and that the setting is still documented where it went.
Tested: 102 passing, up from 88.
From a security review by the agent working on dejima, prompted by the operator
asking whether the three gaps this page listed were really the risks, given the
guard usually runs on an ordinary VM with none of the devbox's hardening. They
were not. Five findings, all confirmed by reading the code.
THE UNIT RAN AS ROOT. The container path has always run unprivileged, on the
argument that root would be granting root to a program holding an SSH key to
another machine, for no reason at all. The unit disagreed with its own project:
no User=, and NoNewPrivileges does not drop capabilities for root. So a bug here
yielded root on a machine nothing in this repository hardens. SECURITY.md listed
"runs as a non-root uid" among the stronger properties, which was true of the
container and read as a property of the whole. Now an account of its own,
StateDirectory, an empty capability bounding set, and the rest of what a unit
that opens two connections and writes one directory can give up.
COLLECTION WAS UNBOUNDED AND THE DEVBOX CHOSE THE SIZE. The answer went into a
variable, then a file, then both sides of a diff. That is memory pressure and a
full /var/lib on a host running other things, which is the clearest thing the
watched machine can do to the watcher's. Capped now, with an over-long answer
treated as a finding rather than truncated: truncation would hand the devbox a
way to manufacture a diff, or to cut the state off just before what it added.
pipefail is off across that pipeline on purpose, since head closing the pipe
SIGPIPEs ssh and would report a capped read as a failed connection.
THE SSH CLIENT INHERITED A CONFIG THIS PROJECT DOES NOT OWN. ForwardAgent,
PermitLocalCommand, ClearAllForwardings and ControlMaster are named explicitly
now, on a connection to a host the design assumes may be hostile. Not -F
/dev/null, which would discard a ProxyJump an operator may need to reach the
devbox at all.
THE FIRST BASELINE WAS TAKEN UNATTENDED. accept exists because that is a
decision, and the timer's first fire bypassed it five minutes after boot, with
the warning going to a journal nobody reads yet. check refuses without one now
and sends no heartbeat, so a guard that has not been told what normal looks like
reports as down, which is what it is.
THE CONFIG FILE IS EXECUTABLE-EQUIVALENT, since alert_command runs from it.
Refused if writable by group or others, the way ssh treats a private key.
And the framing, which is what prompted the review: the page led with three
comparisons against dejima. Those are accurate and none of them is what a reader
most needs. It now leads with the two things that bound this program - the host
it happens to run on, and a collector that runs on the machine under suspicion -
and keeps the dejima comparison below them, where it belongs.
branderson
approved these changes 2026-08-29 13:53:59 -07:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary generated by LLM agent
From a security review by the agent working on dejima, prompted by the operator asking whether the three gaps
SECURITY.mdlisted were really the risks, given the guard usually runs on an ordinary VM with none of the devbox's hardening. They were not. All five findings confirmed by reading the code; all five acted on.The unit ran as root
The container path has always run unprivileged, on the argument that root would be granting root to a program holding an SSH key to another machine, for no reason at all. The unit disagreed with its own project: no
User=, andNoNewPrivilegesdoes not drop capabilities for root, so it was doing less than it looked like. A bug here yielded root on a machine nothing in this repository hardens.SECURITY.mdalso listed "runs as a non-root uid" among the stronger properties, which was true of the container and read as a property of the project. That is the sharpest part of the finding: a security document asserting something false about the path most people will use.Now an account of its own,
StateDirectory=, an emptyCapabilityBoundingSet=, and the rest of what a unit that opens two connections and writes one directory can give up.Collection was unbounded, and the devbox chose the size
Into a variable, then a file, then both sides of a
diff. That is memory pressure and a full/var/libon a host running other things, and it is the clearest thing the watched machine can do to the watcher's.Capped, with an over-long answer treated as a finding rather than truncated. Truncating would hand the devbox a way to manufacture a diff, or to cut the state off just before whatever it had added.
pipefailis off across that one pipeline deliberately:headclosing the pipe SIGPIPEs ssh, so with it on, a capped read reports as a failed connection, which is the reading that sends an operator to the wrong end of the problem.acceptgoes through the same collector, since a baseline is the last place to accept an unbounded answer.The ssh client inherited a config this project does not own
ForwardAgent,PermitLocalCommand,ClearAllForwardingsandControlMasterare named explicitly now, on a connection to a host the whole design assumes may be hostile. Not-F /dev/null, which would also discard aProxyJumpan operator may need to reach the devbox at all.The first baseline was taken unattended
acceptexists because that is a decision, and the timer's first fire bypassed it five minutes after boot, with the warning going to a journal nobody is reading yet.checkrefuses without a baseline now, and sends no heartbeat, so a guard that has not been told what normal looks like reports as down. That is what it is.This rippled through the suite, as it should: every test that relied on a check quietly seeding a baseline now takes one deliberately.
The config file is executable-equivalent
alert_commandruns from it, so write access is code execution as whoever the guard runs as. Refused if group- or other-writable, the way ssh treats a private key. The first version of that check tested the wrong digit and would have missed group-write entirely, which is the half a shared machine is most likely to have.The framing, which is what prompted the review
The page led with three comparisons against dejima. All accurate, and none of them what a reader most needs. It now leads with the two things that actually bound this program: the host it happens to run on, and a collector that executes on the machine under suspicion, which is the detection ceiling. The dejima comparison stays, below them, under a heading that says it is not a fourth enforcement layer.
The push URL is a credential too
Raised by the operator while this was open, and it is the same rule this project had already written down: an Uptime Kuma push URL carries its token in the path, which is why the URL goes to curl through a config on stdin rather than in argv. The compose example then set it in an
environment:block, wheredocker inspectprints it and anything that can read/procsees it. Exactly the exposure the ntfy token was already being kept out of.Worth stating what it grants rather than leaving it as "a URL": anyone holding it can tell Kuma this guard is healthy, which suppresses the one signal that reports a guard that has died. That is the channel a compromised devbox cannot reach, and the reason there is a second server in this design at all.
It moves to the mounted config beside
ntfy_token, with a test that neither appears in the compose environment and that the setting is still documented where it went.Tested: 102 passing, up from 88.