The two sandboxes at a glance
A plugin has up to two halves and each runs in its own sandbox. The two sandboxes are different technologies with the same goal: the plugin reaches the host only through one audited channel, and authority comes from granted capabilities, not from where the code happens to run.Agent half: what it can and cannot do
The agent half of a third-party plugin runs as a generated systemd service inside the sharedados-plugins.slice cgroup. Isolation is
mandatory; there is no in-process tier for third parties. The generated
unit runs as User=ados Group=ados with these hardening directives:
Blast radius if the agent half is malicious
The worst a compromised agent half can do is bounded by its granted capabilities. With only low-risk grants (telemetry.read,
recording.write) it can read telemetry and waste its own CPU budget,
and nothing else. A plugin granted high-risk capabilities is a
different matter: mavlink.write, vehicle.command,
flight.guided_setpoint, and estimator.pose.inject can each move the
vehicle. That is why those carry a high risk badge in the install
dialog and the operator has to approve them explicitly. The sandbox
contains a buggy or hostile plugin from reaching the rest of the box;
it does not pretend a granted flight-control capability is harmless.
GCS half: what it can and cannot do
The GCS half runs inside an iframe mounted with the strictest sandbox flags that still allow JavaScript:connect-src 'self' is the second wall.
Blast radius if the GCS half is malicious
A compromised GCS half is trapped in a null-origin iframe that cannot read the host session, cannot reach the public internet, and cannot navigate the parent page. Its only output ispostMessage envelopes,
and the host bridge re-resolves the capability each envelope actually
needs and checks it against the granted set. The damage ceiling is the
set of GCS capabilities the operator approved (for example command.send
to a paired drone, or mission.write to edit a plan). The iframe is the
reason the GCS half must never be your network egress point: even if you
wanted to phone home from the browser, the CSP forbids it.
Capability least privilege
Permissions are declared per half in the manifest. Each entry has anid and an optional required flag:
required, and
leave the rest optional so an operator can run a reduced version. There
are 43 agent capabilities and 21 GCS capabilities, each carrying a risk
level (low, medium, high, critical) that the install dialog badges. The
full catalog is on the Permissions page.
Capabilities are not just install-time flags. At runtime the supervisor
mints a capability token bound to the plugin id, the granted set, the
session, and an expiry, signed with HMAC-SHA256 and carrying a TTL (10
minutes by default). The plugin presents it on every privileged call and
the host re-checks the HMAC and the expiry on the critical path. Tokens
expire, bind to a session (so a stolen token does not work in a later
one), and let the host audit which call exercised which permission.
Three agent capabilities (event.publish, event.subscribe, and
hardware.gpio_out) are enforced at the IPC layer on every request.
The trust boundary: signed first-party vs third-party
Every.adosplug is an Ed25519-signed zip. On install the agent
recomputes the canonical payload hash (a SHA-256 over the path-sorted
entries, the signature file excluded), then verifies the signature
against its trust list before unpacking anything.
First-party status is granted only to ids on the in-code allowlist, not
by filename prefix, so an attacker who can write to
/etc/ados/plugin-keys/ still cannot plant a key that impersonates
first-party status. A signer id on the revocation list is refused even
if its public key is still present.
Unsigned plugins and developer mode
Signature verification can be turned off only for local development. The CLI install command accepts--allow-unsigned, and the host shows a
visible developer-mode indication while it is active. In any normal
install, an unsigned or unknown-signer archive is rejected:
--allow-unsigned only on a bench box you control. Anything you
distribute must be signed (ados plugin sign), and registry submission
requires a signed archive.
The operator install gate
No plugin installs silently. The install flow is two-stage so the operator sees exactly what they are approving before anything touches the disk:1
Parse in memory
The operator drags a
.adosplug into the install dialog. The host
validates the manifest against the schema, verifies the Ed25519
signature, and returns a summary with the requested permissions, the
signer id, and the risk level. Nothing is written to disk yet.2
Review and approve
The dialog renders the summary plus a permission grid badged by risk.
Required permissions are pinned on; optional ones the operator flips
as wanted. The operator clicks Install.
3
Commit
Only now does the host unpack the archive, generate the systemd unit
from the approved resource budget, and write the install record. A
plugin that never gets past review leaves no trace.
ados plugin perms <id> --revoke <cap>; the plugin loses
access on the next token rotation.
Why each layer exists
The layers are independent on purpose. If one fails, the next still holds.- Signing stops a tampered or unknown-origin archive from ever installing. It is the supply-chain wall.
- The sandbox (subprocess plus cgroup, or null-origin iframe plus CSP) bounds a plugin that did install but turns out to be buggy or hostile. It is the runtime wall.
- Capabilities make the plugin’s reach explicit and operator-approved rather than implicit. They are the authority wall.
- The install gate keeps a human in the loop for every grant, so no authority is handed out without someone seeing the risk badges first.