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 halfGCS half
Where it runsThe drone or ground node, as a subprocessThe Mission Control browser, in an iframe
LanguagePython (ados.sdk) or Rust (ados-sdk)TypeScript (@altnautica/plugin-sdk)
Isolation primitivesystemd unit + ados-plugins.slice cgroup<iframe sandbox="allow-scripts">, null origin
Host channelUnix domain socket, length-prefixed msgpackpostMessage, same envelope shape
Resource capMemoryMax, CPUQuota, TasksMax from the manifestBrowser tab limits; CSP blocks egress
Privilege gateCapability token re-checked on every IPC callCapability token re-checked on every RPC

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:
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=strict
ProtectHome=yes
ReadWritePaths=/var/ados/plugin-data /var/log/ados/plugins /run/ados/plugins
LockPersonality=yes
RestrictRealtime=yes
RestrictSUIDSGID=yes
The plugin CANThe plugin CANNOT
Run its own code in its own processGain new privileges (NoNewPrivileges=yes blocks setuid escalation)
Reach the host through the IPC socketTouch the host filesystem outside its writable paths (ProtectSystem=strict, ProtectHome=yes make the rest read-only or invisible)
Write under /var/ados/plugin-data, its logs, and its run dirWrite anywhere else on disk (only the ReadWritePaths list is writable)
Use up to its declared RAM, CPU, and PID budgetExceed that budget (the kernel OOM-kills on memory, throttles on CPU, refuses new threads on PIDs)
Call a privileged host surface it was granted a capability forCall a surface it was not granted (the IPC server rejects with permission_denied before the handler runs)
Open outbound network connections if it declared network.outboundOpen arbitrary sockets without that capability
Share /tmp with the host or other pluginsSee a shared /tmp (PrivateTmp=yes gives it a private one)

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:
<iframe src="/plugins/<id>/index.html" sandbox="allow-scripts"></iframe>
The host also serves the plugin document with a per-plugin Content Security Policy whose connect-src 'self' is the second wall.
The plugin CANThe plugin CANNOT
Run JavaScript and render its own React UI in the iframeRead host cookies, localStorage, or the host DOM (no allow-same-origin, so it runs in a null origin)
Fetch its own bundle from /plugins/<id>/Fetch from the public internet (connect-src 'self' blocks it; route outbound calls through the agent half with network.outbound)
Call the host over postMessage for capabilities it was grantedRedirect the parent window (no allow-top-navigation)
Mount a panel into a UI slot it has the matching ui.slot.<id> grant forOpen new windows or submit native forms (no allow-popups, no allow-forms)
Receive theme variables and apply them in its own rootLie its way past a missing grant (the host re-resolves the required capability and ignores the envelope’s claimed one)

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:
agent:
  permissions:
    - id: telemetry.read
      required: true
    - id: recording.write
      required: false
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.
/etc/ados/plugin-keys/<signer-id>.pem      # trusted public keys
/etc/ados/plugin-revocations.json          # retired signer ids
There are three trust tiers:
TierHow it is recognizedWhat it unlocks
First-partySigner id on the hardcoded allowlist (altnautica-2026-A, altnautica-2026-B)The inprocess agent isolation and inline GCS isolation, in addition to everything below
Trusted third-partyPublic key present under /etc/ados/plugin-keys/, signer id not revokedNormal install, subprocess and iframe isolation only
UntrustedUnknown signer, or revoked signer, or unsignedRejected, unless the operator is in developer mode
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:
# Production: signature required, this fails for an unsigned archive
ados plugin install ./my-plugin.adosplug

# Developer mode only: skip signature verification for local iteration
ados plugin install ./my-plugin.adosplug --allow-unsigned
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