-
Notifications
You must be signed in to change notification settings - Fork 0
Security
Argus is remote shell access. Anyone holding the token can run anything the user running Argus can run. Treat it exactly like an SSH private key.
-
One token, 64 hex characters from
secrets.token_hex, compared in constant time. Every route and every WebSocket is behind it. - Safe defaults: loopback only, read-only, no proxying, no extra mounts. Everything that can change the machine is opt-in.
-
A path jail that canonicalizes before comparing, so neither
../../etc/passwdnor a symlink pointing out of a root gets through. A path outside the roots is refused identically whether or not it exists, so the API cannot be used to map the filesystem. -
Rendered documents are sandboxed into an opaque origin, so somebody else's HTML
report cannot read the token out of localStorage. Markdown is escaped before rendering
and
javascript:links are stripped. -
Nothing is proxied unless proxying is on and that port was opened by hand, and what
is forwarded carries neither the token in the query nor the
Authorizationheader — the service behind the proxy, and its log, never see the credential. - The tmux config is validated on a throwaway server before being applied to the one holding your sessions.
- No TLS. Over plain HTTP the token crosses the network in the clear, and so does everything you type. Use Tailscale, a tunnel or a reverse proxy on anything but a trusted LAN.
-
The token appears in the pairing URL, so it lands in browser history and server logs.
Rotate it by editing
~/.config/argus/config.yamland restarting. - No rate limiting, no audit log, no second factor. One credential, no accounts.
- Anyone with the token can read every file under the configured roots, and with
--allow-write, change them.
Journal, in the sidebar, answers one question: has somebody been in here.
In it: everything that changed something, and everything that was refused, whatever method it used. Not in it: successful reads. Listing a folder every four seconds while you scroll is most of the traffic and none of the interesting part, and a file full of it is a file nobody opens. A break-in does not look like a suspicious write — it looks like a run of 401s from an address you do not recognise, and a scanner sends GETs.
Each line carries the action, which key did it (the config token, a device by name, or a board), the address, the status and the time. Behind a reverse proxy the peer is always loopback and the real client is in a header, so both are kept and shown apart: a header is whatever the sender wrote.
Refusals from one address are collapsed — a scanner can produce thousands a minute — but the count is kept, so twelve knocks read as twelve.
Only the token from the config can read it: a record a stolen device can read is a record that tells whoever took it what you can see.
It is not tamper-proof and does not pretend to be. Anything with the master token can delete the file, and a shell on the machine certainly can. It answers "what happened" for a person looking back, which is the question that comes up; it is not evidence.
Panoptes keeps its own, for the three things that happen only on a board.
One token means losing a phone costs you every device, because the only remedy is rotating the secret they all share. Settings → Devices gives each one its own.
The token is shown once, in a link and a QR code; only a sha256 of it is kept in
~/.config/argus/devices.json (mode 600), so losing that file does not hand anyone a working
key. There is no bcrypt or argon2 on it deliberately: slow hashes exist to make guessable
secrets expensive to attack, and these are 256 bits of randomness.
Two properties worth knowing:
- Revoking one leaves the others. It takes effect on that device's next request — the file is re-read every time a token is presented, so there is nothing to restart.
- A device may work but may not manage devices. Only the token in the config can add or revoke, and the middleware enforces that rather than the interface hiding it. A phone that is lost cannot mint itself a second key, and cannot lock you out of your own machine.
What this does not give you is an audit trail. The list says when each device was last used, and nothing records which of them did what.
python3 -c "import secrets; print(secrets.token_hex(32))"
# paste it into ~/.config/argus/config.yaml
systemctl --user restart argusEvery device then has to be paired again.
The token in your browser opens everything: a shell, every file under the roots, writes if they are on. That is the right shape for the person using the machine and the wrong shape for a board watching six of them, which would then be a box containing six full keys.
So a machine can hand out weaker ones:
token: <the real one, the one you use>
watchers:
- name: panoptes
token: <a different one, at least 16 characters>A watcher token opens exactly one door, GET /api/overview, and that door returns what a
board needs and nothing else: the hostname, uptime, load, memory and swap, the disk in the
most trouble, and the list of tmux sessions with which of them is ringing. No file, no
shell, no writes, no proxy. Asked for anything else it answers 403 rather than 401,
because the token is real and the request is not.
It is refused a WebSocket outright, which is the one that would matter.
Argus will not start if a watcher token is the same as the main one, or shorter than 16 characters. Both mistakes turn the weak key back into the strong one.
By default a watcher token can only look. It can be allowed to start and stop things too, and the way that is done is the whole reason it stays safe: no command ever arrives in a request.
The machine publishes a list of names. A board may ask for one of those names and nothing else — there is no field in any request where a shell line could go, and there never will be.
runnable:
- name: nightly
run: python3 nightly.py # this is only ever read from here
cwd: /srv/work
watchers:
- name: panoptes
token: <the weak one>
may_run: true # otherwise the token stays read-only
may_stop_argus: false # a separate permission; see below-
GET /api/runnable— the names, and whether each is up. Not the commands: a board has no use for the shell line, and every extra thing on the wire is one more thing a leaked board leaks. -
POST /api/runnable/<name>/<start|stop>— does it.stopkills that session by exact name, and only a session on the list can be killed, so a board can never touch the work you did not write down. Asking twice is not an error.
Anything else still answers 403. With both permissions on, that key is refused files, sessions, the machine page, the config, ports, documents and every write — so the worst anything holding your board's config can do is start or stop what you listed.
may_stop_argus allows one more thing, and it is the only action that cannot be reversed
from the board: afterwards nothing is listening there.
Every tmux session carries on untouched — Argus attaches as a client, it is not their parent — but the way back to the page is a shell on that machine, or whatever supervises it. The reply says so, and the board asks twice before sending it.
It stands on its own: a machine with nothing worth publishing as runnable can still be one
you want to be able to switch off.
Two boxes on one wire where only one direction is open is the case announcing exists for, and there the same two actions arrive in the reply to the machine's own announcement — the only channel there is.
obey_board: true # off by default
board_may_stop_argus: falseobey_board is deliberately not implied by report_to: telling a board what you are doing
is not agreeing to take orders from it. Even on, the answer is bounded by the same list —
a reply can name one of your runnable entries, or ask for the server itself, and nothing
else.