ArombaDocsChangelogSecurityStatus

Docs

Aromba — Security

Aromba runs on the user’s server with real permissions and talks to an AI model. This document is the contract that makes that acceptable. Every rule here is enforced by code, not by prompts, and none of it depends on the user understanding it. Anything not listed as allowed is forbidden.

1. Threat model

What we defend against, in order of likelihood:

  1. The AI does something wrong — hallucinated command, bad advice, over-eager cleanup.
  2. Prompt injection — a log line, a web page, or a file contains text that tries to steer the AI.
  3. A user who taps “yes” without reading — most users. The design must keep them safe anyway.
  4. Telegram account compromise.
  5. Leaked AI key.
  6. Compromised Aromba update — our release channel is attacked, or we ship a bad version.
  7. Compromised aro.mba relay.

Not in scope: an attacker who already has root on the user’s server. Aromba can’t protect against that; it only must not make it easier.

2. Two processes: Worker and Warden

Aromba is two processes that do not trust each other.

WorkerWarden
Doeschores, bot, assistant, health collectorexecutes privileged actions, watches the Worker, enforces policy
Runs asuser aromba, no sudo, no privileges of its ownroot, its own systemd service; minimal code, no LLM, no network except its own channel to the relay (ladder alerts out, freeze signal in)
Canask the Warden to do things over a local socketrefuse, execute inside a cage, run for a core chore the few commands of section 5’s second table outside it, kill the Worker, lock itself down
Can touch the otherneverat any time

The Worker holds no power. Every command that changes the system — every red action, every privileged call, every chore run — is a request to the Warden. The Worker’s own code never imports os/exec, and CI checks every package of ours in the Worker binary for it; the SQLite library’s C-runtime shim (modernc.org/libc) imports it transitively for a system(3) emulation SQLite never calls. The Warden checks the policy (section 5), the approval (section 7), the cage (section 3), and then runs it. If the model, an injection, or a bug fully controls the Worker, they control a process that can only ask.

An assistant tool a skill brought runs through the Warden too, and is the smallest of these paths: a script in a cage with nothing writable at all, no approval and no PIN, because it cannot change anything. What the model passes it arrives as environment variables and never as arguments; the skill must be installed and on, the script must be the bytes it brought, the host must be able to build a cage at all, and the answer is capped and redacted. A tool that needs to write is a chore (PLAN D143).

The chores folder is root’s too, and the Worker cannot write a file into it. A chore the assistant wrote is saved by the Warden, which decides whether it may be: red, off, author: mine, a header that parses, a name that is lower-case letters, digits and dashes and can only be a file in mine/, and a name that is not already taken. None of those is a prompt’s word — a model that writes safety: green is refused, as is one that writes a path where a name goes. Saving changes nothing on the server: the chore is off until its owner turns it on, running it asks what every red chore asks, and lowering its colour is the PIN’s (PLAN 3.9).

The Warden’s binary, config and policy files are root-owned and unreadable to the Worker. The Worker reaches the Warden only over a root-owned Unix socket, and the Warden serves a request only if the peer’s uid (SO_PEERCRED) is the aromba user; terminal commands (aromba panic, resume, fence, pin) use a second socket that accepts only uid 0. aromba key and aromba calendar go to the Worker’s own socket, which answers them only to uid 0, so the Worker writes its own secrets and root never writes into the Worker’s folder (PLAN D101). The Warden verifies its own signature, the Worker’s, and that policy.toml matches the signed release, at start and hourly.

3. The cage: touches is enforced, not documented

Every chore runs in an isolated namespace (bubblewrap or equivalent) where only the paths listed in its touches header are writable; everything else is read-only, and the paths in section 4 are not mounted at all. A chore that declares “Docker cache” and writes to /home/james/clients gets a permission error, is killed, and the event is logged. What is made writable is the folder the Warden checked, not whatever its name points at a moment later: each touches path is opened with no symbolic link followed and mounted into the cage from that open handle, and below a user’s home no link is followed at all — a cache that is a link, or sits behind one, is left out of the run, which says so. The folders under a home are their owner’s; root’s cleanup is not theirs to aim.

Applies to every chore: core, community, assistant, run, and everything a skill brings. Core chores are caged too so we catch our own mistakes. A skill’s permissions (paths, root commands, the network, network hosts) are declared in its manifest, shown before install, and are the maximum any of its chores or tools can get; a skill cannot widen them after install — a version that names one more writable path or one more root command is installed only with --wider, and the PIN again. A declared root command is one of the binaries the chore-action table below already runs, and a red chore of that skill may ask for it exactly as a core chore does, arguments judged by the same grammar (PLAN D134). A skill’s chores run with no network at all unless its manifest says network = true (PLAN D101); every other chore keeps the host’s (D46). No chore can make a Unix socket: a connect to a daemon’s socket is a way out that a read-only mount does not stop (root’s tmux in /tmp, LXD’s under /var/snap, an abstract one for a chore that keeps the network), so every cage runs under a seccomp filter that refuses socket() for AF_UNIX, a Unix socketpair() that is not a stream or seqpacket pair, io_uring, and any call made for another architecture — wherever a socket is bound, and whenever (PLAN D106). A stream or seqpacket pair, which reaches nothing outside itself, still works. A refused call answers “Operation not permitted”, which is not a cage violation. A cage whose filter cannot be loaded is not built, and its chore does not run. The mail queues are fenced as data (/var/spool/postfix and its kin).

No namespaces, no cage, almost nothing runs. Some hosts (LXC guests, some container VPSes) cannot create namespaces. There the Warden runs only the green chores of the system skill and refuses every yellow, red, skill and mine/ chore: “This server has no isolation. I only run green system chores here.” There is no setting and no button to run uncaged; the Trust page shows isolation: none.

Damage cap: no chore may change more than 1,000 files or 20 GB in one run unless the header declares a higher max_changes and the user confirmed it. The Warden checks the cap every 5 seconds during the run and kills at it; in those 5 seconds a chore can exceed it. The exact count is taken after the run and journaled. SECURITY.md says this in the same words.

4. Files and fences

Never readable, never sent anywhere, redacted if they appear in any output:

Auto-fence. At first scan and weekly, the Warden finds data directories on its own and fences them (no read, no write, not mounted in any cage): database data dirs (Postgres, MySQL, Mongo, Redis dumps, SQLite files over 10 MB), uploads, storage, media, data, backups, mail spools, git repos with uncommitted changes. The user sees the fence list on the dashboard and can add with aromba fence <path>, or from the chat: a path named when Aromba asks what it must never touch is offered as a fence, and the tap adds it (PLAN D225). Adding only takes away what Aromba may do, so it needs no PIN; removing a fence needs the terminal.

Honeypots. The Warden plants decoy secrets in places nothing legitimate reads: ~/.aws/credentials.bak, /etc/<app>/.env.production, a fake key file in the web root. Any Aromba process opening one triggers step 3 of the ladder immediately (section 8). Decoys are regenerated on each install and never listed anywhere the Worker can see.

Redaction happens in the Warden before any tool result reaches the model or Telegram. What only the Worker holds — a connected calendar’s addresses and titles, and the titles of the user’s own calendar items (PLAN D101, D107) — the Worker redacts before it leaves the Worker. Redaction counts are logged.

5. Command policy

Three lists, checked by the Warden on every request:

Deny hits are logged, reported (“The assistant proposed a blocked command: …”), and count toward the ladder.

The Warden never runs a shell: an allowed request becomes a fixed binary path and argument list, with an empty environment, / as the working directory, a timeout and a cap on output bytes. Paths in arguments must be absolute and free of .., are resolved through every symlink, and the result is checked against the secret and fence rules — so a link from /var/log into ~/.ssh is refused like ~/.ssh itself. A listed binary asked to do something that is not a read (docker restart, systemctl restart, apt install) gets “ask”, not “deny”: it needs a button.

A chore has no button but its own approval, and the cage hides the sockets systemd and apt live behind. So a chore asks: read-only questions in its header (asks:), which the Warden answers from this same allow list before the run, and do= result lines after a clean real run, which the Warden judges against a second, shorter table it also renders here — systemctl restart <unit>, timedatectl set-ntp true, apt-get install …, and Docker’s leftovers, docker image prune -f and docker builder prune -f (PLAN D230: dangling images and the unused build cache, never -a, a volume or a container) — and runs outside the cage, one journal line and one receipt line each. Only a chore we ship may ask at all — a request is a write no button showed, so a community chore, the user’s own and a skill’s get none of this until a header can declare it to the user. A red chore may ask for any of them; a green chore only for the two time sync needs; a yellow one for none. A restart names one .service, never one whose restart is a reboot, a rescue shell, the bus or Aromba itself; apt-get only upgrades packages dpkg lists as installed here, or installs one of a short named list, never removes, never prompts. A request the deny list or the secret rules stop counts toward the ladder like the Worker’s would; from Freeze up nothing more is run. What apt installs is not snapshotted: the two chores that ask for it say on their button that Aromba cannot undo it, and from 3.4 on that needs the PIN like every action that cannot be undone (section 7).

The lists are compiled into the Warden. /etc/aromba/warden/policy.toml is the Warden’s own rendering of them, written at install, root-owned, part of the integrity check (section 8), and shown verbatim on the dashboard’s Trust page and in SECURITY.md; the Warden never reads it back — editing it changes nothing and trips the check. Only the two tables — the read-only allow list and the chore actions — are compiled in: fences, the PIN hash, prod mode, color overrides and the permissions of installed skills are data under /etc/aromba/warden/, root-owned, written only by the Warden, and covered by the same integrity check, which the Warden re-baselines after its own writes. aromba trust <chore> widens a chore’s rights only within the bounds of its skill’s manifest, never the allow list.

6. Prompt injection

7. Approvals, Telegram, and plain language

8. The ladder

The Warden watches Aromba, not the server, once a minute:

SignalThreshold
Commands requested by the Worker> 100 in 10 min, one step per crossing (PLAN D105)
Deny-list hits3 in an hour
Secret or fenced path access attempts> 2
Outbound connectionany host not in network.toml
Files changed after a choreanything outside its touches
Integritybinaries, config, policy.toml, or Warden state changed
Approvalsa red action run without a fresh, matching approval
Honeypotany read

Response goes up a ladder; each step messages the user:

  1. Slow — Worker rate-limited to one command per minute. Message: “Unusual activity, I’ve slowed down. Was that you?” [Yes, carry on] [Freeze]
  2. Freeze — scheduler and assistant stopped, no chores run, chat stays alive. [It’s me, resume] [Keep frozen]
  3. Cut — the Warden answers the Worker nothing but the read-only allow list, and wipes the AI key from memory and disk; the Worker is read-only. Root’s own commands at the terminal still work — root at the terminal is the root of trust (§1), and fencing a path again is how a person repairs an integrity finding. Resume only with aromba resume in the terminal; a resume from Cut restarts the Worker, which takes the relay’s key back on its next connection (PLAN D271).
  4. Disarm — Warden disables its own service and stops. What’s left is a Worker with no rights. Reinstall to recover.

Honeypot reads, integrity failures, and outbound to unknown hosts jump straight to step 3. Everything else climbs one step per trigger and climbs down after 24 quiet hours. /panic from Telegram = step 3; aromba panic from the terminal = step 4.

A false positive that climbs a step costs the user one tap; one that jumps to Cut costs a trip to the terminal, which is the price of a Cut nobody but root can undo. True positives cost the attacker the server. The honeypot cannot tell who opened a decoy, so anything run as root that reads the homes — a recursive search, a backup of /home — trips it too; that is kept on purpose (PLAN D214).

9. Safe by default, without understanding

10. Network

11. Updates and our own liability

12. Logging and audit

13. What we tell users

A SECURITY.md in the repo with sections 2, 3, 8 and 9 in plain words, policy.toml verbatim, and a one-line summary at the top: “Aromba starts with almost no rights, earns them over a week, and loses them the moment it behaves strangely. The AI proposes; a separate guard executes; you tap.” Plus a disclosure address and a promise to publish every security fix in the changelog.

What review has been done, and what has not (PLAN D148). Twenty-seven rounds of adversarial review have been run against this document and the Warden’s code, each round’s findings fixed before the next: v0.1.2 to v0.1.9 are named for them, and round 27 came back with no High or Medium finding (D104). An external review by somebody who breaks things for a living has not been done, and this launch does not wait for it. It is on the list of what happens after, and until it happens SECURITY.md says so in the same words.