Skip to main content
A plugin runs untrusted code next to a flying vehicle. The security model exists to make that safe: every plugin is sandboxed, every privileged call is gated by a capability the operator approved, and every archive is signed and checked before a single file lands on disk. This page lays out the boundary. It tells you what a plugin half can reach, what it cannot, and what the worst case looks like if a plugin turns out to be malicious or buggy. The mechanics live in Concepts, Permissions, the agent deep dive, and the GCS deep dive; this page is the threat-model summary that ties them together.

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 shared ados-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:
The host also serves the plugin document with a per-plugin Content Security Policy whose 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 is postMessage 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 an id and an optional required flag:
Required permissions are pinned on in the install dialog and the operator cannot install with them off; optional permissions default to off and the operator opts in. Declare the smallest set that makes your plugin work, mark only the genuinely load-bearing ones 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.
There are three trust tiers: 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.
The inprocess (agent) and inline (GCS) isolation levels import a plugin into the host’s own address space, so they skip the sandbox entirely. They are restricted to first-party signers for exactly that reason. A third-party manifest that declares either level is rejected at install. Never assume inprocess/inline is available to your plugin unless you sign with a first-party key.

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:
Use --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.
Updates re-open the gate when they ask for more. A newer version that requests a new optional permission re-prompts with only the new one highlighted; a new required permission must be approved before the update applies. Permissions dropped in an update are removed silently on load. An operator can revoke any granted capability at any time from the GCS or with 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.
A plugin that clears all four is one a malicious author cannot turn into a foothold: the worst it can do is the set of capabilities a human deliberately approved, inside a sandbox it cannot escape.

See also